🌱
Dev Compendium
  • Ethereum
    • Solidity
      • EVM
      • Architecture
      • Execution Context
      • Transactions
      • Gas
      • Calldata, Memory & Storage
      • Gas Optimisation
      • Function Declarations
      • receive() & fallback()
      • CALL vs. DELEGATE CALL
    • Yul
      • Introduction
      • Types
      • Basic Operations
      • Storage
      • Memory
        • Arrays
        • Structs
        • Tuples, revert, keccak256
        • Logs and Events
        • Gotchas
        • abi.encode
      • Calldata
        • External Calls
        • Dynamic Length Inputs
        • Transferring Value
        • Receiving Contract Calls
      • Contracts in Yul
      • Other Yul Functions
    • Foundry
    • Security
      • Common Vulnerabilities
      • Best Practices
      • Development Workflow
      • Contract Migration
    • Auditing Tools
      • Slither
      • Mythril
      • Fuzzing
    • Upgradable Contracts
      • Upgrade Patterns
      • ERC-1967 Implementation
      • Deployment
    • MEV
    • Tooling
      • Chainlink
      • IPFS
      • Radicle
    • Frontend
      • Contract Hooks
      • Wallet Connection
        • wagmi.sh
        • Rainbow Kit
      • thirdweb
    • Protocol Research
      • Uniswap v2
      • Uniswap v3
      • Curve
      • GMX
  • Starkware
    • Fundamentals
    • Account Abstraction
    • Universal Deployer
    • Cairo 1.0
    • starknet.js
    • Security Model
  • Zero Knowledge
    • Group Theory
    • ECDSA
  • Rust
    • Basic Operations
    • Set up
    • Primitives
    • Control Flow
    • Mutability & Shadowing
    • Adding Behavior
    • Lifetimes
    • Std Library
  • SUI
    • Architecture
    • Consensus Mechanism
    • Local Node Setup
    • Sui Client CLI
    • Move Contracts
      • Move
      • Move.toml
      • Move.lock
      • Accessing Time in Sui Move
      • Set up Development Framework
      • Debug & Publish
      • Package Upgrades
      • Sui Move Library
      • Difference from Core Move
    • Object Programming
      • Object Basics
      • Using Objects
      • Immutable Objects
      • Object Wrapping
      • Dynamic Fields
      • Collections
      • Unit Testing
      • Deployment with CLI
  • NEAR
    • Architecture
    • Contract Standards
      • Fungible Token (NEP-141)
      • Non-Fungible Token (NEP-171)
      • Storage Management (NEP-145)
      • Events (NEP-297)
      • Meta-Transactions
    • Rust Contracts
      • Development Workflow
      • Smart Contract Layout
      • Storage Management
      • Events & Meta-transactions
      • Method Types
      • Upgrading Contracts
      • Unit Testing
    • NEAR Libraries
    • Environment Variables
    • Serialisation
    • Security Concepts
    • Collections
    • JS SDK
Powered by GitBook
On this page
  1. Ethereum
  2. Yul
  3. Calldata

External Calls

STATICCALL

contract SolidityContract {
	// "9a884bde": "get21()" - function signature 
    function get21() external pure returns (uint256) {
        return 21;
    }
} 
  • In Yul, we have to load the function selector into memory, then point to the region that will be part of our abi call, i.e. bytes 28 to 32 which contain the function signature

