(番外篇)Michael.W基于Foundry精读Openzeppelin第22期——内联汇编staticcall

  • Michael.W
  • 更新于 2023-08-09 19:55
  • 阅读 1215

内联汇编中,Instruction "staticcall"的功能及使用方法与Instruction "call"类似。唯一不同的是在"staticcall"的过程中不允许发生storage的修改。

0. 版本

[forge-std]:v1.5.6

1. 关于内联汇编staticcall

内联汇编中,Instruction staticcall的功能及使用方法与Instruction call类似。唯一不同的是在staticcall中不允许发生storage的修改。

staticcall的使用方法如下:

assembly{
    staticcall(g, a, in, insize, out, outsize)
}

其中各个参数为:

  • g: 本次调用时设置的gas上限;
  • a: call的目标合约地址;
  • in: memory中的staticcall的calldata的起始位置;
  • insize: memory中的staticcall的calldata的长度;
  • out: memory中存放返回数据的起始位置;
  • outsize: memory中存放返回数据的长度。

需要注意的是:Instruction callcallcodedelegatecall在调用时的参数都与上述staticcall一样。其中,out和outsize是在memory中定义的用来存放返回数据的区域。这个区域的改写规则由返回数据的字节长度来决定:

  1. 当返回数据的字节长度>outsize时,只有mem [out...(out+outsize))会存储返回数据,剩下的返回数据需要通过Instruction returndatacopy来获取;
  2. 当返回数据的字节长度<outsize时,mem [out...(out+outsize))中前面会存储全部返回数据,剩余的mem保持不变;
  3. 当返回数据的字节长度=outsize时,mem [out...(out+outsize))中正好存储全部的返回数据。

2. foundry代码验证

全部foundry测试合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/extra/assembly/STATICCALL.t.sol

2.1 目标合约

contract Target {
    uint _n = 1024;
    address _addr = address(1);
    uint[] _arr = [1, 2];

    // 返回数据字节长度为32
    function getN() external view returns (uint){
        return _n;
    }

    // 返回数据字节长度为64
    function getNAndAddr() external view returns (uint, address){
        return (_n, _addr);
    }

    // 返回数据为动态数组
    function getArr() external view returns (uint[] memory){
        return _arr;
    }
}

2.2 返回数据字节长度为32

contract STATICCALLTest is Test {
    Target t = new Target();

    function test_Staticcall_ReturnUint() external {
        bytes memory encodedParams = abi.encodeCall(t.getN, ());
        address targetAddr = address(t);
        bytes32 outPtr;
        // case 1: right outsize for return data (uint)
        uint outSize = 0x20;
        bool success;
        uint returnSize;
        bytes32 originalValueOnOutPtr;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            originalValueOnOutPtr := mload(outPtr)
        }
        assertTrue(success);
        assertEq(returnSize, 0x20);
        uint n = t.getN();
        assertEq(n, uint(originalValueOnOutPtr));

        // case 2: insufficient outsize for return data
        outSize = 0x00;
        bytes32 outValueFromReturnDataCopy;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            originalValueOnOutPtr := mload(outPtr)
            // copy return data (returnSize bytes) with returndatacopy() to the outPtr
            returndatacopy(outPtr, 0x00, returnSize)
            outValueFromReturnDataCopy := mload(outPtr)
        }
        assertTrue(success);
        assertEq(returnSize, 0x20);
        assertNotEq(n, uint(originalValueOnOutPtr));
        assertEq(n, uint(outValueFromReturnDataCopy));

        // case 3: outsize more than the size of return data
        outSize = 0x40;
        bytes32 restBytesInOutSize;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            originalValueOnOutPtr := mload(outPtr)
            // outsize中多余出来的未使用的内容
            restBytesInOutSize := mload(add(outPtr, returnSize))
        }

        assertTrue(success);
        assertEq(returnSize, 0x20);
        assertEq(n, uint(originalValueOnOutPtr));
        // untouched
        assertEq(0, restBytesInOutSize);
    }
}

2.3 返回数据字节长度为64

