您正在查看: Ethereum-新手教程 分类下的文章

实例验证 - call delegatecall

实例验证下call,delegatecall,两种方式下msg.sender和数据storage存储位置的区别

合约A

pragma solidity ^0.8.19;

contract A {
    address public temp1;
    uint256 public temp2;

    function three_call(address addr) public {
        (bool success, bytes memory result) = addr.call(
            abi.encodeWithSignature("test()")
        ); // 1
        require(success, "The call to B contract failed");
    }

    function three_delegatecall(address addr) public {
        (bool success, bytes memory result) = addr.delegatecall(
            abi.encodeWithSignature("test()")
        ); // 2
        require(success, "The delegatecall to B contract failed");
    }
}

合约B

pragma solidity ^0.8.19;

contract B {
    address public temp1;
    uint256 public temp2;

    function test() public  {
        temp1 = msg.sender;
        temp2 = 100;
    }
}

call 测试

B合约地址:0x086866663330344C7D1C51Bf19FF981AF3cB5782
A合约地址:0x05715D87C062B9685DD877d307b584bAbec964Ed
交易发起地址:0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776

执行前数据

A B
temp1 0x0000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000
temp2 0 0

A合约执行three_call,参数为B合约地址
执行后数据

A B
temp1 0x0000000000000000000000000000000000000000 0x05715D87C062B9685DD877d307b584bAbec964Ed
temp2 0 100

delegatecall 测试

B合约地址:0x27153EDA2E085534811b040f6062f6528D6B80a1
A合约地址:0xd3C23F354Ca2160E6dC168564AB8954146cF35C9
交易发起地址:0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776

执行前数据

A B
temp1 0x0000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000
temp2 0 0

A合约执行three_delegatecall,参数为B合约地址
执行后数据

A B
temp1 address: 0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776 0x0000000000000000000000000000000000000000
temp2 100 0

总结

  • A发起地址->B合约call->C合约
    msg.sender为B合约地址,非原始A发起地址,数据保存在C合约中
  • A地址->B合约delegatecall->C合约
    msg.sender为A发起地址,数据保存在B合约中

注意

callcode 已经在solidity 0.5+废弃,被delegatecall取代,故不做分析测试

TypeError: "callcode" has been deprecated in favour of "delegatecall".

参考
https://hicoldcat.com/posts/web3/senior-track-5/

实例验证 - 访问私有数据

方案的具体讲解来自《智能合约安全审计入门篇 —— 访问私有数据》,本片文章将在QEasyWeb3测试链上进行数据验证

测试合约

contract Vault {
    uint256 public count = 123;
    address public owner = msg.sender;
    bool public isTrue = true;
    uint16 public u16 = 31;
    bytes32 private password;
    bytes32[3] public data;
    struct User {
        uint256 id;
        bytes32 password;
    }
    User[] private users;
    mapping(uint256 => User) private idToUser;

    constructor(bytes32 _password) {
        password = _password;
    }

    function addUser(bytes32 _password) public {
        User memory user = User({id: users.length, password: _password});
        users.push(user);
        idToUser[user.id] = user;
    }
}

部署合约

通过https://remix.ethereum.org/ 进行部署,具体操作不做详细讲解

网络配置

QEasyWeb3的网络配置,可以查看《QEasyChain 测试链信息》

类型 RPC
https https://qeasyweb3.com
http http://qeasyweb3.com

chain id: 9528

合约操作

合约部署时,构造函数参数bytes32 _password,需要传入初始化参数,用于写入测试状态变量password
例如:0x4141414242424343430000000000000000000000000000000000000000000001

然后通过addUser再添加两个测试数据

  • 0x4141414242424343430000000000000000000000000000000000000000000002
  • 0x4141414242424343430000000000000000000000000000000000000000000003

执行完成后,开始slot的读取测试。

读取私有slot数据

为了简化操作,我们直接使用python3 + web3py进行测试脚本的编写, 也可以选用其他web3库,大同小异
我们上面合约部署完的合约地址是0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904

测试脚本

import time
from web3 import Web3

w3 = Web3(Web3.HTTPProvider('http://qeasyweb3.com'))

if __name__ == "__main__":
    for i in range(0,7):
        print("slot"+str(i)+" = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904",hex(i))))

执行后返回数据如下

