# Nuanced EVM --- ## EVM Trivia - 0x4: the identity precompile. Returns whatever data gets sent. - **Question**: What is `extcodesize(0x4)`? --- ## Solidity Trivia ```solidity! interface MagicReturnCheck { // MUST return the magic value // `MagicReturnCheck.foo.selector` function foo(uint) external returns(bytes4); } function magicCheck() { require( MagicReturnCheck(address(0x4)).foo(0x1337) == MagicReturnCheck.foo.selector ); } ``` What happens when `magicCheck()` gets called? --- ## `returndatacopy` - Introduced in Byzantium HF, October 16th 2017. - Fully allowed dynamic types in ABI! ```solidity interface Nameable { function name() view returns(string memory); } // Without `returndatacopy`, the size needs // to be known at compile time. True dynamic // types were not possible string memory name = Nameable(...).name(); ``` --- ## Use case: Minimal Proxy Contract - One implementation, but several cheap clones. For example, a smart contract wallet. - A `delegatecall` forwader: copy all the `calldata` and forward it over. ```solidity! let s := delegatecall(gas(), addr, mPtr, mLength, 0, 0) // Copy the returndata buffer returndatacopy(0, 0, returndatasize()) // Based of the value of `s`, `revert` or `return` // with the returndata. ... ``` --- ## Nuance in `returndatacopy` - Out of bounds access. - `mload(offset)`, if `offset > msize()`, returns `0`. - `calldatacopy(mPtr, cdPtr, length)` if `cdPtr + length > calldatasize()`: empty values. --- ## `returndatacopy` Out of bound access in `returndatacopy(...)` reverts, unlike out of bounds access in `calldatacopy(...)` or `mload(...)` or `calldataload(...)` --- ## How did this happen? The original proposal suggested treating out-of-bounds reads to use 0 as the value: [EIP-211](https://github.com/ethereum/EIPs/pull/211). ![](https://i.imgur.com/3X2Ksdk.png) The EIP was updated to the new semantics: revert on out-of-bounds read. --- ## Bugs related to `returndata` buffer: Vyper's Clone - The old Vyper [`clone()`](https://github.com/vyperlang/vyper/blob/069936fa3fee8646ff362145593128d7ef07da38/vyper/functions/functions.py#L1471) method (`< 0.2.9`). - Psuedocode in Solidity: ```solidity! // Hardcoded offset let s := delegatecall(gas(), addr, memPtr, mLen, 0, 0x1000) // Returndatacopy was not used! // either `revert(0, 0)` or `return(0, 0x1000)` ``` - *Problem*: You always get `0x1000` bytes of data. --- ## Always return `0x1000` bytes of data - The ABI standard allows extra data. - For `uint x = I(...).f(...)`: only 32 bytes is needed, but returning more is accepted in high level Solidity. - But some applications likes to be more restrictive. --- ## Always return `0x1000` bytes of data - Example: some implementations of `SafeERC20` methods. If you used a ERC20 token that was an old Vyper `clone(...)` stricter ABI checks failed: bad for interoperability. --- ## `returndatacopy(...)` bug in solc's optimizer - The reverting semantics can be tricky to handle in the optimizer. - There is an optimizer step that removes memory copies if the copied region does not get used. - `returndatacopy(mPtr, rdPtr, length)`: can we remove this if we know memory between `[mPtr, mPtr + length)` is never read? --- ## The `solc` bug - The expression `returndatacopy(mPtr, rdPtr, length)` can only be removed if it there is no out-of-bounds `returndata` access. - `solc` [didn't include](https://github.com/ethereum/solidity/issues/13039) this check. - Deviation in semantics: unoptimized code can revert, but optimized code will not. --- ## The `solc` bug - Found while triaging a false positive produced by solc's Fuzzer! - Low impact: `returndatacopy(x, 0, returndatasize())`. - Problematic for literal length `returndatacopy(x, y, 32)`, which was rare. --- ## Calling an account without code ```solidity (bool s, bytes memory ret) = address(addr).call(data); ``` - What happens when `addr` does not have code? - What is the value of `s`? `true` or `false`? --- ## Calling an account without code - Low level calls to accounts without code succeeds! - High level calls will have an `extcodesize(...)` check to prevent this. - High level calls add `extcodesize(addr) > 0` before the calls. --- ## Can we optimize the `extcodesize` check? ``` (bool s, bytes memory ret) = address(addr).call(data); ``` - `extcodesize(...)`: extra 100 gas since EIP-2929, even more (700) in the past. - Observation: accounts without code cannot return data (?). - If `ret` needs to be used, then we may just skip `extcodesize(addr) > 0` check! --- ## The `extcodesize` optimization - Most external calls will be followed by decoding the `returndata`. `uint a = I(...).f(..);` - Accounts without code cannot return data. So the ABI decoding will fail in such case. - `extcodesize > 0` check can be skipped if function returns any data. --- ## Functions that return data are cheaper? - Yes! - Because `extcodesize > 0` check can be skipped (since Solidity 0.8.10). --- ## Pattern: return a magic value - Many EIP standards use a magic value to show success. --- ![](https://i.imgur.com/2aKedkF.png) --- ## Rule: "accounts without code cannot return data" - Is that really true? --- ## Extcodesize and Precompiles - `extcodesize`: what should be the `extcodesize` of precompiles? - Currently `0`, but this is deceiving. - These are accounts without code, but they can return data! --- ## Back to the Solidity trivia ```solidity! interface MagicReturnCheck { // Should return the magic value // `MagicReturnCheck.foo.selector` function foo(uint) external returns(bytes4); } function magicCheck() { require( MagicReturnCheck(address(0x4)).foo(0x1337) == MagicReturnCheck.foo.selector ); } ``` - What happens when `magicCheck()` gets called? - Before `0.8.10`, it reverts. After `0.8.10`, it succeeds. --- ### What can we learn from this? - Consistency v/s micro-optimizations. - Seeing the big picture v/s one small change. - A lot of rules in EVM has exceptions. - Design: try to avoid complexity if possible.
{"metaMigratedAt":"2023-06-17T22:00:41.245Z","metaMigratedFrom":"YAML","title":"Nuanced EVM","breaks":true,"contributors":"[{\"id\":\"fb4237dc-a2c5-447b-b203-6f7d1a2af9c5\",\"add\":7201,\"del\":1050}]"}
    824 views