在了解合约调用之前,我们需要知道调用合约的本质是什么。在我们创建合约的时候,由run函数初始化的智能合约code(ret)储存在stateDB中。也就是说在内存中并没有Contract这个对象,而只是存在智能合约code。那我们如何调用合约呢?本质上,调用合约实际上是从合约账户中取出合约代码,然后NewContract()创建出一个临时的contract对象(如下图),然后执行contract的SetCallCode()或其他方法,确定智能合约的执行环境,然后执行run()函数,返回执行后的代码。
知道了这个过程,我们再来看看下面这个函数,智能合约的调用或普通交易——Call()。
Call()的主要功能是执行一笔交易,具体的步骤如下:
1、交易执行前的检查:深度判断和余额状况;
2、如果世界状态中不存在这个账号,则创建这个账号;
3、进行转账;
4、创建一个待执行的合约对象,并执行;
5、处理交易执行返回值
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int)
caller:转出方地址;
addr:转入方地址,如果是调用智能合约,那就是智能合约的地址;
input:调用函数的参数
gas:当前交易的剩余gas;
value:转账额度;
首先,执行前检查。
// 如果不允许递归深度大于0,直接退出
if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, gas, nil
}
// 如果递归深度大于1024,直接退出
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
// 如果余额不够,直接退出
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, gas, ErrInsufficientBalance
}
然后判断世界状态中是否存在这个账号,如果不存在则创建账号;
var (
to = AccountRef(addr)
snapshot = evm.StateDB.Snapshot()
)
if !evm.StateDB.Exist(addr) {
precompiles := PrecompiledContractsHomestead
if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
precompiles = PrecompiledContractsByzantium
}
if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
// Calling a non existing account, don't do anything, but ping the tracer
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)
}
return nil, gas, nil
}
evm.StateDB.CreateAccount(addr)
}
第三步进行转账。
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
第四步创建一个待执行的合约对象,并执行。
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
// Even if the account has no code, we need to continue because it might be a precompile
start := time.Now()
// Capture the tracer start/end events in debug mode
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
defer func() { // Lazy evaluation of the parameters
evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
}()
}
ret, err = run(evm, contract, input, false)
这里create()不同的是构建新contract对象的时候,create()调用了SetCodeOptionalHash(&address, codeAndHash),而这里我们调用的是SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))。create()中构建的contract对象中的Code是来自原始transaction中的Payload,而在Call()构建的contract对象中的Code则是create()中初始化智能合约即执行run()之后返回的ret,这两者在结构上是有区别的。
当我们写完智能合约通过solidity编译器生成合约代码时,最前面是一段合约部署代码,用来引导合约的部署,transaction中的Payload就包含这一段,它可以引导执行合约构造函数。当我们部署完成后,在智能合约地址中储存的Code却不包含合约部署代码部分,而是只有后面的部分,即ret。所以当我们调用智能合约的时候,构建的contract的code只有ret,直接从合约函数引导的代码开始执行。
我们说过智能合约EVM的递归调用深度为1024,也就是指通过一个合约调用另一个合约,像这样的调用可以递归1024次。
为什么说是“递归”?因为从一个智能合约调用另一个智能合约,比如通过Call()方法,都要重新构建contract实例,然后执行run()。而run()的执行是通过EVMinterpreter.Run()进行的。而在EVMInterpreter结构体中又传入了*EVM的地址,然后执行了evm.depth++。所以实际上每一次调用都是在同一个EVM内进行的。
连环调用的方法有下面几种:1、Call();2、CallCode();3、DelegateCall()。
Call()
to = AccountRef(addr)
contract := NewContract(caller, to, value, gas)
// 假设有外部账户A,合约账户B和合约账户C
A Call B ——> ContractB
CallerAddress: A
Caller: A
self: B
B Call C ——> ContractC
CallerAddress: B
Caller: B
self: C
CallCode()
to = AccountRef(caller.Address())
contract := NewContract(caller, to, value, gas)
// 假设有外部账户A,合约账户B和合约账户C
A Call B ——> ContractB
CallerAddress: A
Caller: A
self: B
B Callcode C ——> ContractC
CallerAddress: B
Caller: B
self: B
delegateCall()
to = AccountRef(caller.Address())
contract := NewContract(caller, to, nil, gas).AsDelegate()
func (c *Contract) AsDelegate() *Contract {
parent := c.caller.(*Contract)
c.CallerAddress = parent.CallerAddress
c.value = parent.value
return c
}
// 假设有外部账户A,合约账户B和合约账户C
A Call B ——> ContractB
CallerAddress: A
Caller: A
self: B
B DelegateCall C ——> ContractC
CallerAddress: A
Caller: B
self: B
从代码上看,这三者的主要区别就是这一点,主要就是在contract结构中的callerAddress、caller和self这三个的值不同。
如果外部账户A的某个操作通过Call方法调用B合约,而B合约又通过Call方法调用了C合约,那么最后实际上修改的是合约C账户的值;
如果外部账户A的某个操作通过Call方法调用B合约,而B合约通过CallCode方法调用了C合约,那么B只是调用了C中的函数代码,而最终改变的还是合约B账户的值。
DelegateCall其实跟CallCode方法的目的类似,都是只调用指定地址(合约C)的代码,而操作B的值。只不过它明确了CallerAddress是来自A,而不是B。所以这两种方法都可以用来实现动态库:即你调用我的函数和方法改动的都是你自己的数据)。