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-1167,又称 Minimal Proxy Contract,提供了一种低成本复制合约的方法。它有什么意义呢?我们先来看个例子:
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 通过原始 bytecode 创建合约
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
在 Uniswap v2 的工厂合约中,需要创建交易对合约,这里的方法是直接使用交易对合约,即 UniswapV2Pair 合约的 creationCode 进行创建。由于是创建原始的合约内容,因此有一个缺点就是耗费的 gas 取决于 Pair 合约的大小,Pair 合约的内容越多,耗费的 gas 越多。
那么有没有什么改进方法呢?答案是有的,就是通过代理合约来转发调用。从创建原始合约变为创建代理合约,而代理合约只需要负责将调用转发给原始合约就行了,实际执行的逻辑仍然使用最原始的合约。接下来的内容需要大家了解内存布局和 delegatecall 相关的内容,不了解的朋友可以看看这里和这里。
提到代理,可能大家想到的第一反应就是合约升级,但是我们今天说的代理并不涉及合约升级,它仅仅负责合约调用的转发。
我们先来看看可升级合约的代理合约架构:

整个架构中存在一个代理合约和 N 个逻辑合约,只有一套数据,需要升级时则替换逻辑合约,同一时间只能存在一个逻辑合约。
再来看看今天提到的 Minimal Proxy Contract 架构:

整个架构中存在 N 个代理合约和一个逻辑合约,有多套数据分别存储在不同的代理合约中,所有代理合约共享逻辑合约的执行逻辑,同一时间存在多个代理合约。Minimal Proxy Contract 的原理就是将代理合约作为逻辑合约的复制品,各个代理合约存储各自的数据,需要多少份复制品就创建多少个代理合约即可。而代理合约本身只负责请求转发,因此其内容很少,从而耗费更少的 gas。
我们来看一个例子(注意这里是为了简单起见直接使用了构造函数,实际应用中不应该使用构造函数,因为这部分不会在代理合约中运行,即不会被初始化。因此如果有初始化逻辑,需要放在 initialize 函数中额外调用):
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;
contract Demo {
uint256 public a;
constructor() {
a = 1000;
}
function setA(uint256 _a) external {
a = _a;
}
}
现在我们要对这个 Demo 合约进行复制,可以借助 OZ 的 Clones 合约来完成:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/Clones.sol)
pragma solidity ^0.8.0;
library Clones {
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(ptr, 0x14), shl(0x60, implementation))
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
instance := create(0, ptr, 0x37)
}
require(instance != address(0), "ERC1167: create failed");
}
///....
}
clone 方法需要接受一个原始逻辑合约的地址,然后返回新生成的克隆合约(也就是我们上面说的代理合约)地址。
我们来看看上面代码是什么意思,首先 let ptr := mload(0x40) 获取内存中空闲内存指针的位置,下一行的 mstore(ptr, 0x3d602d80600a3d3981f33…),将该数据存入内存中,接下来的 shl(0x60, implementation) 将地址左移 0x60,即 96 位。由于 address 类型实际只占用 20 个字节,而传入的参数是通过 0 补齐到 32 字节的。假设传入的地址是
0xbebebebebebebebebebebebebebebebebebebebe,则补齐的内容为:
0x000000000000000000000000bebebebebebebebebebebebebebebebebebebebe
左移 96 位之后恰好获得原始的地址数据。
接下来将地址数据 mstore 储存到内存中,内存位置为 0x14,即 20。这个数据恰好是上面一行 0x3d602d80600a3d39 中截断后面零值的长度。
第三个 mstore 再将 0x5af43d82803e903d… 拼接到前面的数据所在内存位置后面。三个 mstore 操作下来,此时内存中的数据为:
即三部分数据拼接到一起的结果。但是在合约地址前后拼接的这两部分是什么意思呢?
首先我们将前 10 个字节 3d602d80600a3d3981f3 反编译一下得到:
contract Contract {
function main() {
var var0 = returndata.length;
memory[returndata.length:returndata.length + 0x2d] = code[0x0a:0x37];
return memory[var0:var0 + 0x2d];
}
}
即克隆合约的构造方法,内容是将整个克隆合约的字节码返回给 EVM。
再将后面的 45 字节数据 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 反编译:
contract Contract {
function main() {
var temp0 = msg.data.length;
memory[returndata.length:returndata.length + temp0] = msg.data[returndata.length:returndata.length + temp0];
var temp1 = returndata.length;
var temp2;
temp2, memory[returndata.length:returndata.length + returndata.length] = address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length]);
var temp3 = returndata.length;
memory[temp1:temp1 + temp3] = returndata[temp1:temp1 + temp3];
var var1 = temp1;
var var0 = returndata.length;
if (temp2) { return memory[var1:var1 + var0]; }
else { revert(memory[var1:var1 + var0]); }
}
}
这部分内容是利用 delegatecall 将调用进行转发的逻辑。
clone 方法的最后一行使用了 create 方法创建克隆合约,长度 0x37 即 55 是内存中克隆合约字节码的长度。
现在我们来实际操作一下,首先编写克隆工厂合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "./Clones.sol";
contract MyClonesFactory {
using Clones for address;
event ProxyGenerated(address proxy);
function clone(address implementation) external {
address proxy = implementation.clone();
emit ProxyGenerated(proxy);
}
}
接下来先部署原始 Demo 合约,然后部署 MyClonesFactory 合约,随后调用 clone 方法,传入 Demo 合约地址,即可创建克隆合约。
创建原始 Demo 合约时耗费了 14 万 gas:

而 clone 方法创建克隆合约只花费了 6 万 gas:

注意克隆合约是通过内部交易创建的:

Etherscan 也已经支持了 EIP-1167 的合约验证,只要原始合约代码已经验证,那么克隆合约代码自动验证。
原始合约验证代码之前的克隆合约:

可以看到克隆合约的字节码就是去除构造方法字节码之后其余的部分,然后我们验证一下原始合约的代码,这时克隆合约就已经自动验证了,并且标注了 Minimal Proxy Contract:

注意需要直接验证原始逻辑合约,如果验证克隆合约会失败。
EIP-1167 提供了一种低成本克隆合约的方法,并且 Etherscan 也已经提供了支持,在需要创建多个合约的场景下,不失为一种好方法。
欢迎和我交流
https://medium.com/coinmonks/diving-into-smart-contracts-minimal-proxy-eip-1167-3c4e7f1a41b8
EIP-1167,又称 Minimal Proxy Contract,提供了一种低成本复制合约的方法。它有什么意义呢?我们先来看个例子:
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 通过原始 bytecode 创建合约
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
在 Uniswap v2 的工厂合约中,需要创建交易对合约,这里的方法是直接使用交易对合约,即 UniswapV2Pair 合约的 creationCode 进行创建。由于是创建原始的合约内容,因此有一个缺点就是耗费的 gas 取决于 Pair 合约的大小,Pair 合约的内容越多,耗费的 gas 越多。
那么有没有什么改进方法呢?答案是有的,就是通过代理合约来转发调用。从创建原始合约变为创建代理合约,而代理合约只需要负责将调用转发给原始合约就行了,实际执行的逻辑仍然使用最原始的合约。接下来的内容需要大家了解内存布局和 delegatecall 相关的内容,不了解的朋友可以看看这里和这里。
提到代理,可能大家想到的第一反应就是合约升级,但是我们今天说的代理并不涉及合约升级,它仅仅负责合约调用的转发。
我们先来看看可升级合约的代理合约架构:

整个架构中存在一个代理合约和 N 个逻辑合约,只有一套数据,需要升级时则替换逻辑合约,同一时间只能存在一个逻辑合约。
再来看看今天提到的 Minimal Proxy Contract 架构:

