I only show this here for
transfer
, but it also applies tosend
.
// SPDX-License-Identifier: WTFPL
pragma solidity 0.8.29;
contract Victim {
mapping(address account => uint256 balances) public balances;
function deposit() external payable {
balances[msg.sender] = msg.value;
}
function withdraw() external {
payable(msg.sender).transfer(balances[msg.sender]);
balances[msg.sender] = 0;
}
}
Compile the Victim.sol
contract by invoking:
solc --experimental-eof-version 1 --evm-version osaka --optimize --via-ir --optimize-runs 200 --combined-json bin,opcodes Victim.sol
The contract bytecode:
ef00010100040200010011030001012b0400000000800002608060405234e100055f6080ee005f80fdef000101000402000100d504004300008000066080806040526004361015e100035f80fd5f3560e01c90816327e235e314e100785080633ccfd60b14e1002a63d0e30db014e100045fe0ffd55f600319360112e10010335f525f6020523460405f20555f80f35f80fd34e1003c5f600319360112e1002f335f525f60205260405f20545f8033f81515e10010335f525f6020525f60408120555f80f36040513d5f823e3d90fd5f80fd5f80fd34e100356020600319360112e100276004359060018060a01b03821680921415e100106020915f525f825260405f20548152f35f80fd5f80fd5f80fda3646970667358221220dd96c75f5604d0663a5ecb31aebef733729ab145614e82f0a46a84120c68f8316c6578706572696d656e74616cf564736f6c634300081d0041
The corresponding opcodes:
0xEF STOP ADD ADD STOP DIV MUL STOP ADD STOP GT SUB STOP ADD ADD 0x2B DIV STOP STOP STOP STOP DUP1 STOP MUL PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE RJUMPI 0x5 PUSH0 PUSH1 0x80 RETURNCONTRACT 0x0 PUSH0 DUP1 REVERT 0xEF STOP ADD ADD STOP DIV MUL STOP ADD STOP 0xD5 DIV STOP NUMBER STOP STOP DUP1 STOP MOD PUSH1 0x80 DUP1 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT ISZERO RJUMPI 0x3 PUSH0 DUP1 REVERT PUSH0 CALLDATALOAD PUSH1 0xE0 SHR SWAP1 DUP2 PUSH4 0x27E235E3 EQ RJUMPI 0x78 POP DUP1 PUSH4 0x3CCFD60B EQ RJUMPI 0x2A PUSH4 0xD0E30DB0 EQ RJUMPI 0x4 PUSH0 RJUMP 0xFFD5 PUSH0 PUSH1 0x3 NOT CALLDATASIZE ADD SLT RJUMPI 0x10 CALLER PUSH0 MSTORE PUSH0 PUSH1 0x20 MSTORE CALLVALUE PUSH1 0x40 PUSH0 KECCAK256 SSTORE PUSH0 DUP1 RETURN PUSH0 DUP1 REVERT CALLVALUE RJUMPI 0x3C PUSH0 PUSH1 0x3 NOT CALLDATASIZE ADD SLT RJUMPI 0x2F CALLER PUSH0 MSTORE PUSH0 PUSH1 0x20 MSTORE PUSH1 0x40 PUSH0 KECCAK256 SLOAD PUSH0 DUP1 CALLER EXTCALL ISZERO ISZERO RJUMPI 0x10 CALLER PUSH0 MSTORE PUSH0 PUSH1 0x20 MSTORE PUSH0 PUSH1 0x40 DUP2 KECCAK256 SSTORE PUSH0 DUP1 RETURN PUSH1 0x40 MLOAD RETURNDATASIZE PUSH0 DUP3 RETURNDATACOPY RETURNDATASIZE SWAP1 REVERT PUSH0 DUP1 REVERT PUSH0 DUP1 REVERT CALLVALUE RJUMPI 0x35 PUSH1 0x20 PUSH1 0x3 NOT CALLDATASIZE ADD SLT RJUMPI 0x27 PUSH1 0x4 CALLDATALOAD SWAP1 PUSH1 0x1 DUP1 PUSH1 0xA0 SHL SUB DUP3 AND DUP1 SWAP3 EQ ISZERO RJUMPI 0x10 PUSH1 0x20 SWAP2 PUSH0 MSTORE PUSH0 DUP3 MSTORE PUSH1 0x40 PUSH0 KECCAK256 SLOAD DUP2 MSTORE RETURN PUSH0 DUP1 REVERT PUSH0 DUP1 REVERT PUSH0 DUP1 REVERT LOG3 PUSH5 0x6970667358 0x22 SLT KECCAK256 0xDD SWAP7 0xC7 PUSH0 JUMP DIV 0xD0 PUSH7 0x3A5ECB31AEBEF7 CALLER PUSH19 0x9AB145614E82F0A46A84120C68F8316C657870 PUSH6 0x72696D656E74 PUSH2 0x6CF5 PUSH5 0x736F6C6343 STOP ADDMOD SAR STOP COINBASE
Now let's read EIP-7069: Revamped CALL instructions:
EXTCALL
(0xf8
) with arguments(target_address, input_offset, input_size, value)
EXTDELEGATECALL
(0xf9
) with arguments(target_address, input_offset, input_size)
EXTSTATICCALL
(0xfb
) with arguments(target_address, input_offset, input_size)
RETURNDATALOAD
(0xf7
) with argumentoffset
Furthermore, we should carefully read section Rationale:
One major change from the original
CALL
series of instructions is that the caller has no control over the amount of gas passed in as part of the call. The number of cases where such a feature is essential are probably better served by direct protocol integration.
The 63/64th rule is still applied, but - At least
MIN_RETAINED_GAS
gas is retained prior to executing the callee, - At leastMIN_CALLEE_GAS
gas is available to the callee.
Let's take a look at the Solidity compiler:
- Gas Stipend: https://github.com/ethereum/solidity/blob/ab55807c0d9198fb126442188d40fcab292a2acb/libevmasm/GasMeter.h#L149
- Relevant code block: https://github.com/ethereum/solidity/blob/ab55807c0d9198fb126442188d40fcab292a2acb/libsolidity/codegen/ir/IRGeneratorForStatements.cpp#L1625-L1657
- Relevant commit that introduced the EOF version of
transfer
andsend
: https://github.com/ethereum/solidity/commit/f495f676ac6749e219f40eca594f1c2f6f63a799
So TL;DR: Solidity removed the custom gas forwarding of 2,300
gas for transfer
and send
and forwards the remaining gas (subject to the 63/64th rule and the additional introduced logic). This means that transfer
and send
are not anymore safe from any reentrancy attacks!
You can see this behaviour in the above compiled contract. We know that the function signature of withdraw()
is:
bytes4(keccak256("withdraw()"));
➜ 0x3ccfd60b
Now search for 3ccfd60b
and step through the opcodes and you will find the EXTCALL
that is called as part of the transfer
function here:
... CALLER EXTCALL
Solidity has been aware of this for over a month, and if they cannot ensure the checks will work in EOF their proposed alternative is to take send and transfer out of eof targeted code. So this is a well understood problem with an in-place plan - ethereum/solidity#15310 (comment)
Also note that the PAY opcode, which was also brought up a month ago by another researcher on one of our calls, provides a different path to keep these constraints in place.