function externalViewCallNoArgs(address _a)
        external
        view
        returns (uint256)
    {
        assembly {
            mstore(0x00, 0x9a884bde)
            // 000000000000000000000000000000000000000000000000000000009a884bde
            //                                                         |       |
            //                                                         28      32
            let success := staticcall(gas(), _a, 28, 32, 0x00, 0x20)
            if iszero(success) {
                revert(0, 0)
            }
            return(0x00, 0x20)
        }
    }
  • Opcodes for making external calls: staticcall, call, and delegatecall

  • If we are inside of a view function, we have to use staticcall, because static calls do not change state. if state is being changed, the call will revert

  • The compiler will not allow us to use call within a view function, since the function is not supposed to modify state

  • Breakdown of arguments in staticcall :

    staticcall(gas(), _a, 28, 32, 0x00, 0x20)
    • gas() - amount of remaining gas the contract has left. can be hardcoded to a smaller amount, can be used if we don’t trust the receiving contract, since it may attempt to consume all the gas in a DoS attack

    • _a - address of the contract we are calling

    • 28, 32 - intended [tx.data](<http://tx.data>) , which is what we have loaded into memory

    • 0x00, 0x20 - region in memory that we are going to copy the results back into. so when the function returns, it will override the function selector in 0x00, but it does not matter since we don’t need this info any more

Getting return values from a revert call in Yul

// function to call: "73712595": "revertWith999()",
    function revertWith999() external pure returns (uint256) {
        assembly {
            mstore(0x00, 999)
            revert(0x00, 0x20)
        }
    }

// Yul calling function 
function getViaRevert(address _a) external view returns (uint256) {
        assembly {
            mstore(0x00, 0x73712595)
            pop(staticcall(gas(), _a, 28, 32, 0x00, 0x20)) // discard the return boolean value since we know it will be 0 / false
            return(0x00, 0x20) // returns 999 
        }
    }
  • when a function reverts, it will still return the revert values to the same location that we assigned to it, in this case 0x00 - 0x20

Calling a function that includes arguments

  • since the arguments take up more than 64 bytes, we cant put it in scratch space

  • have to store the function arguments into locations in memory with mstore

// function to call:"196e6d84": "multiply(uint128,uint16)",
function multiply(uint128 _x, uint16 _y) external pure returns (uint256) {
        return _x * _y;
    }

// Yul calling function 
function callMultiply(address _a) external view returns (uint256 result) {
        assembly {
            let mptr := mload(0x40)
            let oldMptr := mptr
            mstore(mptr, 0x196e6d84)
            mstore(add(mptr, 0x20), 3) // _x 
            mstore(add(mptr, 0x40), 11) // _y 
            mstore(0x40, add(mptr, 0x60)) // advance the memory pointer 3 x 32 bytes
            //  00000000000000000000000000000000000000000000000000000000196e6d84
            //  0000000000000000000000000000000000000000000000000000000000000003
            //  000000000000000000000000000000000000000000000000000000000000000b
            let success := staticcall(
                gas(),
                _a,
                add(oldMptr, 28), // fast forward to get the function signature 
                mload(0x40), // advanced memory pointer after arguments have been stored in memory 
                0x00,
                0x20
            )
            if iszero(success) {
                revert(0, 0)
            }

            result := mload(0x00)
        }
    }

CALL

  • Difference from staticcall is the addition of a callvalue() variable, which is used to forward the ETH received as part of the transaction (msg.value )

  • If a function is not payable, callvalue() can just be set to 0

// function to call : "4018d9aa": "setX(uint256)"
function setX(uint256 _x) external {
        x = _x;
    }

// Yul calling function 
function externalStateChangingCall(address _a) external {
        assembly {
            mstore(0x00, 0x4018d9aa)
            mstore(0x20, 999)
            // memory now looks like this
            // 0x000000000000000000000000000000000000000000000000000000004018d9aa
            // 000000000000000000000000000000000000000000000000000000000000000999
            let success := call(
                gas(),
                _a,
                callvalue(),
                28,
                add(28, 32),
                0x00,
                0x00
            )
            if iszero(success) {
                revert(0, 0)
            }
        }
    }

Unknown return size

  • in some situations, we don’t know what the size of the return value will be

  • if the function returns some unknown data size, e.g. a string or an array, we use returndatasize() to obtain the return data size, and ignore the allocated positions in staticcall

function unknownReturnSize(
        address _a,
        uint256 amount
    ) external view returns (bytes memory) {
        assembly {
            mstore(0x00, 0x7c70b4db)
            mstore(0x20, amount)

            let success := staticcall(gas(), _a, 28, add(28, 32), 0x00, 0x00)
            if iszero(success) {
                revert(0, 0)
            }

            returndatacopy(0, 0, returndatasize())
            return(0, returndatasize())
        }
    }
  • returndatacopy will copy the return value into a location in memory: in this case, copy into slot 0, the data at 0, up to the total size of the return data

DELEGATECALL

  • Openzeppelin implementation of delegatecall

function _delegate(address implementation) internal virtual {
        assembly {
            // Copy msg.data. We take full control of memory in this inline assembly
            // block because it will not return to Solidity code. We overwrite the
            // Solidity scratch pad at memory position 0.
            calldatacopy(0, 0, calldatasize())

            // Call the implementation.
            // out and outsize are 0 because we don't know the size yet.
						// msg.value: hardcoded to zero 
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

            // Copy the returned data.
            returndatacopy(0, 0, returndatasize())

            switch result
            // delegatecall returns 0 on error.
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }

PreviousCalldataNextDynamic Length Inputs

Last updated 1 year ago