EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer
EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer

Subscribe to xyyme.eth

Subscribe to xyyme.eth
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
call 和 delegatecall 是 Solidity 中调用外部合约的方法,但是它俩却有挺大的区别。假设 A 合约调用 B 合约,当在 A 合约中使用 call 调用 B 合约时,使用的是 B 合约的上下文,修改的是 B 合约的内存插槽值。而在如果在 A 合约中使用 delegatecall 调用 B 合约,那么在 B 合约的函数执行过程中,使用的是 A 合约的上下文,同时修改的也是 A 合约的内存插槽值。这么说有些抽象,我们来看一个简单的示意图:


从上面的图中我们可以看出,在使用 call 调用时,B 合约使用的上下文数据均是 B 本身的。而当使用 delegatecall 调用时,B 合约使用了 A 合约中的上下文数据。我们来写段代码测试一下:
pragma solidity 0.8.13;
contract A {
address public b;
constructor(address _b) {
b = _b;
}
function foo() external {
(bool success, bytes memory data) =
b.call(abi.encodeWithSignature("foo()"));
require(success, "Tx failed");
}
}
contract B {
event Log(address sender, address me);
function foo() external {
emit Log(msg.sender, address(this));
}
}
上面代码中,我们在 A 合约中使用 call 调用 B 合约,通过 Log 事件记录一些信息。先部署 B 合约,然后将其地址作为参数部署 A 合约,接着我们调用 foo 函数,可以获取到 Log 事件的内容为:

与我们前面的说的规则一致,使用 call 调用时,使用的是 B 本身的上下文。接下来我们将 call 改成 delegatecall:
function foo() external {
(bool success, bytes memory data) =
b.delegatecall(abi.encodeWithSignature("foo()"));
require(success, "Tx failed");
}
再来看看执行结果:

可以看到当使用了 delegatecall 调用时,使用了 A 合约的上下文。
上面我们还提到,当使用 delegatecall 时,修改的是调用合约的内存插槽值,这是什么意思呢,我们来看一个例子:
pragma solidity 0.8.13;
contract A {
uint256 public alice;
uint256 public bob;
address public b;
constructor(address _b) {
b = _b;
}
function foo(uint256 _alice, uint256 _bob) external {
(bool success, bytes memory data) =
b.delegatecall(abi.encodeWithSignature("foo(uint256,uint256)",
_alice, _bob));
require(success, "Tx failed");
}
}
contract B {
uint256 public alice;
uint256 public bob;
function foo(uint256 _alice, uint256 _bob) external {
alice = _alice;
bob = _bob;
}
}
这段代码中,我们使用 delegatecall 来调用 foo 函数,foo 函数的作用是给 B 合约的两个变量赋值。但是实际调用后的结果是,A 合约的两个变量被赋值,而 B 中的变量仍为空。这就是我们前面说的,delegatecall 会修改调用合约的内存插槽值,我们来看一个图示:

在 A 合约中有三个状态变量,B 合约中有两个状态变量。当 A 合约使用 delegatecall 调用 B 合约时,对 B 合约状态变量的赋值会通过插槽顺序分别影响 A 合约的各个变量。也就是说,对 B 合约插槽 0 的变量 alice 赋值,实际上是把值赋给了 A 合约插槽 0 的变量 alice。同理,对 B 合约的第 n 个插槽赋值,实际上会对 A 合约的第 n 个插槽赋值。注意,这里仅仅和插槽顺序有关,而和变量名无关。如果我们将 B 合约改为:
contract B {
// 调换了变量声明顺序
uint256 public bob;
uint256 public alice;
function foo(uint256 _alice, uint256 _bob) external {
// 调换了赋值内容
bob = _alice;
alice = _bob;
}
}
这段代码中,虽然变量声明以及赋值的顺序调换,但是 foo 的内容仍然是将 _alice 赋值给插槽 0 的变量,将 _bob 赋值给插槽 1 的变量,因此 A 合约的结果不变。
学习理解 delegatecall 是我们后面学习合约升级的基础,合约升级的原理就是代理合约通过 delegatecall 调用逻辑合约,此时逻辑合约的上下文以及数据都是来自于代理合约,那么即使升级,更换了逻辑合约,所有的数据仍然存在于代理合约中,没有影响。可升级合约还有一个限制是,在升级合约时,不能更改已有的状态变量的顺序,如果需要新添变量,只能放在当前所有变量之后,不能在其中插入,原因就是这会改变插槽对应关系,使变量内容混乱。例如,若升级前的插槽为:

此时,变量 a 和 b 的值分别存储在代理合约的插槽 0,1 中。若添加变量 c,将其放在 a 和 b 中间,那么后续对于 c 的修改实际修改的是 b 的插槽,而对于 b 的修改则是在一个新的插槽上操作,造成数据混乱。
delegatecall 会在被调用合约中使用调用合约的上下文,同时影响的是调用合约的内存插槽,这有时会对合约开发带来一些困扰。在使用时,一定要多考虑各方面的影响。同时,delegatecall 也是代理合约升级模式的基石,要理解合约升级,必须要明白这种调用方式的方方面面。
欢迎和我交流
call 和 delegatecall 是 Solidity 中调用外部合约的方法,但是它俩却有挺大的区别。假设 A 合约调用 B 合约,当在 A 合约中使用 call 调用 B 合约时,使用的是 B 合约的上下文,修改的是 B 合约的内存插槽值。而在如果在 A 合约中使用 delegatecall 调用 B 合约,那么在 B 合约的函数执行过程中,使用的是 A 合约的上下文,同时修改的也是 A 合约的内存插槽值。这么说有些抽象,我们来看一个简单的示意图:


从上面的图中我们可以看出,在使用 call 调用时,B 合约使用的上下文数据均是 B 本身的。而当使用 delegatecall 调用时,B 合约使用了 A 合约中的上下文数据。我们来写段代码测试一下:
pragma solidity 0.8.13;
contract A {
address public b;
constructor(address _b) {
b = _b;
}
function foo() external {
(bool success, bytes memory data) =
b.call(abi.encodeWithSignature("foo()"));
require(success, "Tx failed");
}
}
contract B {
event Log(address sender, address me);
function foo() external {
emit Log(msg.sender, address(this));
}
}
上面代码中,我们在 A 合约中使用 call 调用 B 合约,通过 Log 事件记录一些信息。先部署 B 合约,然后将其地址作为参数部署 A 合约,接着我们调用 foo 函数,可以获取到 Log 事件的内容为:

与我们前面的说的规则一致,使用 call 调用时,使用的是 B 本身的上下文。接下来我们将 call 改成 delegatecall:
function foo() external {
(bool success, bytes memory data) =
b.delegatecall(abi.encodeWithSignature("foo()"));
require(success, "Tx failed");
}
再来看看执行结果:

可以看到当使用了 delegatecall 调用时,使用了 A 合约的上下文。
上面我们还提到,当使用 delegatecall 时,修改的是调用合约的内存插槽值,这是什么意思呢,我们来看一个例子:
pragma solidity 0.8.13;
contract A {
uint256 public alice;
uint256 public bob;
address public b;
constructor(address _b) {
b = _b;
}
function foo(uint256 _alice, uint256 _bob) external {
(bool success, bytes memory data) =
b.delegatecall(abi.encodeWithSignature("foo(uint256,uint256)",
_alice, _bob));
require(success, "Tx failed");
}
}
contract B {
uint256 public alice;
uint256 public bob;
function foo(uint256 _alice, uint256 _bob) external {
alice = _alice;
bob = _bob;
}
}
这段代码中,我们使用 delegatecall 来调用 foo 函数,foo 函数的作用是给 B 合约的两个变量赋值。但是实际调用后的结果是,A 合约的两个变量被赋值,而 B 中的变量仍为空。这就是我们前面说的,delegatecall 会修改调用合约的内存插槽值,我们来看一个图示:

在 A 合约中有三个状态变量,B 合约中有两个状态变量。当 A 合约使用 delegatecall 调用 B 合约时,对 B 合约状态变量的赋值会通过插槽顺序分别影响 A 合约的各个变量。也就是说,对 B 合约插槽 0 的变量 alice 赋值,实际上是把值赋给了 A 合约插槽 0 的变量 alice。同理,对 B 合约的第 n 个插槽赋值,实际上会对 A 合约的第 n 个插槽赋值。注意,这里仅仅和插槽顺序有关,而和变量名无关。如果我们将 B 合约改为:
contract B {
// 调换了变量声明顺序
uint256 public bob;
uint256 public alice;
function foo(uint256 _alice, uint256 _bob) external {
// 调换了赋值内容
bob = _alice;
alice = _bob;
}
}
这段代码中,虽然变量声明以及赋值的顺序调换,但是 foo 的内容仍然是将 _alice 赋值给插槽 0 的变量,将 _bob 赋值给插槽 1 的变量,因此 A 合约的结果不变。
学习理解 delegatecall 是我们后面学习合约升级的基础,合约升级的原理就是代理合约通过 delegatecall 调用逻辑合约,此时逻辑合约的上下文以及数据都是来自于代理合约,那么即使升级,更换了逻辑合约,所有的数据仍然存在于代理合约中,没有影响。可升级合约还有一个限制是,在升级合约时,不能更改已有的状态变量的顺序,如果需要新添变量,只能放在当前所有变量之后,不能在其中插入,原因就是这会改变插槽对应关系,使变量内容混乱。例如,若升级前的插槽为:

此时,变量 a 和 b 的值分别存储在代理合约的插槽 0,1 中。若添加变量 c,将其放在 a 和 b 中间,那么后续对于 c 的修改实际修改的是 b 的插槽,而对于 b 的修改则是在一个新的插槽上操作,造成数据混乱。
delegatecall 会在被调用合约中使用调用合约的上下文,同时影响的是调用合约的内存插槽,这有时会对合约开发带来一些困扰。在使用时,一定要多考虑各方面的影响。同时,delegatecall 也是代理合约升级模式的基石,要理解合约升级,必须要明白这种调用方式的方方面面。
欢迎和我交流
No activity yet