slot0 = 0x000000000000000000000000000000000000000000000000000000000000007b
slot1 = 0x000000000000000000001f016bc0e9c6a939f8f6d3413091738665ad1d7d2776
slot2 = 0x4141414242424343430000000000000000000000000000000000000000000001
slot3 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot4 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot5 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot6 = 0x0000000000000000000000000000000000000000000000000000000000000002
  • slot0
    存储的为状态变量count的值,等于123的十六进制
  • slot1
    由于存储紧凑原则,当前slot存储了3部署数据,从右往左依次是
    • 状态变量owner的值,等于用于部署合约的msg.sender=0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776
    • 状态变量isTrue = 01 = true
    • 状态变量u16 = 1f = 31
  • slot2
    存储的为状态变量password
  • slot3
    存储的为状态变量data第1个存储值 = data[0]
  • slot4
    存储的为状态变量data第2个存储值 = data[1]
  • slot5
    存储的为状态变量data第3个存储值 = data[2]
  • slot6
    存储的为变长数组状态变量users的长度,当前已执行两次addUser

查看私有变长数组数据

上面我们从slot6中查询到了变长数组users的长度,下面我们继续查看users中存储的数据。

我们先再次回顾下变长数组的存储原则
对于变长数组,会先启用一个新的插槽 slotA 用来存储数组的长度,其数据存储在另外的编号为 slotV 的插槽中。slotA 表示变长数组声明的位置,用 length 表示变长数组的长度,用 slotV 表示变长数组数据存储的位置,用 value 表示变长数组某个数据的值

对应的代码逻辑

length = sload(slotA)
slotV = keccak256(slotA) + index// 索引下标
value = sload(slotV)

依据变长数组的存储以及紧凑打包的原则,所以对于前面两次addUser的数据存储槽位置为

  • user1.id == keccak256(slotA) + 0
  • user1.password == keccak256(slotA) + 1
  • user2.id == keccak256(slotA) + 2
  • user2.password == keccak256(slotA) + 3

我们继续使用python3 + web3py进行验证
先计算keccak256(slotA)

print("slot6-keccak = " +w3.toHex(Web3.keccak(hexstr="0x0000000000000000000000000000000000000000000000000000000000000006")))

得到

0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f

所以user1和user2的存储槽位置为

  • user1.id == 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f
  • user1.password == 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d40
  • user2.id == 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d41
  • user2.password == 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d42

通过测试脚本,获取上面存储槽位置的值

print("user1.id = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f")))
print("user1.password = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d40")))
print("user2.id = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d41")))
print("user2.password = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d42")))

得到数据如下

user1.id = 0x0000000000000000000000000000000000000000000000000000000000000000
user1.password = 0x4141414242424343430000000000000000000000000000000000000000000002
user2.id = 0x0000000000000000000000000000000000000000000000000000000000000001
user2.password = 0x4141414242424343430000000000000000000000000000000000000000000003

经测试与前面两次addUser的数据一致

总结

对于定长数组data,每个元素单独一个存储槽,所以data状态变量依次为slot3-5
对于变长数组users,会先启用一个新的插槽 slotA 用来存储数组的长度,其数据存储在另外的编号为 slotV 的插槽中。slotA 表示变长数组声明的位置,用 length 表示变长数组的长度,用 slotV 表示变长数组数据存储的位置,用 value 表示变长数组某个数据的值

hardhat config 中optimizer 优化选项

启用optimizer (优化器)后, 优化器会尝试去优化代码,如简化复杂的表达式等,例如常量表达式可能直接优化出计算,以便减少代码大小和执行成本。

runs 很多人有误解,它不是指优化器运行多少次,而是指合约函数未来可能会调用多少次,例如某些锁仓合约只会调用一次,可以将runs值设置为1。 这样编译器会尽可能小的字节码, 减少部署成本(运行时可能会更费 gas)。
如果合约会大量运行,如 token 合约,就可以设为较大的数值,编译器会优化以便在运行时尽可能低 gas 消耗,不过部署时的字节码可能大一些。

how to get eth_signTypedDataV4 signature in web3.py

EIP712 signatures can generated using the https://eth-account.readthedocs.io/en/stable/eth_account.html#eth_account.messages.encode_structured_data function to encode the EIP712 message, and use web3 to sign it.

msg = { "types": 
{ "EIP712Domain": [], "Type":  []},
  "domain":domain,
  "primaryType": 'Type',
  "message":message }

encoded_data=encode_structured_data(msg)
web3.eth.account.sign_message(encoded_data,privateKey)

https://ethereum.stackexchange.com/questions/114190/how-to-get-eth-signtypeddatav4-signature-in-web3-py