Michael.W基于Foundry精读Openzeppelin第1期——Address.sol

  • Michael.W
  • 更新于 2024-01-06 12:59
  • 阅读 2720

从foundry工程化的角度详细解读Openzeppelin中的Address库及对应测试。

0. 版本

[openzeppelin]:v4.8.3,[forge-std]:v1.5.6

0.1 Address.sol

Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/utils/Address.sol

1. 目标合约:

封装Address library成为一个可调用合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/utils/MockAddress.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "openzeppelin-contracts/contracts/utils/Address.sol";

contract MockAddress {
    using Address for address;
    using Address for address payable;

    uint public slot0;
    address public slot1;

    function isContract(address target) external view returns (bool){
        return target.isContract();
    }

    function sendValue(address payable recipient, uint amount) external {
        recipient.sendValue(amount);
    }

    function functionCall(address target, bytes memory data) external returns (bytes memory){
        return target.functionCall(data);
    }

    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) external returns (bytes memory) {
        return target.functionCall(data, errorMessage);
    }

    function functionCallWithValue(
        address target,
        bytes memory data,
        uint value
    ) external returns (bytes memory){
        return target.functionCallWithValue(data, value);
    }

    function functionCallWithValue(
        address target,
        bytes memory data,
        uint value,
        string memory errorMessage
    ) external returns (bytes memory){
        return target.functionCallWithValue(data, value, errorMessage);
    }

    function functionStaticCall(address target, bytes memory data) external view returns (bytes memory){
        return target.functionStaticCall(data);
    }

    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) external view returns (bytes memory) {
        return target.functionStaticCall(data, errorMessage);
    }

    function functionDelegateCall(address target, bytes memory data) external returns (bytes memory) {
        return target.functionDelegateCall(data);
    }

    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) external returns (bytes memory){
        return target.functionDelegateCall(data, errorMessage);
    }
}

全部foundry测试合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/utils/Address.t.sol

2. 代码精读

2.1 isContract(address)

该方法是通过查看一个地址下code的长度来判断该地址是否是合约地址。如果传入地址为合约地址,返回true。

注意:但是有个特例:由于一个合约地址的code会在该合约的constructor函数执行完才被存储,所以如果另外一个合约在它的constructor函数中对该函数进行调用,那么返回值将是false。

代码解读

function isContract(address account) internal view returns (bool) {
        return account.code.length > 0;
}

如果单纯通过该方法的返回值来判断一个地址是否为合约地址其实是不安全的。以下情况该方法都会返回false:

  • EOA地址;
  • 一个处于constructor函数中的合约地址;
  • 即将在该地址上创建合约的地址(例如:利用salt提前计算出的地址);
  • 一个被destroy的合约地址。

foundry代码验证

contract AddressTest is Test {
    MockAddress testing = new MockAddress();
    SelfDestructorCase sdc;

    function setUp() external {
        // 由于一个合约的销毁会在一笔tx的执行结束时执行,所以将selfdestruct()的调用放在setUp()中
        // 这样在每个test用例中sdc合约已经是被selfdestruct了
        sdc = new SelfDestructorCase();
        // destruct the contract
        sdc.kill();
    }

    function test_IsContract() external {
        // contract address
        assertTrue(testing.isContract(address(this)));
        // eoa address
        assertFalse(testing.isContract(msg.sender));
    }

    function test_IsContract_SpecialCase_Constructor() external {
        // case 1: 处于constructor时期的合约地址,isContract()会返回false
        ConstructorCase cc = new ConstructorCase(testing);
        assertFalse(cc.flag());
    }

    function test_IsContract_SpecialCase_ContractDestroyed() external {
        // case 2: 销毁后的合约地址,无法被识别出来
        assertFalse(testing.isContract(address(sdc)));
    }
}

contract ConstructorCase {
    bool public flag;
    constructor(MockAddress ma){
        flag = ma.isContract(address(this));
    }
}

contract SelfDestructorCase {
    function kill() external {
        selfdestruct(payable(msg.sender));
    }
}

所以并不能通过isContract方法来地址闪电贷攻击!而且在合约开发中是不提倡禁止合约的调用,因为这样会使得很多合约钱包失效(比如Gnosis Safe)。

2.2 sendValue(address, uint256)

转账eth。

代码解读

function sendValue(address payable recipient, uint256 amount) internal {
        // 判断本合ETH余额是否满足转账需求
        require(address(this).balance >= amount, "Address: insufficient balance");
    // 通过.call()转账
        (bool success, ) = recipient.call{value: amount}("");
        // 如果转账失败,回滚交易
        require(success, "Address: unable to send value, recipient may have reverted");
}

