It makes the call with 2800 gas.
Yes. I encountered this during Gnosis Safe audit (I looked through byte code, rather than Solidity text). Just tried this code:
pragma solidity ^0.5.0;
contract Sender {
function send(address payable target, uint256 amount) public {
target.transfer(amount);
}
}
If you perform
solc --asm Send.sol
look for the string â8fcâ (which is hex for 2300), and you will see the code that adds the stipend if amount is 0:
/* "Send.sol":108:131 target.transfer(amount) */
0x8fc
/* "Send.sol":124:130 amount */
dup3
/* "Send.sol":108:131 target.transfer(amount) */
swap1
dup2
iszero
mul
swap1
I agree - thatâs why I personally favor options 1 or 2, because they make the stipend invariant more explicit.
Personally I think thatâs quite a messy remediation - it adds a new opcode that exists solely to work around a bug elsewhere.
I see, it only adds to the amount if the value is 0. I have to say, this is pretty damn weird. Why did the Solidity authors feel it important to allow for calls with no value to log things?
Edit: Never mind, I get it. Itâs to work around the case of x.transfer(y)
where y happens to be 0 immediately failing due to out-of-gas.
It has been introduced because lot of people complained about failing âsendâ to contracts if the value is 0. So they would either ask everybody to enclose their âsendâ into
if amount > 0 {
target.send(amount)
}
Or make it for them, to stop them complaining
In general, and especially in light of Eth2 and Ewasm, I am not a fan of changing existing EVM semantics (introducing âbreaking changesâ) at this stage. Introducing a new opcode, instead of changing the behavior of an existing one, could have prevented this issue. I created a new thread to discuss this:
No need for that.
Have another fork, âConstantinople Againâ, that disables EIP-1283, and roll it out on testnets at first opportunity.
Then have another fork, âConstantinople Finallyâ, that re-enables EIP-1283, and enables an EIP that makes the âno SSTORE
if below 2300 gas leftâ invariant explicit.
On main-net, roll out all three forks at the same block.
If testnets are not deemed that important, can also combine âAgainâ and âFinallyâ into one.
For the record, this is more or less what @karalabe proposed, except as two forks rather than three.
Hereâs a snippet from py-evm
:
child_msg_gas = gas + (constants.GAS_CALLSTIPEND if value else 0)
(gas
and value
are arguments to CALL
.)
As shown in this discussion, reasoning gets a little more complicated when taking Solidityâs workaround to enable finger
ing into account:
// Provide the gas stipend manually at first because we may send zero ether.
// Will be zeroed if we send more than zero ether.
m_context << u256(eth::GasCosts::callStipend);
...
// gas <- gas * !value
m_context << Instruction::SWAP1 << Instruction::DUP2;
m_context << Instruction::ISZERO << Instruction::MUL << Instruction::SWAP1;
These lines are between ~2 and ~4 years old.
For ref: implementing commit, PR, and issue. Via the latter - people discussing the workaround. That links to a blog post, which should help find discussions where âa lot of people complainedâ (by reference search).
(This is getting awfully OT, sorry.)
I agree with @ajsutton
The only change to the existing opcode, though, is that it costs less gas in certain situations. If we werenât aware that could introduce a vulnerability, I donât think weâd even be considering it. And if we start introducing a new opcode for every gas cost change, weâll run out of opcodes very quickly.
- Cost could be 2300 gas and 2100 gas refund instead of just cost 200 gas
One more (much technically challenging) solution would be to assign EVM version and gas prices to contracts at deployment. That means, that smart contract that is deployed before the hard fork is always executed with old gas prices (and old features of EVM).
So, we will have EVM0 (pre Constantinople) and EVM1 (Constantinople). When a new contract (running EVM1) calls anything that is deployed before that, EVM1 communicates to EVM0, and the old contract will use old gas prices and old assumptions will stay the same. This communication isnât trivial, but since contracts have very specific interfaces it is not impossible.
Cons:
- more complicated codebase and testing;
- more complicated contract interaction;
- bloating codebase with any hardforks;
Pros:
- contracts that are already deployed aways will stay the same and behave the same;
- incentive for those who can to upgrade their contracts to the new version because cheaper gas, etc.
I still think that this might solve the whole class of problems like that and might be worth it in the long run because the contracts behaviour would be truly immutable.
From Dan Guido (@dguido, Trail of Bits) via gitter:
Hello all, we teamed up with ChainSecurity and Eveem to investigate the ramifications of EIP-1283 over the last 24 hours. We have collected our findings and recommendations into a document which you can find here: publications/reviews/EIP-1283.pdf at master ¡ trailofbits/publications ¡ GitHub
You will want to note a few key takeaways: We strongly recommend against adopting EIP-1283. If you must implement EIP-1283, then you should ensure that dirty storage is tracked per call, not per transaction. This will ensure that any call causing reentrancy will not be given a gas discount.
You will also note that we did find a few contracts that became vulnerable due to EIP-1283, however, our review was NOT exhaustive. All three of the analysis techniques that we used â ChainSecurity, Trail of Bits, and Eveem â have limitations.
I expect that weâll update the doc again tomorrow with some minor additional notes, recommendations, and findings.
This was a race to the finish line, and I think we all would have wanted to do more.
This is a reasonable idea - Iâll add it to the list. The main barrier is that it will require either a new consensus field for accounts, or some other means of communicating EVM versioning.
Itâs worth noting that this doesnât require two entirely separate EVMs, just some context that gets passed around for the current execution environment. Nodes already need most of this functionality to handle previous hard forks that have changed execution rules.
One way to handle this would be to introduce a new opcode, along these lines:
VERSION
: Pops one element from the stack and changes the execution environment to the specified version. Clears stack and local memory before handing control to the new version, which begins executing at the next PC value.
Each new contract would then start with a prologue along the lines of PUSH 1 VERSION
to enable the new EVM. This avoids the need to introduce new consensus data structures.
This can even be used for a transition to Web Assembly; contracts would just start with a prologue that switches the execution environment to EWASM.
Alternately, this could be a pseudo-opcode thatâs only valid at the start of a contract, for simplicity reasons.
Very well articulated. IMO, this is very similar to challenges faced by microprocessor companies when introducing or modifying architectural features. The underlying micro-architecture can/will change to improve performance/power but existing architectural interfaces/semantics will remain the same or are enhanced with new features via new opcodes.
Backward-compatibility and Interoperability are social contracts with developers/users. This may be viewed as legacy baggage but guarantees that code running on one processor will have the same behaviour on any future processor.
Gas usage is exposed to the contract and therefore could have been used to encode certain semantics in it. So changing it in either direction could break existing contracts. It might have been explicitly stated that this could change in future and hence do not rely on it; but if it has been exposed to the developer then thereâs no guarantee on how creatively it may have been used.
Versioning might get complicated but donât see how we can avoid it. Either we version the opcodes i.e. SSTORE, SSTORE2 (similar to CREATE, CREATE2) and run the risk of exhausting one-byte opcodes forcing us to go multi-byte and variable-length opcodes, or we version the EVM semantics. EVM versioning gives the most flexibility IMO.
It is, as many have pointed out, implied functionality. Saying that it was explicitly designed as an invariant is misleading. There was always an expectation that storage would change because of how economically significant it is and due to the complexity of the EVMâs stack-based execution model. This particular feature was added when the idea of logging was added, and yes, it was added to solve the simple problem âif I receive ether to my code account, how will I know?â. The invariant, if there is one, is: there is enough gas to emit a log when a code account receives ether. The invariant was never that a called code account cannot call another method that modifies storage. Just because the behaviour is default, does not mean that it is an invariant.
Although I agree with @AlexeyAkhunov, I think the best and safest option is 5: to require 5000 gas and refund 4800. This keeps the status quo (safe default) while also enabling all of the benefits of the EIP.