What is the meaning of stipend if transactions are always have gas limit set by sender?
Not really. When EOA calls contract A, the gas limit is set in the transaction, this is true. However, when contract A calls contract B, contract A may set limit on how much gas the contract B is allowed to spend. This limit may be lower than the remaining gas contract A has by itself at the moment. In such case, even if call to contract B will run out of gas and thus fail, contract A will still have some gas to handle the situation.
When, in what cases, does this damn 'stipend' apply?
There are several ways how one contract may call another contract, and these ways behave differently:
- One may just call other contract's method as a function. In this case called contract will be allowed to spend all the gas remaining in the transaction.
- One may use functions
call
, delegatecall
, or staticall
that allow explicitly specifying how much gas to called contract will be allowed to spend (though by default these functions allow spending all the gas remaining).
- One may use functions
transfer
of send
that allow called contract to spend at most 2300 gas, and this hardcoded value cannot be changed.
Could someone please provide a piece of code or describe a scenario when 2300 stipend is not enough?
Sure. Lets assume the following implementation of contract B:
contractB {
uint private etherReceived = 0;
function () external payable {
etherReceived += msg.value;
}
}
The contract tries to count how much Ether it ever received via fallback function. This implementation has a problem, as it updates storage variable inside fallback function and such update costs 5000 gas or more. Thus, transfer
to such contract from another contract will fail.
Could someone please provide a piece of code or describe a scenario when ... we set gasLimit which is enough to execute transfer function?
Function transfer
has hardcoded gas limit of 2300 which cannot be changed. However here is an example of a contract whose fallback function fits into this harsh limit and yet does something useful:
contractB {
event Deposit (address from, uint value);
function () external payable {
emit Deposit (msg.sender, msg.value);
}
}
First of all, let's create a basic re-entrancy vulnerable contract:
contract Safe {
mapping(address => uint) public userBalances;
function deposit() public payable {
userBalances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(userBalances[msg.sender] >= _amount, "low balance");
payable(msg.sender).call({value: _amount});
userBalances[msg.sender] -= _amount;
}
}
As you can see we didn't set the max gas that call
can use. What does it mean? You can restrict the gas amount that external function calls can use. Take a look at here. What if you did not set this? It can use all over of your gas (nearly 1m gas). Is it bad? It can be. Fallback functions are for what should contract do when it receives ether
(not just that but it is enough for now to now that).
We are a popular DeFi project. So, there will be thousands of ether in our contract.
This attacker creates a contract for attack to our Safe contract. And this contract has this function:
function attack() public onlyOwner {
Safe.deposit({value: 1 ether});
Safe.withdraw(1 ether);
}
This function is basically to deposit and withdraw 1 ether for the attacker's smart contract. And our attacker's smart contract also has a fallback function:
fallback() external payable {
Safe.withdraw(1 ether);
}
Okay, let's take a deep breath and look step by step what are we doing:
- Create an attacker contract
- Call attack function - Which means deposit and withdraw 1 ether.
- When we try to withdraw, the Safe contract tries to call our fallback function. And that function also tries to call withdraw function.
Can you see that? There is a loop. This loop will continue until all Safe contract funds are over. Because there will be an error. After then we will receive all the funds. Because our funds didn't decrease (because of our loop).
But if you'll change the call function line to this:
payable(msg.sender).call({value: _amount, gas: 30000});
Probably it will be okay (depending the gas calculation). But the best solution is first decrease userBalances, and after then send his money.
Best Answer
The EVM is withholding gas to starve attackers. That severely constrains what they can do before they
return
.Yes, it is bad form but the gas stimpend should protect this poorly-constructed function. The called contract will not have enough gas to change its state or very much at all.
That will pass all (99% ish) of the unspent gas to the called contract (attacker) and it could use the caller's inconsistent state to start bad things. The caller is vulnerable because the state is inconsistent. It has not, yet, updated data that should be updated.
send(), transfer() and call() hand over flow control to another contract, as do function calls, e.g. otherContract.doSomething(args). If the other contract is untrusted, meaning you did not write it yourself (many systems include multiple contracts for different concerns), then it is important to put the state in order first, then transfer flow control to the other contract.
The gas stipend (2300 gas) is a hack to protect some of the bad contracts some of the time. It is far better than you make good contracts all of the time.
Hope it helps.