注:普通的.transfer()转账有2300 gas的限制,而该方法则取消该限制。但是使用该方法时需要注意防止重入。

foundry代码验证

contract AddressTest is Test {
    MockAddress testing = new MockAddress();

    function test_SendValue() external {
        Receiver r = new Receiver();
        address payable recipient = payable(address(r));
        assertEq(recipient.balance, 0);
        vm.deal(address(testing), 1 ether);

        testing.sendValue(recipient, 1 ether);

        assertEq(recipient.balance, 1 ether);
        assertEq(address(testing).balance, 0);

        // revert check
        vm.deal(address(testing), 1 ether);
        // case 1: insufficient balance
        vm.expectRevert("Address: insufficient balance");
        testing.sendValue(recipient, 2 ether);

        // case 2: send eth to the contract with no revert in receive function
        r.setEthReceived(false);
        vm.expectRevert("Address: unable to send value, recipient may have reverted");
        testing.sendValue(payable(address(r)), 1 ether);
    }
}

contract Receiver {
    bool _ethReceived = true;

    function setEthReceived(bool ethReceived) external {
        _ethReceived = ethReceived;
    }

    receive() external payable {
        if (!_ethReceived) {
            revert("revert in receive function");
        }
    }
}

2.3 functionCall(address, bytes memory) && functionCall( address, bytes memory, string memory)

functionCall(address, bytes memory) :不带value值,传入目标合约地址和对应的payload进行底层的call调用。如果call调用的方法中产生revert,本函数将解析出该revert的信息并在本层以相同的内容引发revert(正常solidity的external调用就是这样的行为)。

要求:target地址必须为合约地址。

functionCall( address, bytes memory, string memory): 如果call调用的方法产生的revert不带msg,那么在本层指定msg抛出revert。

代码解读

function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        // 设置value为0,进行底层的call调用。如果call失败且无返回值,那么将引起"Address: low-level call failed"的revert
        return functionCallWithValue(target, data, 0, "Address: low-level call failed");
} 

// 如果call调用的方法产生的revert不带msg,那么在本层指定msg抛出revert。
function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        // 指定revert msg
        return functionCallWithValue(target, data, 0, errorMessage);
}

// 携带value进行call调用
function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        // 检查余额是否满足转账
        require(address(this).balance >= value, "Address: insufficient balance for call");
        // 携带calldata进行底层call
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        // 检查call的成功与否与返回值。如果是在call的函数中发生revert,会在本层获取该revert信息并引发revert(正常solidity的external调用就是这样)
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
 }

// 验证call的结果和返回值
// - 如果call成功:
//      - 如果有返回值,返回返回值;
//      - 如果无返回值,要求target必须是合约地址;
// - 如果call失败:
//      - 如果有返回值(返回值为call的方法中revert出的信息),从该返回值中获取revert的信息
//      - 如果无返回值,利用指定的errorMessage触发新的revert
function verifyCallResultFromTarget(
        address target,
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        // 如果call的返回是成功
        if (success) {
            // 如果call调用的函数没有返回值
            if (returndata.length == 0) {
                // 检查call的对象必须是一个合约
                require(isContract(target), "Address: call to non-contract");
            }
            // 返回call的具体的返回值
            return returndata;
        } else {
            // 如果call返回的是失败
            _revert(returndata, errorMessage);
        }
}

// 在call失败的情况下,从returndata中获取call的方法中引起的revert的信息。如果没有该信息,则引发一个errorMessage的revert
function _revert(bytes memory returndata, string memory errorMessage) private pure {
        // (在call失败的情况下)如果call的有返回值,说明call调用的方法中发生revert,revert的具体字符串描述被返回过来
        if (returndata.length > 0) {
            // 使用汇编来获取returndata中包裹的revert信息
            assembly {
                // returndata_size为returndata的数据长度,即returndata指针开始到其后32字节之间存储的指
                let returndata_size := mload(returndata)
                // 发生revert,revert的内容:从returndata指针后32字节作为起始位置,内容长度为returndata_size
                revert(add(32, returndata), returndata_size)
            }
        } else {
            // 如果没有返回值,说明call方法中并没有revert具体的信息返回,直接用errorMessage引起revert
            revert(errorMessage);
        }
}

foundry代码验证

