专栏名称: 廖雪峰
业余马拉松选手廖雪峰的公众号,各种鸡汤混杂负能量,心理承受能力不强者慎重订阅。
目录
相关文章推荐
参考消息  ·  “中国和欧洲霸榜,美国最高排第13” ·  昨天  
IPRdaily  ·  浅谈商业秘密之技术秘密 ·  昨天  
参考消息  ·  39年,中国收获亚冬会100金! ·  3 天前  
参考消息  ·  祝贺李方慧!首金来了! ·  3 天前  
参考消息  ·  习近平主席宣布哈尔滨亚冬会开幕 ·  4 天前  
51好读  ›  专栏  ›  廖雪峰

以太坊delegatecall详解

廖雪峰  · 公众号  ·  · 2024-03-27 08:22

正文

在以太坊合约中,一个合约可以调用另一个合约,以实现功能模块化。

除了普通的跨合约调用,以太坊还提供了 delegatecall 来跨合约调用。

delegatecall 跨合约调用与普通跨合约调用不同,它不会改变代码执行的上下文环境,而是基于当前合约的上下文来执行目标合约代码,就像这些代码是当前合约自己的代码一样。

什么是合约上下文?我们以一个简单的示例来说,就可以正确理解 delegatecall 的调用方式。

先编写两个合约: Target Delegate ,完整代码如下:

pragma solidity ^0.8.25;

contract Target {

event Log(string msg, address thisAddr, address msgSender, uint256 msgValue, address txOrigin);

string public name = "target";
uint256 public version = 1;

function save(string memory _name, uint256 _version) public payable {
name = _name;
version = _version;
emit Log("Target.save", address(this), msg.sender, msg.value, tx.origin);
}
}

contract Delegate {

event Log(string msg, address thisAddr, address msgSender, uint256 msgValue, address txOrigin);

string public name = "delegate";
uint256 public version = 10;

Target public target;

constructor (address _target) {
target = Target(_target);
}

function save(string memory _name, uint256 _version) public payable {
emit Log("Delegate.save", address(this), msg.sender, msg.value, tx.origin);
target.save(_name, _version);
}

function delegateSave(string memory _name, uint256 _version) public payable {
emit Log("Delegate.delegateSave", address(this), msg.sender, msg.value, tx.origin);
(bool success, bytes memory returndata) = address(target).delegatecall(
abi.encodeWithSelector(
Target.save.selector,
_name,
_version
)
);
if (! success) {
revert("delegate call failed.");
}
}
}

这两个合约部署后, Delegate 地址为 0x2c70... Target 地址为 0xdC31... ,此处地址仅为示例合约部署到某一条ETH链的特定地址,重复本文实验会得到不同的部署地址。

Delegate 合约中,编写两个函数:

  • save() 函数,以正常方式调用 Target 合约的 save() 函数;

  • delegateSave() 函数,以 delegatecall 方式调用 Target 合约的 save() 函数。

调用关系如下图所示:

另外注意到我们在两个合约中均存储了 name version ,并设定了初始值。部署合约后,两个合约的初始状态如下:

下一步,我们用地址 0x98fd... 这个外部地址调用 Delegate save() 函数,传入参数:

  • name = "bob"

  • version = 123

  • ETH = 0.01

Delegate 合约的 save() 函数内部,打印出的日志为:

  • msg = "Delegate.save"

  • thisAddr = 0x2c70...

  • msgSender = 0x98fd...

  • msgValue = 0.01

  • txOrigin = 0x98fd...

Target 合约的 save() 函数内部,打印出的日志为:

  • msg = "Target.save"

  • thisAddr = 0xdC31...

  • msgSender = 0x2c70...

  • msgValue = 0

  • txOrigin = 0x98fd...

可见,正常调用 Target 合约函数,在 Target 合约内部执行 save() 函数时, address(this) 总是指向当前合约, msg.sender 是调用方 Delegate 的地址, msg.value 不再是外部传入的 0.01 ,这就是跨合约调用函数时,上下文会自动切换。

执行后,我们检测两个合约的状态如下:

可见, Target 合约的 save() 函数修改了自身状态,不会修改 Delegate 合约的状态,而外部传入的ETH则留在 Delegate 合约中。

现在我们再以外部地址 0x98fd... 调用 Delegate 合约的 delegateSave() 函数,传入参数:

  • name = "alice"

  • version = 456

  • ETH = 0.02

这个时候, Delegate 合约的 delegateSave() 函数内部,以 delegateCall 调用 Target 合约的 save() 函数,我们先观察执行后两个合约的状态:

注意到 Target 合约的 save() 函数代码如下:

function save(string memory _name, uint256 _version) public payable {
name = _name;
version = _version;
emit ...
}

但它却并没有修改自身状态,而是把 Delegate 合约的 name version 给改了!

这就是 delegatecall 调用时,不会切换当前上下文,导致 Target 合约的 save() 函数看起来就像是在 Delegate 合约中执行的。

我们检查日志,可以看到 Delegate 合约打印的日志:

msg = "Delegate.delegateSave"






请到「今天看啥」查看全文