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
最近在读 Sudoswap 的合约代码,发现其中应用了 EIP-1167 的玩法,有些写法感觉很有意思,因此想特地写篇文章来记录分享一下。对于 EIP-1167 还不太了解的朋友可以看看我之前写的这篇文章。
在 Sudoswap 代码中,LSSVMPairFactory 合约是 Pair 的工厂合约,其中可以通过 createPairETH 创建 ETH 的交易对,通过 createPairERC20 创建 ERC20 的交易对。在创建交易对的过程中,调用 LSSVMPairCloner 库合约,其中应用了 EIP-1167 协议。
我们来看看库合约中创建 Pair 的方法:
function cloneETHPair(
address implementation,
ILSSVMPairFactoryLike factory,
ICurve bondingCurve,
IERC721 nft,
uint8 poolType
) internal returns (address instance) {
assembly {
let ptr := mload(0x40)
mstore(
ptr,
hex"60_72_3d_81_60_09_3d_39_f3_3d_3d_3d_3d_36_3d_3d_37_60_3d_60_35_36_39_36_60_3d_01_3d_73_00_00_00"
)
mstore(add(ptr, 0x1d), shl(0x60, implementation))
mstore(
add(ptr, 0x31),
hex"5a_f4_3d_3d_93_80_3e_60_33_57_fd_5b_f3_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00"
)
mstore(add(ptr, 0x3e), shl(0x60, factory))
mstore(add(ptr, 0x52), shl(0x60, bondingCurve))
mstore(add(ptr, 0x66), shl(0x60, nft))
mstore8(add(ptr, 0x7a), poolType)
instance := create(0, ptr, 0x7b)
}
}
其中前三个 mstore 与我们之前的文章介绍的 EIP-1167 的写法类似,但是后面又多了几行 mstore 以及 mstore8,这些都是什么意思呢?
我们先来看看前三个 mstore,这里的具体字节码与之前我们介绍的 1167 写法有些出入,这三行组合出的字节码是(长度为 62 字节):
分割一下,得到:
1 -> 60723d8160093d39f3
2 -> 3d3d3d3d363d3d37603d6035363936603d013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603357fd5bf3
其中第一部分的九个字节,反编译得到的结果是:
contract Contract {
function main() {
memory[returndata.length:returndata.length + 0x72] = code[0x09:0x7b];
return memory[returndata.length:returndata.length + 0x72];
}
}
可以看到,合约初始化时将字节码中从 9 字节到 123(0x7b) 字节的数据返回到 EVM 中,但是我们前面的字节码只有 62 字节,后面还有 61 字节都是什么呢?
此时我们再回看前面的 cloneETHPair 代码,没错,后面的 61 字节就是来自于其最后的几个 mstore,其中 factory、bondingCurve 以及 nft 都是地址类型,分别占据 20 字节,poolType 是 bool 类型,占据 1 个字节。
我们再将上面字节码的第二部分进行反编译:
contract Contract {
function main() {
var temp0 = returndata.length;
var var1 = returndata.length;
var temp1 = msg.data.length;
memory[returndata.length:returndata.length + temp1] = msg.data[returndata.length:returndata.length + temp1];
memory[msg.data.length:msg.data.length + 0x3d] = code[0x35:0x72];
var temp2;
temp2, memory[returndata.length:returndata.length + returndata.length] =
address(0xbebebebebebebebebebebebebebebebebebebebe)
.delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length + 0x3d]);
var temp3 = returndata.length;
var var0 = returndata.length;
memory[temp0:temp0 + temp3] = returndata[temp0:temp0 + temp3];
if (temp2) { return memory[var1:var1 + var0]; }
else { revert(memory[var1:var1 + var0]); }
}
}
从上面代码中可以看到,代理合约在调用逻辑合约的时候,总会在最后加上 61 (0x3d)字节的 calldata,正是前面的 factory、bondingCurve 、nft 以及 poolType。那么也就是说,任何通过代理合约转发到逻辑合约的调用,最后都会加上这段 calldata,意味着在逻辑合约的方法中,总是可以获取到这段 calldata。
我们来看看 Pair 合约中是如何获取这几个字段值的,以 bondingCurve() 为例:
function bondingCurve() public pure returns (ICurve _bondingCurve) {
// 这里 ETH Pair 是 61,ERC20 Pair 是 81
uint256 paramsLength = _immutableParamsLength();
assembly {
_bondingCurve := shr(
0x60,
calldataload(add(sub(calldatasize(), paramsLength), 20))
)
}
}
_immutableParamsLength() 返回常量,ETH Pair 返回 61,ERC20 Pair 返回 81。我们前面参考的方法是 cloneETHPair,因此我们这里默认其值为 61。
接下来的一段汇编代码,首先最里面的 calldatasize() 获取整个 calldata 的长度,其中是包含后面 61 字节的,然后用 calldatasize() 减去 paramsLength,得到的值就是调用合约方法的基础 calldata 长度,再加上 20(这里的 20 是前面 factory 的地址长度),获取到 bondingCurve 地址在整个 calldata 中的偏移量。然后调用 calldataload,将偏移量作为参数,可以获取到 bondingCurve 地址起始的 32 字节数据,其中前 20 字节是 bondingCurve 的地址,后面的 12 (0x60 / 8)字节通过 shr 操作移除,最终得到的就是 bondingCurve 的地址。
我们来看看图示,更加易懂:

其余的 factory 等获取方法同理,只是偏移量不同,最后的 add 数值不同。
看懂这块,我们同理就可以分析 ERC20 Pair 的操作,唯一的区别是在 cloneERC20Pair 方法中,最后还要在 mstore token 地址,因此 calldata 会多 20,即 81。
那么我们思考一下,为什么 Sudoswap 要采用这么复杂的操作,而不是直接将这几个字段直接存储到 Pair 中呢?最重要的就是 gas 的原因,Solidity 中处理 calldata 的操作耗费的 gas 很少,而如果将这些字段存储到 Pair 中,首先 sstore 操作就要耗费大量 gas,其次每次读取 sload 也会耗费大量 gas,与在 calldata 中操作的 gas 消耗可以说不是一个数量级的。
我们看到,Sudoswap 中采取了一个几乎极致的方法来节省 gas,这其中确实是有炫技的成分,不过里面的方法与思想还是值得我们学习的。随着合约开发逐渐卷起来,我觉得这种字节码层级操作的代码以后会越来越多,这块还是值得深入研究的。
欢迎和我交流
最近在读 Sudoswap 的合约代码,发现其中应用了 EIP-1167 的玩法,有些写法感觉很有意思,因此想特地写篇文章来记录分享一下。对于 EIP-1167 还不太了解的朋友可以看看我之前写的这篇文章。
在 Sudoswap 代码中,LSSVMPairFactory 合约是 Pair 的工厂合约,其中可以通过 createPairETH 创建 ETH 的交易对,通过 createPairERC20 创建 ERC20 的交易对。在创建交易对的过程中,调用 LSSVMPairCloner 库合约,其中应用了 EIP-1167 协议。
我们来看看库合约中创建 Pair 的方法:
function cloneETHPair(
address implementation,
ILSSVMPairFactoryLike factory,
ICurve bondingCurve,
IERC721 nft,
uint8 poolType
) internal returns (address instance) {
assembly {
let ptr := mload(0x40)
mstore(
ptr,
hex"60_72_3d_81_60_09_3d_39_f3_3d_3d_3d_3d_36_3d_3d_37_60_3d_60_35_36_39_36_60_3d_01_3d_73_00_00_00"
)
mstore(add(ptr, 0x1d), shl(0x60, implementation))
mstore(
add(ptr, 0x31),
hex"5a_f4_3d_3d_93_80_3e_60_33_57_fd_5b_f3_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00"
)
mstore(add(ptr, 0x3e), shl(0x60, factory))
mstore(add(ptr, 0x52), shl(0x60, bondingCurve))
mstore(add(ptr, 0x66), shl(0x60, nft))
mstore8(add(ptr, 0x7a), poolType)
instance := create(0, ptr, 0x7b)
}
}
其中前三个 mstore 与我们之前的文章介绍的 EIP-1167 的写法类似,但是后面又多了几行 mstore 以及 mstore8,这些都是什么意思呢?
我们先来看看前三个 mstore,这里的具体字节码与之前我们介绍的 1167 写法有些出入,这三行组合出的字节码是(长度为 62 字节):
分割一下,得到:
1 -> 60723d8160093d39f3
2 -> 3d3d3d3d363d3d37603d6035363936603d013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603357fd5bf3
其中第一部分的九个字节,反编译得到的结果是:
contract Contract {
function main() {
memory[returndata.length:returndata.length + 0x72] = code[0x09:0x7b];
return memory[returndata.length:returndata.length + 0x72];
}
}
可以看到,合约初始化时将字节码中从 9 字节到 123(0x7b) 字节的数据返回到 EVM 中,但是我们前面的字节码只有 62 字节,后面还有 61 字节都是什么呢?
此时我们再回看前面的 cloneETHPair 代码,没错,后面的 61 字节就是来自于其最后的几个 mstore,其中 factory、bondingCurve 以及 nft 都是地址类型,分别占据 20 字节,poolType 是 bool 类型,占据 1 个字节。
我们再将上面字节码的第二部分进行反编译:
contract Contract {
function main() {
var temp0 = returndata.length;
var var1 = returndata.length;
var temp1 = msg.data.length;
memory[returndata.length:returndata.length + temp1] = msg.data[returndata.length:returndata.length + temp1];
memory[msg.data.length:msg.data.length + 0x3d] = code[0x35:0x72];
var temp2;
temp2, memory[returndata.length:returndata.length + returndata.length] =
address(0xbebebebebebebebebebebebebebebebebebebebe)
.delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length + 0x3d]);
var temp3 = returndata.length;
var var0 = returndata.length;
memory[temp0:temp0 + temp3] = returndata[temp0:temp0 + temp3];
if (temp2) { return memory[var1:var1 + var0]; }
else { revert(memory[var1:var1 + var0]); }
}
}
从上面代码中可以看到,代理合约在调用逻辑合约的时候,总会在最后加上 61 (0x3d)字节的 calldata,正是前面的 factory、bondingCurve 、nft 以及 poolType。那么也就是说,任何通过代理合约转发到逻辑合约的调用,最后都会加上这段 calldata,意味着在逻辑合约的方法中,总是可以获取到这段 calldata。
我们来看看 Pair 合约中是如何获取这几个字段值的,以 bondingCurve() 为例:
function bondingCurve() public pure returns (ICurve _bondingCurve) {
// 这里 ETH Pair 是 61,ERC20 Pair 是 81
uint256 paramsLength = _immutableParamsLength();
assembly {
_bondingCurve := shr(
0x60,
calldataload(add(sub(calldatasize(), paramsLength), 20))
)
}
}
_immutableParamsLength() 返回常量,ETH Pair 返回 61,ERC20 Pair 返回 81。我们前面参考的方法是 cloneETHPair,因此我们这里默认其值为 61。
接下来的一段汇编代码,首先最里面的 calldatasize() 获取整个 calldata 的长度,其中是包含后面 61 字节的,然后用 calldatasize() 减去 paramsLength,得到的值就是调用合约方法的基础 calldata 长度,再加上 20(这里的 20 是前面 factory 的地址长度),获取到 bondingCurve 地址在整个 calldata 中的偏移量。然后调用 calldataload,将偏移量作为参数,可以获取到 bondingCurve 地址起始的 32 字节数据,其中前 20 字节是 bondingCurve 的地址,后面的 12 (0x60 / 8)字节通过 shr 操作移除,最终得到的就是 bondingCurve 的地址。
我们来看看图示,更加易懂:

其余的 factory 等获取方法同理,只是偏移量不同,最后的 add 数值不同。
看懂这块,我们同理就可以分析 ERC20 Pair 的操作,唯一的区别是在 cloneERC20Pair 方法中,最后还要在 mstore token 地址,因此 calldata 会多 20,即 81。
那么我们思考一下,为什么 Sudoswap 要采用这么复杂的操作,而不是直接将这几个字段直接存储到 Pair 中呢?最重要的就是 gas 的原因,Solidity 中处理 calldata 的操作耗费的 gas 很少,而如果将这些字段存储到 Pair 中,首先 sstore 操作就要耗费大量 gas,其次每次读取 sload 也会耗费大量 gas,与在 calldata 中操作的 gas 消耗可以说不是一个数量级的。
我们看到,Sudoswap 中采取了一个几乎极致的方法来节省 gas,这其中确实是有炫技的成分,不过里面的方法与思想还是值得我们学习的。随着合约开发逐渐卷起来,我觉得这种字节码层级操作的代码以后会越来越多,这块还是值得深入研究的。
欢迎和我交流
No activity yet