contract AddressTest is Test {
    MockAddress testing = new MockAddress();

    function test_FunctionCall() external {
        Target t = new Target();
        bytes memory returndata = testing.functionCall(
            address(t),
            abi.encodeCall(t.setSlot0, (1024))
        );

        assertEq(t.slot0(), 1024);
        assertEq(abi.decode(returndata, (uint)), 1024 + 1);

        // check revert
        // case 1: revert if target is eoa
        vm.expectRevert("Address: call to non-contract");
        testing.functionCall(msg.sender, "");

        // case 2: revert with the bubbled revert msg
        vm.expectRevert("revert with msg");
        testing.functionCall(
            address(t),
            abi.encodeCall(t.revertWithMsg, ())
        );

        // case 3: revert with specific msg if the target function reverts with no msg
        vm.expectRevert("Address: low-level call failed");
        testing.functionCall(
            address(t),
            abi.encodeCall(t.revertWithNoMsg, ())
        );

        vm.expectRevert("specific revert msg");
        testing.functionCall(
            address(t),
            abi.encodeCall(t.revertWithNoMsg, ()),
            "specific revert msg"
        );
    }
}

contract Target {
    uint public slot0;

    function setSlot0(uint n) external returns (uint){
        slot0 = n;
        return n + 1;
    }

    function revertWithMsg() external payable {
        revert("revert with msg");
    }

    function revertWithNoMsg() external payable {
        revert();
    }

    function payableFunc() external payable returns (uint){
        return msg.value;
    }
}

2.4 functionCallWithValue(address, bytes memory, uint256) && functionCallWithValue(address, bytes memory, uint256, string memory)

functionCallWithValue(address, bytes memory, uint256)和functionCallWithValue(address, bytes memory, uint256, string memory) 就是带value的call调用,其他逻辑处理与functionCall(address, bytes memory)一致。只是functionCallWithValue(address, bytes memory, uint256, string memory)可以在call的目标函数发生revert无msg时指定本层的revert信息。

要求:target地址必须为合约地址。

代码解读(见1.3的代码解读)

foundry代码验证

contract AddressTest is Test {
    MockAddress testing = new MockAddress();

    function test_FunctionCallWithValue() external {
        Target t = new Target();
        vm.deal(address(testing), 1 ether);
        bytes memory returndata = testing.functionCallWithValue(
            address(t),
            abi.encodeCall(t.payableFunc, ()),
            1 ether
        );

        assertEq(abi.decode(returndata, (uint)), 1 ether);
        assertEq(address(t).balance, 1 ether);

        // check revert
        vm.deal(address(testing), 1 ether);
        // case 1: revert if target is eoa
        vm.expectRevert("Address: call to non-contract");
        testing.functionCallWithValue(msg.sender, "", 1 ether);

        // case 2: revert if insufficient balance
        vm.expectRevert("Address: insufficient balance for call");
        testing.functionCallWithValue(
            address(t),
            abi.encodeCall(t.payableFunc, ()),
            2 ether
        );

        // case 3: revert with the bubbled revert msg
        vm.expectRevert("revert with msg");
        testing.functionCallWithValue(
            address(t),
            abi.encodeCall(t.revertWithMsg, ()),
            1 ether
        );

        // case 4: revert with specific msg if the target function reverts with no msg
        vm.expectRevert("Address: low-level call with value failed");
        testing.functionCallWithValue(
            address(t),
            abi.encodeCall(t.revertWithNoMsg, ()),
            1 ether
        );

        vm.expectRevert("specific revert msg");
        testing.functionCallWithValue(
            address(t),
            abi.encodeCall(t.revertWithNoMsg, ()),
            1 ether,
            "specific revert msg"
        );
    }
}

contract Target {
    uint public slot0;

    function setSlot0(uint n) external returns (uint){
        slot0 = n;
        return n + 1;
    }

    function revertWithMsg() external payable {
        revert("revert with msg");
    }

    function revertWithNoMsg() external payable {
        revert();
    }

    function payableFunc() external payable returns (uint){
        return msg.value;
    }
}

2.5 functionStaticCall(address, bytes memory) && functionStaticCall(address, bytes memory, string memory)

向目标地址发起static call(不能在staticcall中修改状态变量,否则会revert)。其他逻辑处理与functionCall(address, bytes memory)一致。

要求:target地址必须为合约地址。

代码解读

// 向目标地址发起static call
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
 }

