Skip to content

Why does staticcall require using view even for precompiled contracts? #16378

@StackOverflowExcept1on

Description

@StackOverflowExcept1on

Abstract

It would be nice to be able to write ECDSA as pure function.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

/**
 * @dev Library for low-level memory interaction.
 */
library Memory {
    /**
     * @dev Allocates chunk of memory of unbounded size.
     * @dev Does not update free memory pointer.
     * @return memPtr Pointer to allocated memory.
     */
    function allocateUnbounded() internal pure returns (uint256 memPtr) {
        // https://github.com/ethereum/solidity/blob/v0.8.30/libsolidity/codegen/YulUtilFunctions.cpp#L3211
        assembly ("memory-safe") {
            memPtr := mload(0x40)
        }
    }

    /**
     * @dev Writes word to memory at given offset.
     * @param memPtr Pointer to memory.
     * @param offset Offset in memory.
     * @param value Word to write.
     */
    function writeWord(uint256 memPtr, uint256 offset, uint256 value) internal pure {
        // https://evm.codes/#52
        assembly ("memory-safe") {
            mstore(add(memPtr, offset), value)
        }
    }
}

/**
 * @dev Library for low-level ECDSA recovery.
 */
library ECDSA {
    /**
     * @dev Recovers Ethereum address from ECDSA signature.
     *      `ecrecover(e, v, r, s)` works according to formula $Q = r^{-1} \( sR - eG \)$
     *      from https://secg.org/sec1-v2.pdf#subsubsection.4.1.6.
     * @param memPtr Memory pointer for writing 128 bytes of input data.
     * @param e Message hash, can be any 256-bit number,
     *          will be reduced to valid scalar (can be zero scalar).
     * @param v Recovery ID, can be 27 or 28.
     *          Point `R(x, y)` has `yParity = v - 27`, `y` is calculated from `yParity`.
     * @param r Non-zero scalar r, must be in `[1, Secp256k1.N)` and `x = r` must be on curve.
     *          Point `R(x, y)` has coordinate `x = r`.
     * @param s Non-zero scalar s, must be in `[1, Secp256k1.N)`.
     * @return recovered 160-bit Ethereum address of `Q` point.
     * @dev If `v, r, s` do not satisfy above conditions, then `recovered = 0`
     */
    function recover(uint256 memPtr, uint256 e, uint256 v, uint256 r, uint256 s)
        internal
        view
        returns (uint256 recovered)
    {
        // https://github.com/ethereum/solidity/blob/v0.8.30/libsolidity/codegen/ir/IRGeneratorForStatements.cpp#L1653
        Memory.writeWord(memPtr, 0x00, e);
        Memory.writeWord(memPtr, 0x20, v);
        Memory.writeWord(memPtr, 0x40, r);
        Memory.writeWord(memPtr, 0x60, s);

        Memory.writeWord(0x00, 0x00, 0x00);

        // https://evm.codes/precompiled#0x01
        assembly ("memory-safe") {
            pop(staticcall(gas(), 0x01, memPtr, 0x80, 0x00, 0x20))
            recovered := mload(0x00)
        }
    }
}

contract Demo {
    constructor() payable {}

    // pure
    function recover1(bytes32 e, uint8 v, bytes32 r, bytes32 s) public pure returns(address) {
        return ecrecover(e, v, r, s);
    }

    // view
    function recover2(bytes32 e, uint8 v, bytes32 r, bytes32 s) public view returns(address) {
        uint256 memPtr = Memory.allocateUnbounded();
        return address(uint160(ECDSA.recover(memPtr, uint256(e), v, uint256(r), uint256(s))));
    }
}

Motivation

This can be useful for optimization. For example, suppose you're building a cryptographic library in Solidity. There's no point in using view if it's actually pure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions