本文从EVM操作码的角度,研究合约创建的详细过程。
solidity version = 0.8.15,optimizer = true,optimizer_runs = 200,evm_version = "london"
在线反汇编 https://ethervm.io/decompile
evm执行模拟使用foundry环境https://book.getfoundry.sh/reference/forge/forge-debug
合约代码如下
pragma solidity 0.8.15;
contract Test {
uint256 public val;
uint256 immutable c;
constructor(uint256 _val, uint256 _c) {
val = _val;
c = _c;
}
function setNumber(uint256 newNumber) public {
val = newNumber + c;
}
}
我们在在foundry环境中,在script文件里,使用new进行部署。
new的部署方式和外部账户通过交易部署合约其实是很类似的,都是message call的过程。
contract TestScript is Script {
function setUp() public view {}
function run() public {
vm.startBroadcast(); // record call information so deploy to chain
Test t = new Test(0x1234, 0x5678);
vm.stopBroadcast();
}
}
在TestScript执行new Test的时候,
我们可以观察一下memory的情况,这里存储的是calldata内容。
0x080~0x1fe: 这一段是deploy code(60a0开始 一直到 0033为之)
接在后面的还有两个uint256的数字,0x1234和0x5678,这两个是我们即将部署的合约的构造函数实参。
继续往下走,我们就会进入真正的deploy阶段。
把deploy code进行反汇编。
contract Contract {
function main() {
memory[0x40:0x60] = 0xa0;
var var0 = msg.value;
if (var0) { revert(memory[0x00:0x00]); }
var temp0 = memory[0x40:0x60];
var temp1 = code.length - 0x017e;
memory[temp0:temp0 + temp1] = code[0x017e:0x017e + temp1];
var var1 = temp0 + temp1;
memory[0x40:0x60] = var1;
var0 = 0x002f;
var var2 = temp0;
var0, var1 = func_003D(var1, var2);
storage[0x00] = var0;
memory[0x80:0xa0] = var1;
var temp2 = memory[0x80:0xa0];
memory[0x00:0x0103] = code[0x7b:0x017e];
memory[0x66:0x86] = temp2;
return memory[0x00:0x0103];
}
function func_003D(var arg0, var arg1) returns (var r0, var arg0) {
var var0 = 0x00;
var var1 = var0;
if (arg0 - arg1 i< 0x40) { revert(memory[0x00:0x00]); }
var temp0 = arg1;
r0 = memory[temp0:temp0 + 0x20];
arg0 = memory[temp0 + 0x20:temp0 + 0x20 + 0x20];
return r0, arg0;
}
}
代码的第1个关键点在storage[0x00] = var0;
这里实际上对应构造函数中的val = _val;
代码的第2个关键点是memory[0x00:0x0103] = code[0x7b:0x017e];
这里是把deploy code中的一部分代码拷贝到memory中。
deploy code其实有两部分,第一部分是用于deploy的代码部分,这些对应上面的反汇编代码。
60a060405234801561001057600080fd5b5060405161017e38038061017e8339 8101604081905261002f9161003d565b600091909155608052610061565b6000 806040838503121561005057600080fd5b505080516020909101519092909150 565b60805161010361007b6000396000606601526101036000f3fe
另一部分是将会被部署上链的代码,被叫做deployed code。这些代码就是在合约上链之后,后面与之交互时所使用的部分。
6080604052348015600f57600080fd5b506004361060325760003560e01c8063 3c6bb4361460375780633fb5c1cb146051575b600080fd5b603f60005481565b 60405190815260200160405180910390f35b6060605c3660046090565b606256 5b005b608a7f0000000000000000000000000000000000000000000000000000 0000000000008260a8565b60005550565b60006020828403121560a157600080 fd5b5035919050565b6000821982111560c857634e487b7160e01b6000526011 60045260246000fd5b50019056fea26469706673582212200b220ab5e3b5c534 b47030c3b89735e224c0d52e5454511a0274bc5bbbfd2abd64736f6c63430008 0f0033
在memory[0x00:0x0103] = code[0x7b:0x017e];执行之后,我们可以看到deployed code已经被拷贝到memory中。在操作码中,对应CODECOPY。
第3个关键点是memory[0x66:0x86] = temp2;
这一句对应构造函数中的c = _c;
因为c是immutable,所以c的值并不会占据storage空间,而是会被在字节码中写死。所以还需要在字节码中,把c的值赋上。
最后的形态如下:
memory中时最终的deployed code,而栈中的两个数字则指定了memory中deployed code的位置。
0x103 = 259,这就是deployed code的大小。
deploy函数的返回值就是最终的deployed code,紧接着deployed code就会被写入区块链,从而完成合约部署过程。