contract STATICCALLTest is Test {
    Target t = new Target();

    function test_Staticcall_ReturnUintAndAddress() external {
        bytes memory encodedParams = abi.encodeCall(t.getNAndAddr, ());
        address targetAddr = address(t);
        bytes32 outPtr;
        // case 1: right outsize for return data (uint and address)
        uint outSize = 0x40;
        bool success;
        uint returnSize;
        bytes32 originalValueOnOutPtr;
        bytes32 nextWordOfOutPtr;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            // outsize为2个字,即32*2=64个字节(即0x40)
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            // outPtr开始,第一个字为第一个uint返回值,第二个字为第二个address返回值
            originalValueOnOutPtr := mload(outPtr)
            nextWordOfOutPtr := mload(add(outPtr, 0x20))
        }
        assertTrue(success);
        assertEq(returnSize, 0x40);
        (uint n, address addr) = t.getNAndAddr();
        assertEq(n, uint(originalValueOnOutPtr));
        assertEq(addr, address(uint160(uint(nextWordOfOutPtr))));

        // case 2: insufficient outsize for return data
        outSize = 0x20;
        bytes32 outValueFromReturnDataCopy;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            // outsize为1个字,即32*1=32个字节(即0x20)
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            // outPtr开始,第一个字为第一个uint返回值
            originalValueOnOutPtr := mload(outPtr)
            // 第二个字不再是第二个address返回值,因为outSize不够
            nextWordOfOutPtr := mload(add(outPtr, 0x20))
            // 从returndata中复制第二个字到outPtr指向的内存中
            returndatacopy(outPtr, 0x20, 0x20)
            // 从outPtr指向的内存中取出内容(即returndata中的第二个address返回值)
            outValueFromReturnDataCopy := mload(outPtr)
        }
        assertTrue(success);
        assertEq(returnSize, 0x40);
        assertEq(n, uint(originalValueOnOutPtr));
        // 由于outSize设置过小,outPtr开始的第二个字内容不是returndata中的第二个address返回值
        assertNotEq(addr, address(uint160(uint(nextWordOfOutPtr))));
        assertEq(addr, address(uint160(uint(outValueFromReturnDataCopy))));

        // case 3: outsize more than the size of return data
        outSize = 0x60;
        bytes32 restBytesInOutSize;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            // outsize为3个字,即32*3=96个字节(即0x60)
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            originalValueOnOutPtr := mload(outPtr)
            nextWordOfOutPtr := mload(add(outPtr, 0x20))
            // outsize中多余出来的未使用的内容
            restBytesInOutSize := mload(add(outPtr, returnSize))
        }
        assertTrue(success);
        assertEq(returnSize, 0x40);
        assertEq(n, uint(originalValueOnOutPtr));
        assertEq(addr, address(uint160(uint(nextWordOfOutPtr))));
        // untouched
        assertEq(0, restBytesInOutSize);
    }
}

2.4 返回数据为动态数组