整个架构中存在 N 个代理合约和一个逻辑合约,有多套数据分别存储在不同的代理合约中,所有代理合约共享逻辑合约的执行逻辑,同一时间存在多个代理合约。Minimal Proxy Contract 的原理就是将代理合约作为逻辑合约的复制品,各个代理合约存储各自的数据,需要多少份复制品就创建多少个代理合约即可。而代理合约本身只负责请求转发,因此其内容很少,从而耗费更少的 gas。
我们来看一个例子(注意这里是为了简单起见直接使用了构造函数,实际应用中不应该使用构造函数,因为这部分不会在代理合约中运行,即不会被初始化。因此如果有初始化逻辑,需要放在 initialize 函数中额外调用):
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;
contract Demo {
uint256 public a;
constructor() {
a = 1000;
}
function setA(uint256 _a) external {
a = _a;
}
}
现在我们要对这个 Demo 合约进行复制,可以借助 OZ 的 Clones 合约来完成:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/Clones.sol)
pragma solidity ^0.8.0;
library Clones {
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(ptr, 0x14), shl(0x60, implementation))
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
instance := create(0, ptr, 0x37)
}
require(instance != address(0), "ERC1167: create failed");
}
///....
}
clone 方法需要接受一个原始逻辑合约的地址,然后返回新生成的克隆合约(也就是我们上面说的代理合约)地址。
我们来看看上面代码是什么意思,首先 let ptr := mload(0x40) 获取内存中空闲内存指针的位置,下一行的 mstore(ptr, 0x3d602d80600a3d3981f33…),将该数据存入内存中,接下来的 shl(0x60, implementation) 将地址左移 0x60,即 96 位。由于 address 类型实际只占用 20 个字节,而传入的参数是通过 0 补齐到 32 字节的。假设传入的地址是
0xbebebebebebebebebebebebebebebebebebebebe,则补齐的内容为:
0x000000000000000000000000bebebebebebebebebebebebebebebebebebebebe
左移 96 位之后恰好获得原始的地址数据。
接下来将地址数据 mstore 储存到内存中,内存位置为 0x14,即 20。这个数据恰好是上面一行 0x3d602d80600a3d39 中截断后面零值的长度。
第三个 mstore 再将 0x5af43d82803e903d… 拼接到前面的数据所在内存位置后面。三个 mstore 操作下来,此时内存中的数据为:
即三部分数据拼接到一起的结果。但是在合约地址前后拼接的这两部分是什么意思呢?
首先我们将前 10 个字节 3d602d80600a3d3981f3 反编译一下得到:
contract Contract {
function main() {
var var0 = returndata.length;
memory[returndata.length:returndata.length + 0x2d] = code[0x0a:0x37];
return memory[var0:var0 + 0x2d];
}
}
即克隆合约的构造方法,内容是将整个克隆合约的字节码返回给 EVM。
再将后面的 45 字节数据 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 反编译:
contract Contract {
function main() {
var temp0 = msg.data.length;
memory[returndata.length:returndata.length + temp0] = msg.data[returndata.length:returndata.length + temp0];
var temp1 = returndata.length;
var temp2;
temp2, memory[returndata.length:returndata.length + returndata.length] = address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length]);
var temp3 = returndata.length;
memory[temp1:temp1 + temp3] = returndata[temp1:temp1 + temp3];
var var1 = temp1;
var var0 = returndata.length;
if (temp2) { return memory[var1:var1 + var0]; }
else { revert(memory[var1:var1 + var0]); }
}
}
这部分内容是利用 delegatecall 将调用进行转发的逻辑。
clone 方法的最后一行使用了 create 方法创建克隆合约,长度 0x37 即 55 是内存中克隆合约字节码的长度。
现在我们来实际操作一下,首先编写克隆工厂合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "./Clones.sol";
contract MyClonesFactory {
using Clones for address;
event ProxyGenerated(address proxy);
function clone(address implementation) external {
address proxy = implementation.clone();
emit ProxyGenerated(proxy);
}
}
接下来先部署原始 Demo 合约,然后部署 MyClonesFactory 合约,随后调用 clone 方法,传入 Demo 合约地址,即可创建克隆合约。
创建原始 Demo 合约时耗费了 14 万 gas:

而 clone 方法创建克隆合约只花费了 6 万 gas:

注意克隆合约是通过内部交易创建的:

Etherscan 也已经支持了 EIP-1167 的合约验证,只要原始合约代码已经验证,那么克隆合约代码自动验证。
原始合约验证代码之前的克隆合约:

可以看到克隆合约的字节码就是去除构造方法字节码之后其余的部分,然后我们验证一下原始合约的代码,这时克隆合约就已经自动验证了,并且标注了 Minimal Proxy Contract:

注意需要直接验证原始逻辑合约,如果验证克隆合约会失败。
EIP-1167 提供了一种低成本克隆合约的方法,并且 Etherscan 也已经提供了支持,在需要创建多个合约的场景下,不失为一种好方法。
欢迎和我交流
https://medium.com/coinmonks/diving-into-smart-contracts-minimal-proxy-eip-1167-3c4e7f1a41b8
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 还有一个优点,相...
Share Dialog
Share Dialog
Smart Contract Developer

Subscribe to xyyme.eth

Subscribe to xyyme.eth
<100 subscribers
<100 subscribers
No activity yet