// 如果本static call的目标方法中产生revert无msg,则在本层发起指定msg为errorMessage的revert
function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        // 底层通过static call来实现合约间的调用
        (bool success, bytes memory returndata) = target.staticcall(data);
        // 检查static call的成功与否与返回值。如果是在static call的函数中发生revert,会在本层获取该revert信息并引发revert(正常solidity的external调用就是这样)
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
 }

foundry代码验证

contract AddressTest is Test {
    MockAddress testing = new MockAddress();

    function test_functionStaticCall() external {
        Target t = new Target();
        bytes memory returndata = testing.functionStaticCall(
            address(t),
            abi.encodeCall(t.slot0, ())
        );

        assertEq(abi.decode(returndata, (uint)), 1);

        // check revert
        // case 1: revert if target is eoa
        vm.expectRevert("Address: call to non-contract");
        testing.functionStaticCall(msg.sender, "");

        // case 2: revert with the bubbled revert msg
        vm.expectRevert("revert with msg");
        testing.functionStaticCall(
            address(t),
            abi.encodeCall(t.revertWithMsg, ())
        );

        // case 3: revert with specific msg if the target function reverts with no msg
        vm.expectRevert("Address: low-level static call failed");
        testing.functionStaticCall(
            address(t),
            abi.encodeCall(t.revertWithNoMsg, ())
        );

        // revert if the target function tries to modify the storage
        vm.expectRevert("specific revert msg");
        testing.functionStaticCall(
            address(t),
            abi.encodeCall(t.setSlot0, (1024)),
            "specific revert msg"
        );
    }
}

contract Target {
    uint public slot0 = 1;

    function setSlot0(uint n) external returns (uint){
        slot0 = n;
        return n + 1;
    }

    function revertWithMsg() external payable {
        revert("revert with msg");
    }

    function revertWithNoMsg() external payable {
        revert();
    }

    function payableFunc() external payable returns (uint){
        return msg.value;
    }
}

2.6 functionDelegateCall(address, bytes memory) && functionDelegateCall(address, bytes memory, string memory)

向目标地址发起delegate call(使用调用者的上下文)。其他逻辑处理与functionCall(address, bytes memory)一致。

要求:target地址必须为合约地址。

代码解读

// 向目标地址发起delegate call
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}

// 如果本delegate call的目标方法中产生revert无msg,则在本层发起指定msg为errorMessage的revert
function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        // 底层通过delegate call来实现合约间的调用
        (bool success, bytes memory returndata) = target.delegatecall(data);
        // 检查delegate call的成功与否与返回值。如果是在delegate call的函数中发生revert,会在本层获取该revert信息并引发revert(正常solidity的external调用就是这样)
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}

foundry代码验证

contract AddressTest is Test {
    MockAddress testing = new MockAddress();

    function test_functionDelegateCall() external {
        Target t = new Target();
        bytes memory returndata = testing.functionDelegateCall(
            address(t),
            abi.encodeCall(t.setSlot0AndReturnSlot1, (1024))
        );

        // return the value of slot 1 in contract MockAddress
        assertEq(abi.decode(returndata, (address)), address(0));
        // delegate call set slot 1 value in contract MockAddress
        assertEq(testing.slot0(), 1024);
        // slot 1 value in target contract not set
        assertEq(t.slot0(), 1);

        // check revert
        // case 1: revert if target is eoa
        vm.expectRevert("Address: call to non-contract");
        testing.functionDelegateCall(msg.sender, "");

        // case 2: revert with the bubbled revert msg
        vm.expectRevert("revert with msg");
        testing.functionDelegateCall(
            address(t),
            abi.encodeCall(t.revertWithMsg, ())
        );

        // case 3: revert with specific msg if the target function reverts with no msg
        vm.expectRevert("Address: low-level delegate call failed");
        testing.functionDelegateCall(
            address(t),
            abi.encodeCall(t.revertWithNoMsg, ())
        );

        vm.expectRevert("specific revert msg");
        testing.functionDelegateCall(
            address(t),
            abi.encodeCall(t.revertWithNoMsg, ()),
            "specific revert msg"
        );
    }
}

contract Target {
    uint public slot0 = 1;
    address public slot1 = address(1);

    function setSlot0(uint n) external returns (uint){
        slot0 = n;
        return n + 1;
    }

    function revertWithMsg() external payable {
        revert("revert with msg");
    }

    function revertWithNoMsg() external payable {
        revert();
    }

    function payableFunc() external payable returns (uint){
        return msg.value;
    }

    function setSlot0AndReturnSlot1(uint n) external returns (address){
        slot0 = n;
        return slot1;
    }
}

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

1.jpeg

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

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

0 条评论

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