contract STATICCALLTest is Test {
    Target t = new Target();

    function test_Staticcall_ReturnUintArr() external {
        bytes memory encodedParams = abi.encodeCall(t.getArr, ());
        address targetAddr = address(t);
        bytes32 outPtr;
        // case 1: right outsize for return data
        // (uint[] with 2 members —— 1(offset to start the array) + 1(length of array) + 2(2 words for 3 members) = 4)
        uint outSize = 0x80;
        bool success;
        uint returnSize;
        bytes32 originalValueOnOutPtr;
        bytes32 secondWordOfOutPtr;
        bytes32 thirdWordOfOutPtr;
        bytes32 forthWordOfOutPtr;
        bytes32 fifthWordOfOutPtr;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            // outsize为4个字,即32*4=128个字节(即0x80)
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            // outPtr开始,第一个字为动态数组真实数据的偏移值
            originalValueOnOutPtr := mload(outPtr)
            // 第二个字为动态数组长度
            secondWordOfOutPtr := mload(add(outPtr, 0x20))
            // 第三个字为第一个元素值
            thirdWordOfOutPtr := mload(add(outPtr, 0x40))
            // 第四个字为第二个元素值
            forthWordOfOutPtr := mload(add(outPtr, 0x60))
        }
        assertTrue(success);
        assertEq(0x80, returnSize);
        uint[] memory arr = t.getArr();
        // offset to start the uint array is 0x20
        assertEq(0x20, uint(originalValueOnOutPtr));
        // 动态数组长度
        assertEq(arr.length, uint(secondWordOfOutPtr));
        // 依次为2个元素的值
        assertEq(arr[0], uint(thirdWordOfOutPtr));
        assertEq(arr[1], uint(forthWordOfOutPtr));

        // case 2: insufficient outsize for return data
        // (uint[] with 2 members —— 1(offset to start the array) + 1(length of array) + 2(2 words for 3 members) = 4)
        outSize = 0x20;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            // outsize为1个字,即32*1=32个字节(即0x20)
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            // outPtr开始,第一个字为动态数组真实数据的偏移值
            originalValueOnOutPtr := mload(outPtr)
            // 第二个字不再是动态数组的长度,因为outSize不够
            secondWordOfOutPtr := mload(add(outPtr, 0x20))
        }
        assertTrue(success);
        assertEq(0x80, returnSize);
        // offset to start the uint array is 0x20
        assertEq(0x20, uint(originalValueOnOutPtr));
        // returndatacopy()之前,outPtr后的第二个字内容不是数组长度
        assertNotEq(arr.length, uint(secondWordOfOutPtr));

        assembly{
            // 从returndata中复制第二个至第四个字到outPtr+0x20开始的内存中
            returndatacopy(add(outPtr, 0x20), 0x20, sub(returnSize, 0x20))
            // outPtr后的第二个字为数组长度
            secondWordOfOutPtr := mload(add(outPtr, 0x20))
            // 第三个字为第一个元素值
            thirdWordOfOutPtr := mload(add(outPtr, 0x40))
            // 第四个字为第二个元素值
            forthWordOfOutPtr := mload(add(outPtr, 0x60))
        }
        // returndatacopy()之后,outPtr后的第二个字内容为数组长度
        assertEq(arr.length, uint(secondWordOfOutPtr));
        // returndatacopy()之后,outPtr后的第三个至第五个字内容依次为数组元素值
        assertEq(arr[0], uint(thirdWordOfOutPtr));
        assertEq(arr[1], uint(forthWordOfOutPtr));

        // case 3: outsize more than the size of return data
        // (uint[] with 2 members —— 1(offset to start the array) + 1(length of array) + 2(2 words for 3 members) = 4)
        outSize = (4 + 1) * 0x20;
        assembly{
            outPtr := mload(0x40)  // free memory pointer
            // outsize为5个字,即32*5=160个字节(即0xa0)
            success := staticcall(100000, targetAddr, add(encodedParams, 0x20), mload(encodedParams), outPtr, outSize)
            returnSize := returndatasize()
            // outPtr开始,第一个字为动态数组真实数据的偏移值
            originalValueOnOutPtr := mload(outPtr)
            // 第二个字为动态数组长度
            secondWordOfOutPtr := mload(add(outPtr, 0x20))
            // 第三个字为第一个元素值
            thirdWordOfOutPtr := mload(add(outPtr, 0x40))
            // 第四个字为第二个元素值
            forthWordOfOutPtr := mload(add(outPtr, 0x60))
            // 第五个字为outsize中未使用的空间
            fifthWordOfOutPtr := mload(add(outPtr, 0x80))
        }
        assertTrue(success);
        assertEq(0x80, returnSize);
        // offset to start the uint array is 0x20
        assertEq(0x20, uint(originalValueOnOutPtr));
        // 动态数组长度
        assertEq(arr.length, uint(secondWordOfOutPtr));
        // 依次为2个元素的值
        assertEq(arr[0], uint(thirdWordOfOutPtr));
        assertEq(arr[1], uint(forthWordOfOutPtr));
        // 第五个字为untouched
        assertEq(0, fifthWordOfOutPtr);
    }
}

ps:\ 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

1.jpeg

公众号名称:后现代泼痞浪漫主义奠基人

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Michael.W
Michael.W
0x93E7...0000
狂热的区块链爱好者