├── .DS_Store ├── .gitmodules ├── tutorials ├── 09_AbiEvent │ ├── img │ │ ├── 9-1.png │ │ ├── 9-2.png │ │ ├── 9-3.png │ │ ├── 9-4.png │ │ └── 9-5.png │ └── readme.md ├── 01_ValueStorage │ ├── .DS_Store │ ├── img │ │ └── 1-1.png │ └── readme.md ├── 03_BytesStorage │ ├── img │ │ ├── 3-1.png │ │ └── 3-2.png │ └── readme.md ├── 04_MemoryLayout │ ├── img │ │ ├── 4-1.png │ │ ├── 4-2.png │ │ ├── 4-3.png │ │ ├── 4-4.png │ │ ├── 4-5.png │ │ ├── 4-6.png │ │ ├── 4-7.png │ │ ├── 4-8.png │ │ └── 4-9.png │ └── readme.md ├── 06_AbiDynamic │ ├── img │ │ └── 6-1.png │ └── readme.md ├── 10_AbiError │ └── readme.md ├── 08_AbiCalldata │ └── readme.md ├── 05_AbiEncode │ └── readme.md ├── 02_MappingStorage │ └── readme.md └── 07_AbiFormula │ └── readme.md ├── foundry.toml ├── .gitignore ├── script └── Counter.s.sol ├── src ├── 08_AbiCalldata.sol ├── Counter.sol ├── 03_BytesStorage.sol ├── 05_AbiEncode.sol ├── 10_AbiErorr.sol ├── 01_ValueStorage.sol ├── 02_MappingStorage.sol ├── 07_AbiFormula.sol ├── 04_MemoryLayout.sol ├── 06_AbiDynamic.sol └── 09_AbiEvent.sol ├── test └── Counter.t.sol ├── .github └── workflows │ └── test.yml └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /tutorials/09_AbiEvent/img/9-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/09_AbiEvent/img/9-1.png -------------------------------------------------------------------------------- /tutorials/09_AbiEvent/img/9-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/09_AbiEvent/img/9-2.png -------------------------------------------------------------------------------- /tutorials/09_AbiEvent/img/9-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/09_AbiEvent/img/9-3.png -------------------------------------------------------------------------------- /tutorials/09_AbiEvent/img/9-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/09_AbiEvent/img/9-4.png -------------------------------------------------------------------------------- /tutorials/09_AbiEvent/img/9-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/09_AbiEvent/img/9-5.png -------------------------------------------------------------------------------- /tutorials/01_ValueStorage/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/01_ValueStorage/.DS_Store -------------------------------------------------------------------------------- /tutorials/01_ValueStorage/img/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/01_ValueStorage/img/1-1.png -------------------------------------------------------------------------------- /tutorials/03_BytesStorage/img/3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/03_BytesStorage/img/3-1.png -------------------------------------------------------------------------------- /tutorials/03_BytesStorage/img/3-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/03_BytesStorage/img/3-2.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-1.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-2.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-3.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-4.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-5.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-6.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-7.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-8.png -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/img/4-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/04_MemoryLayout/img/4-9.png -------------------------------------------------------------------------------- /tutorials/06_AbiDynamic/img/6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Solidity-Internals/HEAD/tutorials/06_AbiDynamic/img/6-1.png -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | **.DS_Store 5 | .DS_Store 6 | 7 | # Ignores development broadcast logs 8 | !/broadcast 9 | /broadcast/*/31337/ 10 | /broadcast/**/dry-run/ 11 | 12 | # Docs 13 | docs/ 14 | 15 | # Dotenv file 16 | .env 17 | -------------------------------------------------------------------------------- /script/Counter.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | 6 | contract CounterScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/08_AbiCalldata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract AbiCalldata { 5 | function testCalldata(uint256 x, bool y) public pure{ 6 | } 7 | 8 | function testReturndata() public pure returns(uint x, bool y){ 9 | x = 99; 10 | y = true; 11 | } 12 | } -------------------------------------------------------------------------------- /src/Counter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | contract Counter { 5 | uint256 public number; 6 | 7 | function setNumber(uint256 newNumber) public { 8 | number = newNumber; 9 | } 10 | 11 | function increment() public { 12 | number++; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/03_BytesStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract BytesStorage { 5 | string public shortString = "WTF"; 6 | bytes public longBytes = hex"365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3"; 7 | 8 | function getHash(bytes memory bb) public pure returns(bytes32){ 9 | return keccak256(bb); 10 | } 11 | } -------------------------------------------------------------------------------- /src/05_AbiEncode.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract AbiEncode { 5 | function testAbiEncode() public pure returns (bytes memory){ 6 | uint a = 1; 7 | uint8 b = 2; 8 | uint32[3] memory c = [uint32(3),4,5]; 9 | bool d = true; 10 | bytes1 e = hex"aa"; 11 | address f = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; 12 | return abi.encode(a, b, c, d, e, f); 13 | } 14 | } -------------------------------------------------------------------------------- /src/10_AbiErorr.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract AbiError { 5 | error InsufficientBalance(address from, uint256 required); 6 | 7 | // revert-error 8 | function testError(address addr, uint amount) public pure { 9 | revert InsufficientBalance(addr, amount); 10 | } 11 | 12 | // require 13 | function testRequire(address from) public pure { 14 | require(from == address(0), "Transfer Not Zero"); 15 | } 16 | } -------------------------------------------------------------------------------- /src/01_ValueStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract ValueStorage1 { 5 | uint256 public a = 5; 6 | uint256 public b = 2; 7 | } 8 | 9 | 10 | contract ValueStorage2 { 11 | uint128 public a = 5; 12 | uint64 public b = 2; 13 | uint32 public c = 3; 14 | uint64 public d = 1; 15 | } 16 | 17 | contract ValueStorage3 { 18 | struct S { uint64 b; uint32 c; } 19 | 20 | uint128 public a = 5; 21 | S public s = S(2, 3); 22 | uint64 public d = 1; 23 | } 24 | -------------------------------------------------------------------------------- /test/Counter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {Counter} from "../src/Counter.sol"; 6 | 7 | contract CounterTest is Test { 8 | Counter public counter; 9 | 10 | function setUp() public { 11 | counter = new Counter(); 12 | counter.setNumber(0); 13 | } 14 | 15 | function test_Increment() public { 16 | counter.increment(); 17 | assertEq(counter.number(), 1); 18 | } 19 | 20 | function testFuzz_SetNumber(uint256 x) public { 21 | counter.setNumber(x); 22 | assertEq(counter.number(), x); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /src/02_MappingStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract MappingStorage { 5 | mapping(uint => uint) public a; // slot 0 仅用作占位 6 | uint256 public b = 5; // slot 1 7 | 8 | constructor(){ 9 | a[0] = 1; // slot 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5 10 | a[1] = 2; // slot 0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7d 11 | } 12 | } 13 | 14 | contract ArrayStorage { 15 | uint128 public a = 9; // slot 0 16 | uint128[] public b; // slot 1 保存数组长度 17 | 18 | constructor(){ 19 | b.push(10); // slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 20 | b.push(11); // slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 21 | b.push(12); // slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf7 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/07_AbiFormula.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract AbiFormula { 5 | function testAbiUintTuple() public pure returns (bytes memory) { 6 | uint x = 1; 7 | return abi.encode((x)); 8 | } 9 | 10 | function testAbiArray() public pure returns (bytes memory) { 11 | uint[] memory x = new uint[](3); 12 | x[0] = 1; 13 | x[1] = 2; 14 | x[2] = 3; 15 | return abi.encode(x); 16 | } 17 | 18 | struct DynamicStruct { 19 | uint x; 20 | uint[] y; 21 | string z; 22 | } 23 | 24 | function testAbiDynamicArray() public pure returns (bytes memory) { 25 | uint x = 99; 26 | uint[] memory y = new uint[](3); 27 | y[0] = 1; 28 | y[1] = 2; 29 | y[2] = 3; 30 | string memory z = "WTF"; 31 | 32 | bytes memory encodedX = abi.encode(x); 33 | bytes memory encodedY = abi.encode(y); 34 | bytes memory encodedZ = abi.encode(z); 35 | 36 | return abi.encodePacked(encodedX, encodedY, encodedZ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/04_MemoryLayout.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract MemoryLayout { 5 | function testUint() public pure returns (uint){ 6 | uint a = 3; 7 | return a; 8 | } 9 | 10 | function testShortString() public pure returns (string memory){ 11 | string memory x = "WTF"; 12 | return x; 13 | } 14 | 15 | function testLongBytes() public pure returns (bytes memory){ 16 | bytes memory x = hex"365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3"; 17 | return x; 18 | } 19 | 20 | function testStaticArray() public pure returns (uint8[3] memory){ 21 | uint8[3] memory b = [1,2,3]; 22 | return b; 23 | } 24 | 25 | function testDynamicArray() public pure returns (uint[] memory){ 26 | uint[] memory x = new uint[](3); 27 | x[0] = 1; 28 | x[2] = 4; 29 | return x; 30 | } 31 | 32 | function testMultiDimensionalArray(string memory info, uint16 length) public pure returns (string[] memory){ 33 | string[] memory x = new string[](length); 34 | x[0] = info; 35 | x[1] = "HELLO"; 36 | x[length - 1] = "WTF"; 37 | return x; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/06_AbiDynamic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract AbiDynamic { 5 | function testAbiArray() public pure returns (bytes memory){ 6 | uint[] memory a = new uint[](3); 7 | a[0] = 1; 8 | a[1] = 2; 9 | a[2] = 3; 10 | return abi.encode(a); 11 | } 12 | 13 | function testAbiString() public pure returns (bytes memory){ 14 | string memory b = "WTF"; 15 | return abi.encode(b); 16 | } 17 | 18 | function testAbiStringStaticArray() public pure returns (bytes memory){ 19 | string[2] memory strings = ["WTF", "Academy"]; 20 | return abi.encode(strings); 21 | } 22 | 23 | 24 | function testAbiStringArray() public pure returns (bytes memory){ 25 | string[] memory strings = new string[](2); 26 | strings[0] = "WTF"; 27 | strings[1] = "Academy"; 28 | return abi.encode(strings); 29 | } 30 | 31 | struct DynamicStruct { uint a; uint[] b; string c; } 32 | 33 | function testAbiDynamicStruct() public pure returns (bytes memory){ 34 | uint a = 99; 35 | uint[] memory b = new uint[](3); 36 | b[0] = 1; 37 | b[1] = 2; 38 | b[2] = 3; 39 | string memory c = "WTF"; 40 | 41 | DynamicStruct memory ds = DynamicStruct(a, b, c); 42 | return abi.encode(ds); 43 | } 44 | } -------------------------------------------------------------------------------- /src/09_AbiEvent.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | contract AbiEvent { 5 | 6 | // 定义Transfer event,记录transfer交易的转账地址,接收地址和转账数量 7 | event Transfer(address indexed from, address indexed to, uint value); 8 | // 定义String event,分别以topic和data记录相同的string 9 | event String(string indexed strTopic, string strData); 10 | // 定义Array event,分别以topic和data记录相同的数组 11 | event Array(uint[] indexed arrTopic, uint[] arrData); 12 | // 定义匿名事件,它的日志中不包含事件哈希,最多拥有`4`个`indexed`参数 13 | event Anon(address indexed from, address indexed to, uint256 indexed num1, uint256 indexed num2) anonymous; 14 | 15 | function testEventTransfer( 16 | address from, 17 | address to, 18 | uint256 amount 19 | ) external { 20 | emit Transfer(from, to, amount); 21 | } 22 | 23 | function testEventString( 24 | string memory str 25 | ) external { 26 | emit String(str, str); 27 | } 28 | 29 | function testEventArray() external { 30 | uint[] memory x = new uint[](3); 31 | x[0] = 1; 32 | x[1] = 2; 33 | x[2] = 3; 34 | 35 | emit Array(x, x); 36 | } 37 | 38 | function testEventAnon( 39 | address from, 40 | address to, 41 | uint256 num1, 42 | uint256 num2 43 | ) external { 44 | emit Anon(from, to, num1, num2); 45 | } 46 | } -------------------------------------------------------------------------------- /tutorials/10_AbiError/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 10. Error的ABI编码 3 | tags: 4 | - solidity 5 | - abi 6 | --- 7 | 8 | # WTF Solidity内部标准: 10. Error的ABI编码 9 | 10 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 11 | 12 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 13 | 14 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 15 | 16 | 所有代码和教程开源在github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 17 | 18 | ----- 19 | 20 | 这一讲,我们将介绍Solidity的错误(error)的ABI编码规则。 21 | 22 | ## error 23 | 24 | 合约在运行发生故障时可以使用`REVERT`操作码终止运行并回滚交易,而且可以返回一个错误信息给调用者。这个错误信息的编码方式与`calldata`一致。 25 | 26 | ### 自定义错误 27 | 28 | 编码由`error`关键字定义的自定义错误信息时,前`4`字节为错误选择器。错误选择器与函数选择器类似,是错误签名的Keccak256哈希的前`4`字节,比如下面`InsufficientBalance`错误,错误签名为`InsufficientBalance(address,uint256)`,错误选择器为`0xf6deaa04`。 29 | 30 | ```solidity 31 | error InsufficientBalance(address from, uint256 required); 32 | 33 | // revert-error 34 | function testError(address addr, uint amount) public pure { 35 | revert InsufficientBalance(addr, amount); 36 | } 37 | ``` 38 | 39 | 对于函数输入 `000000000000000000000000d8b934580fce35a11b58c6d73adee468a2833fa8` 和 `1`,该错误信息的编码为: 40 | 41 | ``` 42 | 0xf6deaa04 43 | 000000000000000000000000d8b934580fce35a11b58c6d73adee468a2833fa8 44 | 0000000000000000000000000000000000000000000000000000000000000001 45 | ``` 46 | 47 | ### require 和 assert 48 | 49 | 自定义错误是在Solidity `0.8.4`版本加入的,之前只能用`require`和`assert`抛出一个字符串作为错误信息,那么它是如何编码的呢? 50 | 51 | Solidity规定了它们的错误签名为`Error(string)`,对应的选择器为`0x08c379a0`。 52 | 53 | ```solidity 54 | // require 55 | function testRequire(address from) public pure { 56 | require(from == address(0), "Transfer Not Zero"); 57 | } 58 | ``` 59 | 60 | 上面函数`require`抛出的错误会被编码为: 61 | 62 | ``` 63 | 0x08c379a0 64 | 0000000000000000000000000000000000000000000000000000000000000020 65 | 0000000000000000000000000000000000000000000000000000000000000011 66 | 5472616e73666572204e6f74205a65726f000000000000000000000000000000 67 | ``` 68 | 69 | ## 总结 70 | 71 | 这一讲,我们介绍了Solidity中错误的ABI编码规则,包括`0.8.4`新加入的自定义错误和以前`require`和`assert`抛出的字符串错误。 -------------------------------------------------------------------------------- /tutorials/08_AbiCalldata/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 08. calldata/returndata的ABI编码 3 | tags: 4 | - solidity 5 | - abi 6 | --- 7 | 8 | # WTF Solidity内部标准: 08. calldata/returndata的ABI编码 9 | 10 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 11 | 12 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 13 | 14 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 15 | 16 | 所有代码和教程开源在github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 17 | 18 | ----- 19 | 20 | 这一讲,我们将介绍Solidity的函数调用`calldata`和返回值`returndata`的ABI编码规则。 21 | 22 | ## calldata 23 | 24 | 当我们调用函数时,我们需要为要调用的函数名以及参数进行ABI编码。对于参数`a_1, ..., a_n`的函数`f`的调用会被编码为: 25 | 26 | ``` 27 | function_selector(f) e((a_1, ..., a_n)) 28 | ``` 29 | 30 | 其中`function_selector(f)`为函数的选择器(函数签名的`Keccak-256`哈希值的前`4`字节),`(a_1, ..., a_n)`为参数组成的元组,`e(x)`为变量`x`的ABI编码。 31 | 32 | 举个简单的例子: 33 | 34 | ```solidity 35 | function testCalldata(uint256 x, bool y) public pure{ 36 | } 37 | ``` 38 | 39 | 当我们调用上面的`testCalldata()`函数,参数为`99`(也就是`0x63`)和`true`时,会如何编码呢? 40 | 41 | 首先,我们需要计算这个函数的函数选择器,为`0xb3b1391c`。如果你不了解函数选择器,可以阅读[WTF Solidity教程第29讲](https://www.wtf.academy/solidity-advanced/Selector/)。 42 | 43 | 接下来我们需要编码`(x, y)`。由于`x`和`y`都是静态类型,这个元组也是静态类型,直接编码就可以。 44 | 45 | 因此,`calldata`的编码为: 46 | 47 | ``` 48 | 0xb3b1391c 49 | 0000000000000000000000000000000000000000000000000000000000000063 50 | 0000000000000000000000000000000000000000000000000000000000000001 51 | ``` 52 | 53 | ## returndata 54 | 55 | 当函数返回时,我们需要对返回值进行编码。返回值`a_1, ..., a_n`会被被编码为: 56 | 57 | ``` 58 | e((a_1, ..., a_n)) 59 | ``` 60 | 61 | 举个简单的例子: 62 | 63 | ```solidity 64 | function testReturndata() public pure returns(uint x, bool y){ 65 | x = 99; 66 | y = true; 67 | } 68 | ``` 69 | 70 | 当我们调用上面的`testReturndata()`函数,返回值是如何编码的? 71 | 72 | 其实很简单,返回值的编码就是`e((x, y))`,也就是上一节`calldata`编码去掉函数选择器的部分: 73 | 74 | ``` 75 | 0x 76 | 0000000000000000000000000000000000000000000000000000000000000063 77 | 0000000000000000000000000000000000000000000000000000000000000001 78 | ``` 79 | 80 | ## 总结 81 | 82 | 这一讲,我们介绍了Solidity合约的ABI编码公式,有了它,再复杂的编码也不怕。它的核心思想是使用递归的方法把复杂类型的编码转换成简单类型的编码。 -------------------------------------------------------------------------------- /tutorials/03_BytesStorage/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 03. 字节数组和字符串的存储布局 3 | tags: 4 | - solidity 5 | - storage 6 | - storage layout 7 | - mapping 8 | - dynamic array 9 | - state variables 10 | --- 11 | 12 | # WTF Solidity内部标准: 03. 字节数组和字符串的存储布局 13 | 14 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 15 | 16 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 17 | 18 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 19 | 20 | 所有代码和教程开源在github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 21 | 22 | ----- 23 | 24 | 这一讲,我们将介绍字节数组`bytes`和字符串`string`类型的状态变量是如何在合约中存储的。 25 | 26 | ## 字节数组和字符串 27 | 28 | 字节数组和字符串的编码是相同的,存储布局类似于`bytes1[]`(元素为1字节的动态数组),即有一个存放数组长度的槽和一个由哈希决定的数据区。但是对于长度小于`32`字节的短字节,元素与长度存储在同一个槽中。 29 | 30 | 我们以`BytesStorage`合约为例,其中声明了两个状态变量,一个为短字符串`"WTF"`,另一个为长字节`365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3`。 31 | 32 | 33 | ```solidity 34 | contract BytesStorage { 35 | string public shortString = "WTF"; 36 | bytes public longBytes = hex"365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3"; 37 | 38 | function getHash(bytes memory bb) public pure returns(bytes32){ 39 | return keccak256(bb); 40 | } 41 | } 42 | ``` 43 | 44 | ### 短字节 45 | 46 | 对于短字节/字符串(长度<=`31`字节),字符串的内容和长度会存到同一个槽中,从而节省gas。字符串的内容从最高阶字节开始存,而字节长度的两倍(`len * 2`)则会存储在最低阶的字节中。 47 | 48 | 上面合约中的`shortString`变量仅占用Slot 0,`"WTF"`字节长度为`3`,`ASCII`值为`575446`,在存储槽中表示为`5754460000000000000000000000000000000000000000000000000000000006`。 49 | 50 | ![](./img/3-1.png) 51 | 52 | ### 长字节 53 | 54 | 对于长字节/字符串(长度>`31`字节),存储方式与动态数组相同,只不过存储槽`p`存储的不是数组长度,而是`字符串长度 * 2 + 1`。这样,我们可以通过检查最低位是否为`0`来区分短字节(为`0`)和长字节(为`1`)。 55 | 56 | 上面合约中的`longBytes`变量的长度为`44`字节,`字符串长度 * 2 + 1`为`89`,写作十六进制为`0x59`,保存在Slot 1。接下来前`32`字节保存在Slot `keccak(1)`,后面的`12`个字节保存在Slot `keccak(1)+1`。 57 | 58 | 因此,`BytesStorage`合约的存储布局为: 59 | 60 | | Name | Slot | Value| 61 | |------|-----------------------------|----| 62 | | shortString | 0 | 5754460000000000000000000000000000000000000000000000000000000006 | 63 | | longBytes.length *2 +1 | 1 | 0x59| 64 | | longBytes.first | 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 | 0x365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d | 65 | | longBytes.second | 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf7 | 0x5f5f3e5f3d91602a57fd5bf30000000000000000000000000000000000000000 | 66 | 67 | 68 | > **注意:**字节/字符串与值变量不同,以左对齐的方式存储(从最高阶开始存),不够`32`字节的话右边补零 69 | 70 | ## 总结 71 | 72 | 这一讲,我们介绍了字节数组和字符串的存储布局,短字节和长字节有着不同的存储布局,需要注意。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WTF Solidity Internals 2 | 3 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 4 | 5 | 先修课程: 6 | 1. [WTF Solidity](https://github.com/AmazingAng/WTF-Solidity) 7 | 8 | ## 教程 9 | 10 | **第01讲:基础存储布局**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/01_ValueStorage.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/01_ValueStorage/readme.md) 11 | 12 | **第02讲:映射和动态数组的存储布局**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/02_MappingStorage.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/02_MappingStorage/readme.md) 13 | 14 | **第03讲:字节数组和字符串的存储布局**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/03_BytesStorage.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/03_BytesStorage/readme.md) 15 | 16 | **第04讲:内存布局**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/04_MemoryLayout.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/04_MemoryLayout/readme.md) 17 | 18 | **第05讲:ABI编码基础**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/05_AbiEncode.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/05_AbiEncode/readme.md) 19 | 20 | **第06讲:动态类型的ABI编码**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/06_AbiDynamic.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/06_AbiDynamic/readme.md) 21 | 22 | **第07讲:ABI编码公式**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/07_AbiFormula.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/07_AbiFormula/readme.md) 23 | 24 | **第08讲:calldata/returndata的ABI编码**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/08_AbiCalldata.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/08_AbiCalldata/readme.md) 25 | 26 | **第09讲:事件的ABI编码**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/09_AbiEvent.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/09_AbiEvent/readme.md) 27 | 28 | **第10讲:Error的ABI编码**:[Code](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/src/10_AbiError.sol) | [文章](https://github.com/WTFAcademy/WTF-Solidity-Internals/blob/master/tutorials/10_AbiError/readme.md) 29 | 30 | ## 运行 31 | 32 | ### 配置环境 33 | 34 | 要使用此模板,你需要安装以下内容。请按照链接和指示操作。 35 | 36 | - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 37 | - 如果你可以运行`git --version`,则说明你已正确安装。 38 | - [Foundry / Foundryup](https://github.com/gakonst/foundry) 39 | - 这将会安装`forge`,`cast`和`anvil` 40 | - 通过运行`forge --version`并获取类似`forge 0.2.0 (92f8951 2022-08-06T00:09:32.96582Z)`的输出,你可以检测是否已正确安装。 41 | - 要获取每个工具的最新版本,只需运行`foundryup`。 42 | 43 | ### 快速开始 44 | 45 | 1. 克隆[本仓库](https://github.com/WTFAcademy/WTF-Solidity-Internals)。 46 | 47 | 运行: 48 | 49 | ``` 50 | git clone https://github.com/WTFAcademy/WTF-Solidity-Internals 51 | cd WTF-Huff 52 | ``` 53 | 54 | 2. 安装依赖 55 | 56 | 克隆并进入你的仓库后,你需要安装必要的依赖项。为此,只需运行: 57 | 58 | ```shell 59 | forge install 60 | ``` 61 | 62 | 3. 打印合约存储布局 63 | 64 | 要打印合约存储布局,你可以运行: 65 | 66 | ```shell 67 | forge inspect ValueStorage3 storage-layout --pretty 68 | ``` 69 | 70 | 有关如何使用Foundry的更多信息,请查看[Foundry Github Repository](https://github.com/foundry-rs/foundry/tree/master/forge)和[foundry-huff library repository](https://github.com/huff-language/foundry-huff)。 71 | 72 | ## 参考 73 | 74 | - [Solidity-doc](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html) 75 | -------------------------------------------------------------------------------- /tutorials/05_AbiEncode/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 05. ABI编码基础 3 | tags: 4 | - solidity 5 | - abi 6 | --- 7 | 8 | # WTF Solidity内部标准: 05. ABI编码基础 9 | 10 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 11 | 12 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 13 | 14 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 15 | 16 | 所有代码和教程开源在github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 17 | 18 | ----- 19 | 20 | 合约ABI(Application Binary Interface,应用二进制接口)规范是在以太坊生态系统中与合约交互的标准方式,包括从区块链外部和合约间的交互。这一讲,我们将介绍ABI编码基础,主要介绍静态类型的ABI编码规范。 21 | 22 | ## ABI编码 23 | 24 | 合约ABI规范是在以太坊生态系统中与合约交互的标准方式,包括从区块链外部和合约间的交互。其中很重要的一项就是数据编码和解码:Solidity合约中的函数参数(`calldata`)和返回值(`returndata`)可能具有复杂的数据结构,包括结构体、数组和映射等。ABI 规范定义了如何对这些数据进行编码和解码,以便在合约之间传递和解释数据。这使得合约能够正确处理不同数据类型和数据结构,确保数据的完整性和一致性。 25 | 26 | ## 静态类型的ABI编码规范 27 | 28 | 我们可以把Solidity中的变量类型分为两组,静态类型和动态类型。动态类型包括: 29 | 30 | 1. `bytes`和`string`。 31 | 2. 动态数组。 32 | 3. 动态类型`T`的定长数组`T[k]`,其中`k > 0`。 33 | 4. 由任意动态类型构成的元组。 34 | 35 | 其余均为静态类型。静态类型是直接编码的,而动态类型是在当前块之后的一个单独分配的位置进行编码,我们会在之后的章节中详细介绍动态类型的编码。 36 | 37 | 在ABI编码时,我们通常以`32`字节为一个单位来编码数据。下面,我们介绍静态类型的ABI编码规范: 38 | 39 | 1. `uint`: `M`位的无符号整数,`M`的取值为`0`到`256`之间的可以整除8的整数,比如`uint8`,`uint32`,`uint256`(`uint`是`uint256`的同义词)。编码时,会在它们左侧补充若干`0`以使其长度成为`32`字节。 40 | 41 | 2. `address`: 与`uint160`的编码方式相同。`address payable`和`contract`类型的变量也使用相同的编码方式。 42 | 43 | 3. `bool`: `1`表示`true`,`0`表示`false`,编码方式与`uint8`的情况相同。 44 | 45 | 4. `bytes`:长度为`M`字节的字节数组,`0 < M <= 32`,编码时会在右侧补若干`0`使其长度成为`32`字节。 46 | 47 | 5. 静态定长数组`T[k]`: `T`为静态类型,比如`uint`,`k`为定长数组的长度,是正整数。它就像先将相同类型的`k`个元素分别编码,再顺序拼接到一起。 48 | 49 | 6. 静态元组`(T1,...,Tk)`: `T1,...,Tk`均为静态类型,它先将`k`个元素分别编码,再顺序拼接到一起。 50 | 51 | ### 示例 52 | 53 | ```solidity 54 | function testAbiEncode() public pure returns (bytes memory){ 55 | uint a = 1; 56 | uint8 b = 2; 57 | uint32[3] memory c = [uint32(3),4,5]; 58 | bool d = true; 59 | bytes1 e = hex"aa"; 60 | address f = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; 61 | return abi.encode(a, b, c, d, e, f); 62 | } 63 | ``` 64 | 65 | 在`testAbiEncode()`函数中,我们将`6`个类型不同的变量进行ABI编码(使用`abi.encode()`)并返回。下面,我们逐个分析。 66 | 67 | 1. `uint a = 1`,本身就是`32`字节,会被编码为`0000000000000000000000000000000000000000000000000000000000000001`。 68 | 2. `uint8 b = 2`,根据规则,会在左边填充`0`至`32`字节,被编码为`0000000000000000000000000000000000000000000000000000000000000002`。 69 | 3. `uint32[3] memory c = [uint32(3),4,5]`,静态定长数组,每个元素会单独编码,并拼接到一起。因此它会被编码为 70 | ``` 71 | 0000000000000000000000000000000000000000000000000000000000000003 72 | 0000000000000000000000000000000000000000000000000000000000000004 73 | 0000000000000000000000000000000000000000000000000000000000000005 74 | ``` 75 | 4. `bool d = true`,根据规则,会在左边填充`0`至`32`字节,被编码为`0000000000000000000000000000000000000000000000000000000000000001`。 76 | 5. `bytes1 e = hex"aa"`,定长字节数组,根据规则,会在右边填充`0`至`32`字节,被编码为`aa00000000000000000000000000000000000000000000000000000000000000` 77 | 6. `address f`,与`uint160`的编码方式相同,会在左边填充`0`至`32`字节,被编码为`0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71`。 78 | 79 | 因此,这个函数的返回值应该是: 80 | ``` 81 | 0x 82 | 0000000000000000000000000000000000000000000000000000000000000001 83 | 0000000000000000000000000000000000000000000000000000000000000002 84 | 0000000000000000000000000000000000000000000000000000000000000003 85 | 0000000000000000000000000000000000000000000000000000000000000004 86 | 0000000000000000000000000000000000000000000000000000000000000005 87 | 0000000000000000000000000000000000000000000000000000000000000001 88 | aa00000000000000000000000000000000000000000000000000000000000000 89 | 0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71 90 | ``` 91 | 92 | ## 总结 93 | 94 | 这一讲,我们介绍了Solidity合约的ABI编码基础,尤其是静态类型的编码。合约ABI规范是在以太坊生态系统中与合约交互的标准方式,理解它非常重要。 -------------------------------------------------------------------------------- /tutorials/09_AbiEvent/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 09. 事件的ABI编码 3 | tags: 4 | - solidity 5 | - abi 6 | --- 7 | 8 | # WTF Solidity内部标准: 09. 事件的ABI编码 9 | 10 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 11 | 12 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 13 | 14 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 15 | 16 | 所有代码和教程开源在github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 17 | 18 | ----- 19 | 20 | 这一讲,我们将介绍Solidity的事件(event)的ABI编码规则。 21 | 22 | ## event 23 | 24 | Solidity中的事件是对EVM日志的抽象。日志包含合约的地址(`address`), 一系列主题(`topics`,每个`32`字节,最多`4`个,)和任意长度的二进制数据(`data`)。如果你对事件不了解,可以阅读[WTF Solidity教程第12讲](https://www.wtf.academy/solidity-start/Event/)。 25 | 26 | 最常见的事件之一就是`ERC20`的`Transfer`事件,我们以它为例讲解Solidity事件的ABI编码规则: 27 | 28 | 29 | ```solidity 30 | event Transfer(address indexed from, address indexed to, uint value); 31 | ``` 32 | 33 | 1. `address`: 合约的地址,自动从EVM读取,不需要开发者提供。对于`Transfer`事件,`address`就是该`ERC20`的合约地址 34 | 35 | 2. `topics[0]`: 第一个主题为`32`字节的事件哈希,也就是事件签名的`keccak256`哈希。与函数选择器类似,但它是`32`字节,而非`4`字节。举个例子,`Transfer`事件的事件哈希为(`uint`要写成`uint256`): 36 | 37 | ```solidity 38 | keccak256("Transfer(address,address,uint256)") 39 | 40 | //0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef 41 | ``` 42 | 43 | 3. `topics[n]`: 第`n`个主题保存事件中第`n-1`个`indexed`字段。以`Transfer`事件为例,它的第`2`个主题就是`from`地址,第`3`个主题就是`to`地址。对于简单类型,它们会以ABI编码规则进行编码,直接保存在主题中;对于复杂类型,比如`string`/`bytes`/数组/结构体,按照特定的方式进行编码(与ABI编码有所不同,后面会讲),计算`keccak256`哈希,最后把哈希保存在主题中。 44 | 45 | 4. `data`: 事件的数据部分就是把所有非`indexed`数据组成的元组进行ABI编码并保存,就像上一讲介绍的`returndata`那样。比如`WTF`会被编码为: 46 | 47 | ![](./img/9-1.png) 48 | 49 | 下面是一个`Transfer`事件的例子: 50 | 51 | ```solidity 52 | function testEventTransfer( 53 | address from, 54 | address to, 55 | uint256 amount 56 | ) external { 57 | emit Transfer(from, to, amount); 58 | } 59 | ``` 60 | 61 | ![](./img/9-2.png) 62 | 63 | 64 | 65 | ## 复杂类型的主题编码 66 | 67 | 复杂类型作为事件的`indexed`参数时,我们会将它按照特殊规则编码,然后计算`keccak256`哈希,最后把哈希保存在主题中。特殊编码规则如下: 68 | 69 | ### 1. bytes和string 70 | 71 | `bytes`和`string`编码时只保存字符串的内容,没有任何填充或长度前缀。比如`WTF`会被编码为`0x575446`,它的哈希为:`0x07aa5e15d4e202938899c76793e1e80b05efcb9cf1548dc9e34fa8813466f4f1`。 72 | 73 | ```solidity 74 | // 定义String event,分别以topic和data记录相同的string 75 | event String(string indexed strTopic, string strData); 76 | 77 | function testEventString( 78 | string memory str 79 | ) external { 80 | emit String(str, str); 81 | } 82 | ``` 83 | 84 | ![](./img/9-3.png) 85 | 86 | 87 | ### 2. 结构体 88 | 89 | `struct`结构体的编码时其成员编码的串联,其中每个成员都会填充为`32`字节的倍数(包括`bytes`/`string`成员)。 90 | 91 | ### 3. 数组 92 | 93 | 数组(包括静态和动态)仅是其元素编码的串联,不包含任何数组长度或偏移量的前缀,其中每个成员都会填充为`32`字节的倍数(包括`bytes`/`string`成员)。比如下面的`uint[]`类型的变量,值为`[1, 2, 3]`,会被编码为 94 | 95 | ``` 96 | 0x 97 | 0000000000000000000000000000000000000000000000000000000000000001 98 | 0000000000000000000000000000000000000000000000000000000000000002 99 | 0000000000000000000000000000000000000000000000000000000000000003 100 | ``` 101 | 102 | 它的哈希为`0x07aa5e15d4e202938899c76793e1e80b05efcb9cf1548dc9e34fa8813466f4f1`。 103 | 104 | ```solidity 105 | // 定义Array event,分别以topic和data记录相同的数组 106 | event Array(uint[] indexed arrTopic, uint[] arrData); 107 | 108 | function testEventArray() external { 109 | uint[] memory x = new uint[](3); 110 | x[0] = 1; 111 | x[1] = 2; 112 | x[2] = 3; 113 | 114 | emit Array(x, x); 115 | } 116 | ``` 117 | 118 | ![](./img/9-4.png) 119 | 120 | 121 | ## 匿名事件 122 | 123 | 匿名事件(anonymous event)是一种特殊的事件,不常用。它的日志中不存储事件哈希,因此它最多可以拥有`4`个`indexed`参数。你需要用`anonymous`关键字声明它: 124 | 125 | ```solidity 126 | // 定义匿名事件,它的日志中不包含事件哈希,最多拥有`4`个`indexed`参数 127 | event Anon(address indexed from, address indexed to, uint256 indexed num1, uint256 indexed num2) anonymous; 128 | 129 | function testEventAnon( 130 | address from, 131 | address to, 132 | uint256 num1, 133 | uint256 num2 134 | ) external { 135 | emit Anon(from, to, num1, num2); 136 | } 137 | ``` 138 | 139 | ![](./img/9-5.png) 140 | 141 | ## 总结 142 | 143 | 这一讲,我们介绍了Solidity事件的ABI编码规则,包括主题和数据部分的编码,以及匿名事件。 -------------------------------------------------------------------------------- /tutorials/04_MemoryLayout/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 04. 内存布局 3 | tags: 4 | - solidity 5 | - memory 6 | --- 7 | 8 | # WTF Solidity内部标准: 04. 内存布局 9 | 10 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 11 | 12 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 13 | 14 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 15 | 16 | 所有代码和教程开源在github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 17 | 18 | ----- 19 | 20 | 这一讲,我们将介绍Solidity中的变量是如何在内存中保存的。 21 | 22 | ## EVM的内存 23 | 24 | EVM使用内存来支持交易执行期间的数据存储和读取。EVM的内存是一个线性寻址存储器,你可以把它理解为一个动态字节数组,可以根据需要动态扩展。它支持以8或256 bit写入(`MSTORE8`/`MSTORE`),但只支持以256 bit读取(`MLOAD`)。 25 | 26 | 需要注意的是,EVM的内存是“易失性”的:交易开始时,所有内存位置的值均为0;交易执行期间,值被更新;交易结束时,内存中的所有数据都会被清除,不会被持久化。如果需要永久保存数据,就需要使用EVM的存储。 27 | 28 | ![](./img/4-1.png) 29 | 30 | ## 内存布局 31 | 32 | 相比于存储,在内存上写入和读取数据要便宜的多,因此我们不需要像使用存储那样节省,可以豪横一点。这主要体现在我们不会将多个变量压缩保存在同一个内存槽中。 33 | 34 | 在内存中读写数据便宜的原因有两个:一是数据分别放在各自的内存槽中,而不是尽量压缩在一个槽中。如果是后者,那EVM每次使用数据时就会多一步操作,从槽中截取出真正自己要使用的数据,也就有了更多的gas消耗。二是内存空间是临时的,使用完就会释放,并不像Storage存储那样永久占用链上空间。因此,数据各自占用一个槽位,不仅带来了计算时的便利,又不会真正占用存储空间,gas费自然就低。 35 | 36 | ### 预留插槽 37 | 38 | Solidity保留了前4个内存插槽,每个插槽32字节,用于特殊目的: 39 | 40 | 1. `0x00` - `0x3f` (64字节):用于哈希方法的临时空间,比如读取mapping里的数据时,要用到key的hash值,key的hash结果就暂存在这里。 41 | 2. `0x40` - `0x5f` (32字节):当前分配的内存大小,又称空闲内存指针(Free Memory Pointer),指向当前空闲的内存位置。Solidity 总会把新对象保存在空闲内存指针的位置,并更新它的值到下一个空闲位置。 42 | 3. `0x60` - `0x7f` (32字节): 32字节的`0`值插槽,用于需要零值的地方,比如动态长度数据的初始长度值。 43 | 44 | ### 值变量 45 | 46 | 每个值变量会占用一个内存插槽。 47 | 48 | ```solidity 49 | function testUint() public pure returns (uint){ 50 | uint a = 3; 51 | return a; 52 | } 53 | ``` 54 | 55 | 可以看到,上面`testUint()`函数中的`a`变量的值被存在内存槽`0x80`中: 56 | 57 | ![](./img/4-2.png) 58 | 59 | ### 字符串/字节数组 60 | 61 | 对于内存布局,字符串/字节数组不论长短规则都是一样的。字符串/字节数组长度保存在单独的一个内存槽中,接着是内容,一个内存槽不够的话会顺序保存到后面的内存槽中。 62 | 63 | ```solidity 64 | function testShortString() public pure returns (string memory){ 65 | string memory x = "WTF"; 66 | return x; 67 | } 68 | ``` 69 | 70 | 上面的字符串变量`x`的长度为`3`,保存在内存槽`0x80`;内容为`WTF`,保存在内存槽`0xa0` 71 | 72 | ![](./img/4-5.png) 73 | 74 | ```solidity 75 | function testLongBytes() public pure returns (bytes memory){ 76 | bytes memory x = hex"365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3"; 77 | return x; 78 | } 79 | ``` 80 | 81 | 上面的字节数组变量`x`的长度为`44`(`0x2c`),保存在内存槽`0x80`;内容保存在内存槽`0xa0`-`0xc0`中。 82 | 83 | ![](./img/4-6.png) 84 | 85 | 86 | ### 静态数组 87 | 88 | 静态数组的每一个元素会占用一个单独的内存槽。 89 | 90 | ```solidity 91 | function testStaticArray() public pure returns (uint8[3] memory){ 92 | uint8[3] memory b = [1,2,3]; 93 | return b; 94 | } 95 | ``` 96 | 97 | 可以看到,上面`testStaticArray()`函数中的`b`变量的元素被顺序的存在内存槽`0xe0`-`0x120`中,虽然每个元素为`uint8`类型,但仍然占用一个单独的内存槽: 98 | 99 | ![](./img/4-3.png) 100 | 101 | ### 动态数组 102 | 103 | 动态数组的长度以及每一个元素会占用一个单独的内存槽。 104 | 105 | ```solidity 106 | function testDynamicArray() public pure returns (uint[] memory){ 107 | uint[] memory x = new uint[](3); 108 | x[0] = 1; 109 | x[2] = 4; 110 | return x; 111 | } 112 | ``` 113 | 114 | 可以看到,上面`testDynamicArray()`函数中的`x`变量的长度和元素被顺序的存在内存槽`0x80`-`0xe0`中: 115 | 116 | ![](./img/4-4.png) 117 | 118 | ### 多维数组 119 | 120 | 当数组里存的是可变长度的数据或其他数组时,对应元素的内存槽存的是变长数据在内存中的指针也就是起始地址。 121 | 122 | ```solidity 123 | function testMultiDimensionalArray(string memory info, uint16 length) public pure returns (string[] memory){ 124 | string[] memory x = new string[](length); 125 | x[0] = info; 126 | x[1] = "HELLO"; 127 | x[length - 1] = "WTF"; 128 | return x; 129 | } 130 | ``` 131 | 132 | 可以看到, `testMultiDimensionalArray("123456789", 5)`函数中,参数`info`的长度和数据存在内存槽`0x80` - `0xa0`中,参数`length`因为被编译器优化没有被存在接下来的内存槽中,内存槽`0xc0`存的是`x`变量的长度,内存槽`0xe0` - `0x160`存的是对应元素的string数据在内存中的起始地址, `x[0]`中存的是`0x80`,`x[1]`中存的是`0x180`也就是`string("HELLO")`在内存中的起始位置,`x[2]`、`x[3]`都存的是`0x60`,内存槽`0x60`中是恒定的32字节0,在这里就表示长度为0的string,`x[4]`中存的是`0x1c0`也就是`string("WTF")`在内存中的起始位置,`0x200`后的数据都是`abi.encode`过的返回数据。 133 | 134 | ![](./img/4-7.png) 135 | ![](./img/4-8.png) 136 | ![](./img/4-9.png) 137 | 138 | ## 总结 139 | 140 | 这一讲,我们介绍了Solidity合约的内存布局。内存布局与存储布局大致类似,但是由于内存操作消耗的gas很低,我们不需要像存储布局那样将多个变量保存在同一个内存槽中。 141 | -------------------------------------------------------------------------------- /tutorials/01_ValueStorage/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 01. 基础存储布局 3 | tags: 4 | - solidity 5 | - storage 6 | - storage layout 7 | - state variables 8 | --- 9 | 10 | # WTF Solidity内部标准: 01. 基础存储布局 11 | 12 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 13 | 14 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 15 | 16 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 17 | 18 | 所有代码和教程开源在github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 19 | 20 | ----- 21 | 22 | 在WTF Solidity内部标准系列教程中,我们将详细介绍Solidity合约中变量的存储布局,内存存储,以及`calldata`和`returndata`遵守的ABI编码规则,帮助大家理解Solidity的内部规则。这一讲,我们将介绍Solidity的值类型变量是如何在合约中存储的,你需要事先安装[foundry](https://book.getfoundry.sh/getting-started/installation)。 23 | 24 | ## EVM的存储 25 | 26 | EVM的账户存储(Account Storage)是一种映射(mapping,键值对存储),每个键和值都是256 bit的数据,它支持256 bit的读和写。这种存储在每个合约账户上都存在,并且是持久的:它的数据会保持在区块链上,直到被明确地修改。 27 | 28 | 对存储的读取(`SLOAD`)和写入(`SSTORE`)都需要gas,并且比内存操作更昂贵。这样设计可以防止滥用存储资源,因为所有的存储数据都需要在每个以太坊节点上保存。 29 | 30 | ![](./img/1-1.png) 31 | 32 | ## 值类型的存储布局 33 | 34 | 在Solidity中,值类型(比如`uint8`和静态数组)和引用类型(比如映射和动态数组)的存储布局不同,这一讲我们只介绍前者。 35 | 36 | ### 规则1. 默认从存储槽0开始被逐项存储 37 | 38 | 合约中的第一个状态变量默认存储在槽`0`中,之后的状态变量(如果不能放在同一个存储槽)存储在槽`1`中,以此类推。在下面的`ValueStorage1`合约中,我们声明了2个`uint256`类型的状态变量: 39 | 40 | ```solidity 41 | contract ValueStorage1 { 42 | uint256 public a = 5; 43 | uint256 public b = 2; 44 | } 45 | ``` 46 | 47 | 你可以使用下面的命令打印合约的存储布局,可以看到变量`a`被存储在Slot 0,变量`b`被存储在Slot 1。 48 | 49 | ```shell 50 | forge inspect src/01_ValueStorage.sol:ValueStorage1 storage-layout --pretty 51 | ``` 52 | 53 | | Name | Type | Slot | Offset | Bytes | Contract | 54 | |------|---------|------|--------|-------|---------------------------------------| 55 | | a | uint256 | 0 | 0 | 32 | src/01_ValueStorage.sol:ValueStorage1 | 56 | | b | uint256 | 1 | 0 | 32 | src/01_ValueStorage.sol:ValueStorage1 | 57 | 58 | ### 规则2. 多个值可共享同一个存储槽 59 | 60 | 由于EVM的存储很贵,在合约中,状态变量以一种紧凑的方式存储:值类型只使用存储它们所需的字节数,如果多个连续状态变量的大小总和不足32字节,它们会被放在同一个存储槽中;如果一个值类型不适合一个存储槽的剩余部分,它将被存储在下一个存储槽。 61 | 62 | 在下面的`ValueStorage2`合约中,我们声明了`4`个状态变量。 63 | 64 | ```solidity 65 | contract ValueStorage2 { 66 | uint128 public a = 5; 67 | uint64 public b = 2; 68 | uint32 public c = 3; 69 | uint64 public d = 1; 70 | } 71 | ``` 72 | 73 | 你可以使用下面的命令打印合约的存储布局: 74 | 75 | ```shell 76 | forge inspect src/01_ValueStorage.sol:ValueStorage2 storage-layout --pretty 77 | ``` 78 | 79 | 我们可以看到,变量`a`,`b`和`c`都被存储在`Slot 0`中,这是因为它们的类型分别为`uint128`,`uint64`和`uint32`,而`128+64+32=224`小于`256`。变量`d`被存储在`Slot 1`,这是因为它是`uint64`类型,加上前面的变量就超过`256`位了。 80 | 81 | | Name | Type | Slot | Offset | Bytes | Contract | 82 | |------|---------|------|--------|-------|---------------------------------------| 83 | | a | uint128 | 0 | 0 | 16 | src/01_ValueStorage.sol:ValueStorage2 | 84 | | b | uint64 | 0 | 16 | 8 | src/01_ValueStorage.sol:ValueStorage2 | 85 | | c | uint32 | 0 | 24 | 4 | src/01_ValueStorage.sol:ValueStorage2 | 86 | | d | uint64 | 1 | 0 | 8 | src/01_ValueStorage.sol:ValueStorage2 | 87 | 88 | > **思考题**:如果变量`d`是`uint32`的话,它会被存储在哪里呢? 89 | 90 | ### 规则3. 存储插槽的第一项会以低位对齐(即右对齐)的方式储存 91 | 92 | `ValueStorage1`合约中的变量`a`和`b`(均为`uint256`类型)会被存储为 93 | 94 | ``` 95 | a: 0x0000000000000000000000000000000000000000000000000000000000000005 96 | b: 0x0000000000000000000000000000000000000000000000000000000000000002 97 | ``` 98 | 99 | `ValueStorage2`合约中的变量`a`,`b`和`c`(分别为`uint128`,`uint64`和`uint32`)会被存储为: 100 | 101 | ``` 102 | a b c: 0x0000000000000003000000000000000200000000000000000000000000000005 103 | ``` 104 | 105 | 这是因为变量`a`是第一项,会从最右边开始存,值为`5`,占`128`位(`16`字节)。然后`b`从右数`16`字节开始存,值为`2`,占`64`位(`8`字节)。最后`c`从右数`24`字节开始存,值为`3`,占`32`位(`4`字节)。 106 | 107 | ### 规则4. 结构/数组总是从一个新的存储槽开始,紧跟着它们的变量总是开辟一个新的存储槽 108 | 109 | 在下面的`ValueStorage3`合约中,我们声明了一个`S`结构体,它包含两个成员:`uint64`类型的`b`和`uint32`类型的`c`。按照规则3的话,变量`a`和结构体`s`会共享Slot 0;而如果按照规则4,变量`a`会被单独保存在Slot 0,结构体`s`会被保存在Slot 1,而变量`d`会被保存在Slot 2。 110 | 111 | ```solidity 112 | contract ValueStorage3 { 113 | struct S { uint64 b; uint32 c; } 114 | 115 | uint128 public a = 5; 116 | S public s = S(2, 3); 117 | uint64 public d = 1; 118 | } 119 | ``` 120 | 121 | 你可以使用下面的命令打印合约的存储布局: 122 | 123 | ```shell 124 | forge inspect src/01_ValueStorage.sol:ValueStorage3 storage-layout --pretty 125 | ``` 126 | 127 | 可以看到,按照规则4,三个变量被保存在不同的存储槽中。 128 | 129 | | Name | Type | Slot | Offset | Bytes | Contract | 130 | |------|------------------------|------|--------|-------|---------------------------------------| 131 | | a | uint128 | 0 | 0 | 16 | src/01_ValueStorage.sol:ValueStorage3 | 132 | | s | struct ValueStorage3.S | 1 | 0 | 32 | src/01_ValueStorage.sol:ValueStorage3 | 133 | | d | uint64 | 2 | 0 | 8 | src/01_ValueStorage.sol:ValueStorage3 | 134 | 135 | ## 总结 136 | 137 | 这一讲,我们介绍了值变量,静态数组和结构体数据在合约中的存储结构。 138 | -------------------------------------------------------------------------------- /tutorials/02_MappingStorage/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 02. 映射和动态数组的存储布局 3 | tags: 4 | - solidity 5 | - storage 6 | - storage layout 7 | - mapping 8 | - dynamic array 9 | - state variables 10 | --- 11 | 12 | # WTF Solidity 内部标准: 02. 映射和动态数组的存储布局 13 | 14 | 《WTF Solidity 内部标准》教程将介绍 Solidity 智能合约中的存储布局,内存布局,以及 ABI 编码规则,帮助大家理解 Solidity 的内部规则。 15 | 16 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 17 | 18 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 19 | 20 | 所有代码和教程开源在 github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 21 | 22 | --- 23 | 24 | 这一讲,我们将介绍映射和动态数组类型的状态变量是如何在合约中存储的。 25 | 26 | ## 映射和动态数组 27 | 28 | 由于映射和动态数组的大小是可以改变的,不能事先预知,因此,它们有着特别的存储布局。 29 | 30 | ## 映射 31 | 32 | 在基础存储布局规则中,映射只占用`32`个字节。假设一个映射被存在了槽`p`,这个槽仅用作占位,不存储任何内容,保持空(`0`)的状态。而映射中键`k`对应的值会被存储在由 Keccak 哈希决定的槽中,计算方法为`keccak256(h(k) . p)`, 其中`.`是连接符,`h`是一个函数,根据键的类型应用于键。 33 | 34 | - 对于值类型,函数`h`将与在内存中存储值的相同方式来将值填充为`32`字节。比如`uint8`类型的`1`会被填充为`0000000000000000000000000000000000000000000000000000000000000001`。 35 | 36 | - 对于字符串和字节数组,`h(k)`只是未填充的数据。 37 | 38 | 我们以`MappingStorage`合约为例,其中声明了两个状态变量,一个为`mapping(uint => uint)`类型,另一个为`uint256`类型。 39 | 40 | ```solidity 41 | contract MappingStorage { 42 | mapping(uint => uint) public a; 43 | uint256 public b = 5; 44 | 45 | constructor(){ 46 | a[0] = 1; 47 | a[1] = 2; 48 | } 49 | 50 | function getEncode(uint k, uint p) public pure returns(bytes memory){ 51 | return abi.encode(k, p); 52 | } 53 | 54 | function getHash(bytes memory bb) public pure returns(bytes32){ 55 | return keccak256(bb); 56 | } 57 | 58 | // hashmap的slot计算公式:slot = keccak256(h(k) . p),其中 . 意味着把前后2个值拼接到一起,类似于abi.encode(h(k), p) 59 | // get slot of a[0] 时 key = 0, p = 0, result = 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5 60 | // get slot of a[1] 时 key = 1, p = 0, result = 0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7d 61 | function getSlot(uint key, uint p) public pure returns(bytes32){ 62 | return keccak256(abi.encode(key, p)); 63 | } 64 | } 65 | ``` 66 | 67 | 你可以使用下面的命令打印合约的存储布局: 68 | 69 | ```shell 70 | forge inspect src/02_MappingStorage.sol:MappingStorage storage-layout --pretty 71 | ``` 72 | 73 | 可以看到,映射`a`被存在了 Slot 0,而变量`b`被存在了 Slot 1。 74 | 75 | | Name | Type | Slot | Offset | Bytes | Contract | 76 | | ---- | --------------------------- | ---- | ------ | ----- | ---------------------------------------- | 77 | | a | mapping(uint256 => uint256) | 0 | 0 | 32 | src/02_MappingStorage.sol:MappingStorage | 78 | | b | uint256 | 1 | 0 | 32 | src/02_MappingStorage.sol:MappingStorage | 79 | 80 | 其实这里`foundry`并没有将映射的存储布局显示完全,没有给出`a[0]`和`a[1]`的位置。其实,它们分别在: 81 | 82 | ``` 83 | a[0]: 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5 84 | a[1]: 0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7d 85 | ``` 86 | 87 | 它们分别由:`keccak256(abi.encode(0, 0))`和`keccak256(abi.encode(1, 0))`计算而得,你可以用`getEncode()`和`getHash()`函数,检查输出的值。 88 | 89 | ## 动态数组 90 | 91 | 与映射类似,动态数组的成员也会被保存在由 Keccak 哈希决定的槽中。在基础存储布局规则中,动态数组只占用`32`个字节。假设一个动态数组被存在了槽`p`,这个槽仅用于保存动态数组当前的长度(字节数组和字符串例外)。而数组的元素从`keccak256(p)`开始保存,排列方式与静态数组的元素相同: 一个元素接着一个元素,如果元素的长度不超过 16 字节,就有可能共享存储槽。 92 | 93 | 我们以`ArrayStorage`合约为例,其中声明了两个状态变量,一个为`uint128`类型,另一个为`uint128[]`类型。 94 | 95 | ```solidity 96 | contract ArrayStorage { 97 | uint128 public a = 9; 98 | uint128[] public b; 99 | 100 | constructor(){ 101 | b.push(10); 102 | b.push(11); 103 | b.push(12); 104 | } 105 | 106 | function getEncode(uint k, uint p) public pure returns(bytes memory){ 107 | return abi.encode(k, p); 108 | } 109 | 110 | function getHash(bytes memory bb) public pure returns(bytes32){ 111 | return keccak256(bb); 112 | } 113 | 114 | // 数组的slot计算公式,slot = keccak256(p),其中p为数组状态变量在基本布局中的位置,此时b的位置p为1(状态变量a位置为0) 115 | // get slot of b[0] 时,variableStatePosition = 1, result = 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 116 | // 对于1维数组只需要计算出第一个元素的slot即可,其他的元素依次排列,直到当前slot填满,再开启下一个slot 117 | function getSlot(uint128 variableStatePosition) public pure returns(bytes32){ 118 | return keccak256(abi.encode(variableStatePosition)); 119 | } 120 | } 121 | ``` 122 | 123 | 这个合约的存储布局如下所示: 124 | 125 | | Name | Slot | 126 | | -------- | ------------------------------------------------------------------ | 127 | | a | 0 | 128 | | b.length | 1 | 129 | | b[0] | 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 | 130 | | b[1] | 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 | 131 | | b[2] | 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf7 | 132 | 133 | 其中`b[0]`和`b[1]`共用一个存储槽`keccak(1)`,`b[2]`保存在`keccak(1)+1`。 134 | 135 | 你可以使用`getHash()`(需要把参数填充到`32`字节),或者下面的命令行验证: 136 | 137 | ```shell 138 | cast keccak 0x0000000000000000000000000000000000000000000000000000000000000001 139 | # output 140 | # 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 141 | ``` 142 | 143 | ## 总结 144 | 145 | 这一讲,我们介绍了映射和动态数组的存储布局。与值类型不同,映射和动态数组的长度不能事先预知,因此使用的存储槽由哈希决定。 146 | -------------------------------------------------------------------------------- /tutorials/06_AbiDynamic/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 06. 动态类型的ABI编码 3 | tags: 4 | - solidity 5 | - abi 6 | --- 7 | 8 | # WTF Solidity内部标准: 06. 动态类型的ABI编码 9 | 10 | 《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。 11 | 12 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 13 | 14 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 15 | 16 | 所有代码和教程开源在github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 17 | 18 | ----- 19 | 20 | 上一讲,我们介绍了静态类型的ABI编码规则。而在这一讲,我们将讲解更复杂的动态类型的ABI编码规则。 21 | 22 | > 注意,`abi.encode(x)`编码的实际上是`(x)`,即仅包含`x`一个元素的元组。所以我们这一讲给出的是复杂类型在元组中的编码规则,不在元组中的规则请见下一讲。 23 | 24 | ![](./img/6-1.png) 25 | 26 | ## 动态类型的ABI编码规则 27 | 28 | 我们可以把Solidity中的动态类型包括: 29 | 30 | 1. `bytes`和`string`。 31 | 2. 动态数组。 32 | 3. 动态类型`T`的定长数组`T[k]`,其中`k > 0`。 33 | 4. 由任意动态类型构成的元组。 34 | 35 | 静态类型是直接编码的,而动态类型是在当前数据槽之后的一个单独分配的位置进行编码,当前块记录只分配位置的偏移量。 36 | 37 | ### 动态数组 38 | 39 | 对于动态数组,编码时当前数据槽记录偏移量,然后记录动态数组的长度和每一个元素,它们分别占据一个单独的数据槽(`32`字节)。在下面的`testAbiArray()`中,我们定义了一个`uint[] memory a = new uint[](3)`,并返回它的ABI编码。 40 | 41 | ```solidity 42 | function testAbiArray() public pure returns (bytes memory){ 43 | uint[] memory a = new uint[](3); 44 | a[0] = 1; 45 | a[1] = 2; 46 | a[2] = 3; 47 | return abi.encode(a); 48 | } 49 | ``` 50 | 51 | 首先,`0x00`槽会记录偏移量,因为我们只编码一个数据,`uint[]`的长度和数值会在`0x20`开始记录,偏移量就是`0x20`。接下来,`0x20`槽会记录数组的长度,这里为`3`。`0x40`-`0x80`会分别记录数组的元素`[1, 2, 3]`。因此,编码后的结果为: 52 | 53 | ``` 54 | 0x 55 | 0000000000000000000000000000000000000000000000000000000000000020 56 | 0000000000000000000000000000000000000000000000000000000000000003 57 | 0000000000000000000000000000000000000000000000000000000000000001 58 | 0000000000000000000000000000000000000000000000000000000000000002 59 | 0000000000000000000000000000000000000000000000000000000000000003 60 | ``` 61 | 62 | ### `bytes`和`string` 63 | 64 | `bytes`和`string`的编码方式相同,会在当前数据槽记录分配位置的偏移量,然后在分配的位置记录`bytes/string`的字节长度,接下来是字节内容,编码时会在右侧补若干`0`使其长度成为`32`字节的倍数。在下面的`testAbiString()`中,我们定义了`string memory b = "WTF"`,并返回它的ABI编码。 65 | 66 | ```solidity 67 | function testAbiString() public pure returns (bytes memory){ 68 | string memory b = "WTF"; 69 | return abi.encode(b); 70 | } 71 | ``` 72 | 73 | 首先,`0x00`槽会记录偏移量`0x20`。接下来`0x20`槽会记录字符串长度`3`。最后,`0x40`槽会记录`"WTF"`的UTF8编码,并在右侧补若干`0`使其长度成为`32`字节,也就是`5754460000000000000000000000000000000000000000000000000000000000`。因此,编码后的结果为: 74 | 75 | ```solidity 76 | 0x 77 | 0000000000000000000000000000000000000000000000000000000000000020 78 | 0000000000000000000000000000000000000000000000000000000000000003 79 | 5754460000000000000000000000000000000000000000000000000000000000 80 | ``` 81 | 82 | ### 动态类型的定长数组 83 | 84 | 动态类型的定长数组,比如`string[2]`,会在当前数据槽记录分配位置的偏移量,然后再在之后的槽中记录每个元素的偏移量,比如`string[0]`和`string[1]`的偏移量,最后记录元素的值。在下面的`testAbiStringStaticArray()`中,我们定义了`string[2] memory strings = ["WTF", "Academy"]`,并返回它的ABI编码。 85 | 86 | ```solidity 87 | function testAbiStringStaticArray() public pure returns (bytes memory){ 88 | string[2] memory strings = ["WTF", "Academy"]; 89 | return abi.encode(strings); 90 | } 91 | ``` 92 | 93 | 首先,`0x00`槽会记录数组的偏移量`0x20`。接下来`0x20`和`0x40`槽分别记录`string[0]`和`string[1]`的偏移量,`0x40`和`0x80`。注意,这里的偏移量是相对于`0x20`槽的,而不是`0x00`的。然后`string[0]`,也就是`"WTF"`,会保存在`0x60-0x80`槽中(`0x20`偏移`0x40`);`string[1]`,也就是`"Academy"`,会保存在`0xa0-0xc0`槽中(`0x20`偏移`0x80`)。因此,编码后的结果为: 94 | 95 | ``` 96 | 0x 97 | 0000000000000000000000000000000000000000000000000000000000000020 98 | 0000000000000000000000000000000000000000000000000000000000000040 99 | 0000000000000000000000000000000000000000000000000000000000000080 100 | 0000000000000000000000000000000000000000000000000000000000000003 101 | 5754460000000000000000000000000000000000000000000000000000000000 102 | 0000000000000000000000000000000000000000000000000000000000000007 103 | 41636164656d7900000000000000000000000000000000000000000000000000 104 | ``` 105 | 106 | ### 包含动态类型的元组/结构体 107 | 108 | 元组和结构体的编码方式相同,对于包含动态类型的元组/结构体,会在当前数据槽记录分配位置的偏移量,然后再分别编码成员:如果成员是静态类型,就直接在当前槽中编码;如果是动态类型,则先记录偏移量,再在后面分配的位置进行编码。在下面的合约中,我们定义了一个`DynamicStruct`结构体,它包含`3`个成员:`uint a`,`uint[] b`,和`string c`,后两者均为动态类型。函数`testAbiDynamicStruct`会返回`DynamicStruct`结构体的ABI编码。 109 | 110 | ```solidity 111 | struct DynamicStruct { uint a; uint[] b; string c; } 112 | 113 | function testAbiDynamicStruct() public pure returns (bytes memory){ 114 | uint a = 99; 115 | uint[] memory b = new uint[](3); 116 | b[0] = 1; 117 | b[1] = 2; 118 | b[2] = 3; 119 | string memory c = "WTF"; 120 | 121 | DynamicStruct memory ds = DynamicStruct(a, b, c); 122 | return abi.encode(ds); 123 | } 124 | ``` 125 | 126 | 首先,`0x00`槽会记录整个结构体的偏移量`0x20`。接下来开始记录数组成员,`0x20`槽记录`uint a`的值`99`(`0x63`),`0x40`槽记录`uint[] b`的偏移量`0x60`(相对槽`0x20`),`0x60`槽记录`string c`的偏移量`0xe0`(相对槽`0x20`)。最后,`0x80`-`0xe0`记录`uint[] b`的长度`3`和值`[1,2,3]`,`0x100-0x120`记录`string c`的字节长度`3`和值`575446`。因此,编码后的结果为: 127 | 128 | ``` 129 | 0x 130 | 0000000000000000000000000000000000000000000000000000000000000020 131 | 0000000000000000000000000000000000000000000000000000000000000063 132 | 0000000000000000000000000000000000000000000000000000000000000060 133 | 00000000000000000000000000000000000000000000000000000000000000e0 134 | 0000000000000000000000000000000000000000000000000000000000000003 135 | 0000000000000000000000000000000000000000000000000000000000000001 136 | 0000000000000000000000000000000000000000000000000000000000000002 137 | 0000000000000000000000000000000000000000000000000000000000000003 138 | 0000000000000000000000000000000000000000000000000000000000000003 139 | 5754460000000000000000000000000000000000000000000000000000000000 140 | ``` 141 | 142 | ## 其他ABI编码 143 | 144 | 这一章节总结一些特殊类型的ABI编码,方便大家理解。 145 | 146 | ### 动态类型的不定长数组 147 | 148 | 上面我们学习了动态类型的定长数组的ABI编码规则,现在我们看看动态类型的不定长数组是如何编码的。下面`testAbiStringArray()`函数中我们定义了一个`string[] strings`变量,并返回了它的ABI编码 149 | 150 | ```solidity 151 | function testAbiStringArray() public pure returns (bytes memory){ 152 | string[] memory strings = new string[](2); 153 | strings[0] = "WTF"; 154 | strings[1] = "Academy"; 155 | return abi.encode(strings); 156 | } 157 | ``` 158 | 159 | 首先,`0x00`槽记录不定长数组的偏移量`0x20`。接下来`0x20`槽中会记录不定长数组的长度`2`。之后槽`0x40`和`0x60`分别记录了`strings[0]`和`strings[1]`的偏移量`0x40`和`0x80`(相对于槽`0x40`)。最后,槽`0x80`-`0xa0`记录了`strings[0]`的字节长度和内容,槽`0xc0`-`0xe0`记录了`strings[1]`的字节长度和内容。因此,编码后的结果为: 160 | 161 | ``` 162 | 0x 163 | 0000000000000000000000000000000000000000000000000000000000000020 164 | 0000000000000000000000000000000000000000000000000000000000000002 165 | 0000000000000000000000000000000000000000000000000000000000000040 166 | 0000000000000000000000000000000000000000000000000000000000000080 167 | 0000000000000000000000000000000000000000000000000000000000000003 168 | 5754460000000000000000000000000000000000000000000000000000000000 169 | 0000000000000000000000000000000000000000000000000000000000000007 170 | 41636164656d7900000000000000000000000000000000000000000000000000 171 | ``` 172 | 173 | ## 总结 174 | 175 | 这一讲,我们介绍了Solidity合约中动态类型的ABI编码规则。相比于静态类型,动态类型的ABI编码规则要复杂得多,大家需要借助例子取理解它。 -------------------------------------------------------------------------------- /tutorials/07_AbiFormula/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 07. ABI编码公式 3 | tags: 4 | - solidity 5 | - abi 6 | --- 7 | 8 | # WTF Solidity 内部标准: 07. ABI 编码公式 9 | 10 | 《WTF Solidity 内部标准》教程将介绍 Solidity 智能合约中的存储布局,内存布局,以及 ABI 编码规则,帮助大家理解 Solidity 的内部规则。 11 | 12 | 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) 13 | 14 | 社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) 15 | 16 | 所有代码和教程开源在 github: [github.com/AmazingAng/WTF-Solidity-Internals](https://github.com/AmazingAng/WTF-Solidity-Internals) 17 | 18 | --- 19 | 20 | 这一讲,我们介绍基于[Solidity 文档](https://docs.soliditylang.org/en/v0.8.21/abi-spec.html)总结的 ABI 编码公式。 21 | 22 | ## 符号说明 23 | 24 | 首先,我们定义公式会使用到的符号的意义: 25 | 26 | 1. `T`: 任意变量类型。 27 | 2. `(T1, T2,...,Tn)`: 由`T1`...`Tn`等类型的变量组成的元组。 28 | 3. `len(a)`: `a`的字节长度,用于计算数据偏移量。 29 | 4. `e(X)`: 变量`X`的 ABI 编码。 30 | 5. `head(X)`: `X`变量的头部编码。 31 | 6. `tail(X)`: `X`变量的尾部编码。 32 | 7. `pad_right(X)`: 在`X`的值的右侧补若干个`0`,使其长度成为`32`字节。 33 | 34 | ## ABI 编码公式 35 | 36 | 我们将 Solidity 合约的 ABI 编码公式分为两部分,第一部分为递归编码公式,第二部分为类型编码公式。逻辑就是将复杂类型使用递归公式转换成简单类型进行编码。 37 | 38 | ### 递归编码公式 39 | 40 | 递归编码公式会将复杂类型的编码以递归的方式转换为简单类型的编码。 41 | 42 | 1. 元组(结构体也会转换成元组进行编码) 43 | 44 | 对于元组`X = (T1,...,Tk)`,其中`k>=0`并且`T1`...`Tn`为任意类型,有: 45 | 46 | ``` 47 | e(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k)) 48 | ``` 49 | 50 | 其中`X(n)`为元组`X`的第`n`个元素。也就是说,元组`X`的编码由两部分组成,第一部分是头部编码,第二部分是尾部编码,它们都按照元素在元组中的顺序排列。 51 | 52 | 那么`head(Xi)`和`tail(Xi)`如何定义? 53 | 54 | - 如果`X(i)`的类型`Ti`为静态类型,则直接在头部进行编码,没有尾部编码: 55 | - `head(X(i)) = e(X(i))` 56 | - `tail(X(i)) = ""`(为空) 57 | - 如果`X(i)`的类型`Ti`为动态类型,那么会在尾部进行编码,头部编码为尾部编码`tail(X(i))`的偏移量: 58 | - `head(X(i)) = e(len(head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1))))`(这是从`head(X1)`到`tail(X(i-1))`的字节长度,也是`tail(X(i))`的偏移量) 59 | - `tail(X(i)) = e(X(i))` 60 | 61 | 2. 定长数组 62 | 63 | 对于长度为`k`的定长数组`X = T[k]`,其中`k>=0`并且`T`为任意类型,有: 64 | 65 | ``` 66 | e(X) = e((X[0], ..., X[k-1])) 67 | ``` 68 | 69 | 其中`X[n]`为数组`X`的第`n`个元素。也就是说,将它当作由相同类型的`k`个元素组成的元组那样被编码的。 70 | 71 | 3. 不定长数组 72 | 73 | 对于长度为`k`的不定长数组`X = T[]`,其中其中`k>=0`并且`T`为任意类型,有: 74 | 75 | ``` 76 | e(X) = e(k) e((X[0], ..., X[k-1])) 77 | ``` 78 | 79 | 其中长度`k`当作`uint256`类型编码。也就是说,它的编码有两部分,第一部分为长度`k`的编码,第二部分为等效的定长数组的编码。 80 | 81 | ## 类型编码公式 82 | 83 | 类型编码公式以穷举的方式给出简单类型的 ABI 编码规则。 84 | 85 | 1. `uint`:`M`位的无符号整数,`M`的取值为`0`到`256`之间的可以整除 8 的整数,比如`uint8`,`uint32`,`uint256`(`uint`是`uint256`的同义词)。编码时,会在它们左侧补充若干`0`以使其长度成为`32`字节。 86 | 87 | 2. `address`:地址类型,与`uint160`的编码方式相同。`address payable`和`contract`类型的变量也使用相同的编码方式。 88 | 89 | 3. `bool`: `1`表示`true`,`0`表示`false`,编码方式与`uint8`的情况相同。 90 | 91 | 4. `bytes`:长度为`M`字节的定长字节数组,`0 < M <= 32`,编码时会在右侧补若干`0`使其长度成为`32`字节。 92 | 93 | 5. `bytes`: 若`X`是长度为`k`(`k`的类型为`uint256`)的不定长字节数组,则`enc(X) = enc(k) pad_right(X)`,也就是先编码长度`k`,再编码内容。编码内容时会在右侧补若干`0`使其长度成为`32`字节的倍数。 94 | 95 | 6. `string`: 会先用`UTF-8`编码为`bytes`,然后使用`bytes`的规则进行 ABI 编码。 96 | 97 | 以上是常用类型的 ABI 编码规则,不常用类型(如`int`,`fixed`,`ufixed`)的规则见[文档](https://docs.soliditylang.org/en/v0.8.21/abi-spec.html)。 98 | 99 | ## 示例 100 | 101 | ### 示例 1: (uint x) 102 | 103 | ```solidity 104 | function testAbiUintTuple() public pure returns (bytes memory){ 105 | uint x = 1; 106 | return abi.encode((x)); 107 | } 108 | ``` 109 | 110 | `(x)`是元组,根据 ABI 编码公式,`e((x)) = head(x) tail(x)`。 111 | 112 | `x`为`uint`类型,所以`head(x) = e(x)`,`tail(x) = ""`,所以`(x)`的 ABI 编码为 113 | 114 | ``` 115 | 0x0000000000000000000000000000000000000000000000000000000000000001 116 | ``` 117 | 118 | ### 示例 2: (uint[] x) 119 | 120 | ```solidity 121 | function testAbiArray() public pure returns (bytes memory){ 122 | uint[] memory x = new uint[](3); 123 | x[0] = 1; 124 | x[1] = 2; 125 | x[2] = 3; 126 | return abi.encode(x); 127 | } 128 | ``` 129 | 130 | `(x)`是元组,根据 ABI 编码公式,`e((x)) = head(x) tail(x)`。`x`为`uint[]`类型(不定长数组),是动态类型,因此: 131 | 132 | - `head(x) = len(head(x))`也就是`0x20` 133 | 134 | - `tail(x) = e(x) = e(k) e(x[0]) e(x[1]) e(x[2])`,其中`k = 3`是数组的长度。 135 | 136 | 所以`(x)`的 ABI 编码为: 137 | 138 | ``` 139 | 0x 140 | 0000000000000000000000000000000000000000000000000000000000000020 141 | 0000000000000000000000000000000000000000000000000000000000000003 142 | 0000000000000000000000000000000000000000000000000000000000000001 143 | 0000000000000000000000000000000000000000000000000000000000000002 144 | 0000000000000000000000000000000000000000000000000000000000000003 145 | ``` 146 | 147 | ### 示例 3: ((uint x, uint[] y, string z)) 148 | 149 | ```solidity 150 | function testAbiDynamicArray() public pure returns (bytes memory){ 151 | uint x = 99; 152 | uint[] memory y = new uint[](3); 153 | y[0] = 1; 154 | y[1] = 2; 155 | y[2] = 3; 156 | string memory z = "WTF"; 157 | 158 | bytes memory encodedX = abi.encode(x); 159 | bytes memory encodedY = abi.encode(y); 160 | bytes memory encodedZ = abi.encode(z); 161 | 162 | return abi.encodePacked(encodedX, encodedY, encodedZ); 163 | } 164 | ``` 165 | 166 | 由于`y`和`z`都是动态类型,因此`(x, y, z)`元组为动态类型,`((x, y, z))`元组也是动态类型,根据 ABI 编码公式,`e(((x, y, z))) = head(((x, y, z))) tail(((x, y, z)))`。 167 | 168 | 其中`head(((x, y, z))) = len(head(((x, y, z))))` 为`0x20`,因为`((x, y, z))`只有一个元素`(x, y, z)`,虽然这个元素也是一个元组。`tail(((x, y, z))) = e((x, y, z))`(这里我们脱了一层括号,开始编码元组的元素了)。 169 | 170 | 到目前为止的编码为: 171 | 172 | ``` 173 | 0x 174 | 0000000000000000000000000000000000000000000000000000000000000020 175 | e((x, y, z)) 176 | ``` 177 | 178 | 下面我们来计算`e((x, y, z))`,由于它是动态元组,因此`e((x, y, z)) = head(x) head(y) head(z) tail(x) tail(y) tail(z)`。到目前为止的编码为: 179 | 180 | ``` 181 | 0x 182 | 0000000000000000000000000000000000000000000000000000000000000020 183 | head(x) 184 | head(y) 185 | head(z) 186 | tail(x) 187 | tail(y) 188 | tail(z) 189 | ``` 190 | 191 | 下面我们依次计算`x`,`y`和`z`的编码。`x`为`uint`类型,属于静态类型,值为`99`(也就是`0x63`)。因此,`head(x) = e(x)`,`tail(x) = ""`。到目前为止的编码为: 192 | 193 | ``` 194 | 0x 195 | 0000000000000000000000000000000000000000000000000000000000000020 196 | 0000000000000000000000000000000000000000000000000000000000000063 197 | head(y) 198 | head(z) 199 | tail(y) 200 | tail(z) 201 | ``` 202 | 203 | 接下来,我们来看`y`的编码。`y`为`uint[]`类型,是动态类型,长度为`3`。因此有 204 | 205 | - `head(y) = e(len(head(x) head(y) head(z) tail(x))) = 0x60` 206 | 207 | - `tail(y) = e(y) = e(3) e(y[0]) e(y[1]) e(y[2])` 208 | 209 | 到目前为止的编码为: 210 | 211 | ``` 212 | 0x 213 | 0000000000000000000000000000000000000000000000000000000000000020 214 | 0000000000000000000000000000000000000000000000000000000000000063 215 | 0000000000000000000000000000000000000000000000000000000000000060 216 | head(z) 217 | 0000000000000000000000000000000000000000000000000000000000000003 218 | 0000000000000000000000000000000000000000000000000000000000000001 219 | 0000000000000000000000000000000000000000000000000000000000000002 220 | 0000000000000000000000000000000000000000000000000000000000000003 221 | tail(z) 222 | ``` 223 | 224 | 最后,我们来看`z`的编码。`z`为`string`类型,是动态类型;`WTF`的`UTF-8`编码为`575446`,长度为`3`字节。因此有: 225 | 226 | - `head(z) = e(len(head(x) head(y) head(z) tail(x) tail(y))) = 0xe0` 227 | 228 | - `tail(z) = e(z) = e(3) pad_right(575446)` 229 | 230 | 呼,最后,我们就得到了`((uint x, uint[] y, string z))`的 ABI 编码: 231 | 232 | ``` 233 | 0x 234 | 0000000000000000000000000000000000000000000000000000000000000020 235 | 0000000000000000000000000000000000000000000000000000000000000063 236 | 0000000000000000000000000000000000000000000000000000000000000060 237 | 00000000000000000000000000000000000000000000000000000000000000e0 238 | 0000000000000000000000000000000000000000000000000000000000000003 239 | 0000000000000000000000000000000000000000000000000000000000000001 240 | 0000000000000000000000000000000000000000000000000000000000000002 241 | 0000000000000000000000000000000000000000000000000000000000000003 242 | 0000000000000000000000000000000000000000000000000000000000000003 243 | 5754460000000000000000000000000000000000000000000000000000000000 244 | ``` 245 | 246 | 如果你能自己推导出`((uint x, uint[] y, string z))`的编码,说明你已经掌握了 ABI 编码的规律! 247 | 248 | ## 总结 249 | 250 | 这一讲,我们介绍了 Solidity 合约的 ABI 编码公式,有了它,再复杂的编码也不怕。它的核心思想是使用递归的方法把复杂类型的编码转换成简单类型的编码。 251 | --------------------------------------------------------------------------------