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
Share Dialog
Share Dialog

Subscribe to xyyme.eth

Subscribe to xyyme.eth
<100 subscribers
<100 subscribers
我们今天来研究一下前段时间 Audius 项目被黑的原因。这部分涉及到了内存槽位和合约升级方面的内容,如果有朋友不了解这一块,可以看看我之前写的这个系列。看完之后再来看这篇文章就比较容易理解了。
我们先看一下 Audius 的合约架构,它采用了可升级合约的架构:

我们知道,可升级合约架构中,代理合约存储了数据,逻辑合约中只是执行逻辑而已,执行过程中涉及到的内存变量会反向修改代理合约中的数据。我们来看看 Audius 的合约是怎么写的:
contract AudiusAdminUpgradeabilityProxy is UpgradeabilityProxy {
address private proxyAdmin;
string private constant ERROR_ONLY_ADMIN = (
"AudiusAdminUpgradeabilityProxy: Caller must be current proxy admin"
);
}
在代理合约中,slot 0 的位置是 proxyAdmin,这与我们之前讲过的合约升级不同。我们前面说过为了避免内存槽位冲突,EIP-1967 标准应运而生。它将 implementation 和 admin 这两个字段放在了两个特殊的插槽中:
# bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
# bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
这样做的目的就是为了最大限度保证这两个保留数据不会和逻辑合约中的数据槽位冲突,而 Audius 这种写法正是造成这次 bug 的原因。
我们再来看看逻辑合约的内容:
contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
*/
bool private initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private initializing;
/**
* @dev Modifier to use in the initializer function of a contract.
*/
modifier initializer() {
require(
initializing || isConstructor() || !initialized,
"Contract instance has already been initialized"
);
bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}
_;
if (isTopLevelCall) {
initializing = false;
}
}
///....
// Reserved storage space to allow for layout changes in the future.
uint256[50] private ______gap;
}
contract InitializableV2 is Initializable {
/// ....
}
contract Governance is InitializableV2 {
/// ....
}
按照合约继承的内存分布规则,Initializable 合约中的 initialized 和 initializing 这两个变量分别位于逻辑合约 Governance 的 slot 0 和 slot 1 中。
看到这里,大家是不是已经发现了问题。如果按照这种写法,那么代理合约和逻辑合约的内存槽位不是冲突了吗?没错,但还有一点要注意的是,由于 initialized 和 initializing 都是 bool 类型变量,因此他们各自都只占据一字节(注意,是 1 byte,不是 1 bit),所以说它们俩实际上是被打包放在了 slot 0 中。也就是说,slot 0 的结构是:

上图是逻辑合约的 slot 0 内存分布。由于与代理合约的 ProxyAdmin 冲突,且 ProxyAdmin 的值为:
0x4DEcA517D6817B6510798b7328F2314d3003AbAC
因此,对应的 slot 0 槽位图示为:

这说明 initialized 和 initializing 这两个变量的值使用了 ProxyAdmin 实际值的最后两个字节!而恰好最后两个字节(0xAB, 0xAC)都是非零值,这也就造成在实际可升级合约的数据读取中,initialized 和 initializing 的值总是 true。而这个巧合其实也取决于 ProxyAdmin 的最后两个字节是什么,如果它的地址最后两字节都是零: 0x4DEcA517D6817B6510798b7328F2314d30030000,那么 initialized 和 initializing 便都是 false 了。
冲突原因已经找到了,我们来看看这个冲突会造成什么。再次看看逻辑合约:
contract Initializable {
///....
modifier initializer() {
require(
initializing || isConstructor() || !initialized,
"Contract instance has already been initialized"
);
bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}
_;
if (isTopLevelCall) {
initializing = false;
}
}
///....
// Reserved storage space to allow for layout changes in the future.
uint256[50] private ______gap;
}
对于 initializer 修饰符,由于 initializing 为 true,因此可以通过 require 校验。而下面的 isTopLevelCall 会被赋值为 false,造成 if 语句无法执行,那么 initializing 将永远为 true,也就是说 initializer 已经起不到限制作用了。
黑客就是利用了这个 bug,从而可以调用各种被 initializer 修饰的方法。这些方法中包含一些特权方法,本来只能被管理员调用一次,这下被黑客调用,损失惨重。
解决这个 bug 的中心思想就是去除代理合约和逻辑合约的内存冲突,我们来看看官方的新版逻辑合约:
contract Initializable {
address private proxyAdmin;
uint256 private filler1;
uint256 private filler2;
/**
* @dev Indicates that the contract has been initialized.
*/
bool private initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private initializing;
/**
* @dev Modifier to use in the initializer function of a contract.
*/
modifier initializer() {
require(msg.sender == proxyAdmin, "Only proxy admin can initialize");
require(
initializing || isConstructor() || !initialized,
"Contract instance has already been initialized"
);
bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}
_;
if (isTopLevelCall) {
initializing = false;
}
}
/// @dev Returns true if and only if the function is running in the constructor
function isConstructor() private view returns (bool) {
// extcodesize checks the size of the code stored in an address, and
// address returns the current address. Since the code is still not
// deployed when running a constructor, any checks on its code size will
// yield zero, making it an effective way to detect if a contract is
// under construction or not.
address self = address(this);
uint256 cs;
assembly {
cs := extcodesize(self)
}
return cs == 0;
}
// Reserved storage space to allow for layout changes in the future.
uint256[47] private ______gap;
}
将 initialized 和 initializing 变量后移,预留出空间解决内存冲突
initializer 修饰符中同时添加只能 proxyAdmin 调用的限制,双重保险
由于前面添加了三个变量,因此最后的 ______gap 预留位置要减少,由之前的 50 个减少为 47 个,这样做是为了兼容之前的数据。避免更新后老数据又冲突了。
一个内存插槽冲突引发的血案,警示我们在编写可升级合约时一定要注意这方面问题。
欢迎和我交流
我们今天来研究一下前段时间 Audius 项目被黑的原因。这部分涉及到了内存槽位和合约升级方面的内容,如果有朋友不了解这一块,可以看看我之前写的这个系列。看完之后再来看这篇文章就比较容易理解了。
我们先看一下 Audius 的合约架构,它采用了可升级合约的架构:

我们知道,可升级合约架构中,代理合约存储了数据,逻辑合约中只是执行逻辑而已,执行过程中涉及到的内存变量会反向修改代理合约中的数据。我们来看看 Audius 的合约是怎么写的:
contract AudiusAdminUpgradeabilityProxy is UpgradeabilityProxy {
address private proxyAdmin;
string private constant ERROR_ONLY_ADMIN = (
"AudiusAdminUpgradeabilityProxy: Caller must be current proxy admin"
);
}
在代理合约中,slot 0 的位置是 proxyAdmin,这与我们之前讲过的合约升级不同。我们前面说过为了避免内存槽位冲突,EIP-1967 标准应运而生。它将 implementation 和 admin 这两个字段放在了两个特殊的插槽中:
# bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
# bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
这样做的目的就是为了最大限度保证这两个保留数据不会和逻辑合约中的数据槽位冲突,而 Audius 这种写法正是造成这次 bug 的原因。
我们再来看看逻辑合约的内容:
contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
*/
bool private initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private initializing;
/**
* @dev Modifier to use in the initializer function of a contract.
*/
modifier initializer() {
require(
initializing || isConstructor() || !initialized,
"Contract instance has already been initialized"
);
bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}
_;
if (isTopLevelCall) {
initializing = false;
}
}
///....
// Reserved storage space to allow for layout changes in the future.
uint256[50] private ______gap;
}
contract InitializableV2 is Initializable {
/// ....
}
contract Governance is InitializableV2 {
/// ....
}
按照合约继承的内存分布规则,Initializable 合约中的 initialized 和 initializing 这两个变量分别位于逻辑合约 Governance 的 slot 0 和 slot 1 中。
看到这里,大家是不是已经发现了问题。如果按照这种写法,那么代理合约和逻辑合约的内存槽位不是冲突了吗?没错,但还有一点要注意的是,由于 initialized 和 initializing 都是 bool 类型变量,因此他们各自都只占据一字节(注意,是 1 byte,不是 1 bit),所以说它们俩实际上是被打包放在了 slot 0 中。也就是说,slot 0 的结构是:

上图是逻辑合约的 slot 0 内存分布。由于与代理合约的 ProxyAdmin 冲突,且 ProxyAdmin 的值为:
0x4DEcA517D6817B6510798b7328F2314d3003AbAC
因此,对应的 slot 0 槽位图示为:

这说明 initialized 和 initializing 这两个变量的值使用了 ProxyAdmin 实际值的最后两个字节!而恰好最后两个字节(0xAB, 0xAC)都是非零值,这也就造成在实际可升级合约的数据读取中,initialized 和 initializing 的值总是 true。而这个巧合其实也取决于 ProxyAdmin 的最后两个字节是什么,如果它的地址最后两字节都是零: 0x4DEcA517D6817B6510798b7328F2314d30030000,那么 initialized 和 initializing 便都是 false 了。
冲突原因已经找到了,我们来看看这个冲突会造成什么。再次看看逻辑合约:
contract Initializable {
///....
modifier initializer() {
require(
initializing || isConstructor() || !initialized,
"Contract instance has already been initialized"
);
bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}
_;
if (isTopLevelCall) {
initializing = false;
}
}
///....
// Reserved storage space to allow for layout changes in the future.
uint256[50] private ______gap;
}
对于 initializer 修饰符,由于 initializing 为 true,因此可以通过 require 校验。而下面的 isTopLevelCall 会被赋值为 false,造成 if 语句无法执行,那么 initializing 将永远为 true,也就是说 initializer 已经起不到限制作用了。
黑客就是利用了这个 bug,从而可以调用各种被 initializer 修饰的方法。这些方法中包含一些特权方法,本来只能被管理员调用一次,这下被黑客调用,损失惨重。
解决这个 bug 的中心思想就是去除代理合约和逻辑合约的内存冲突,我们来看看官方的新版逻辑合约:
contract Initializable {
address private proxyAdmin;
uint256 private filler1;
uint256 private filler2;
/**
* @dev Indicates that the contract has been initialized.
*/
bool private initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private initializing;
/**
* @dev Modifier to use in the initializer function of a contract.
*/
modifier initializer() {
require(msg.sender == proxyAdmin, "Only proxy admin can initialize");
require(
initializing || isConstructor() || !initialized,
"Contract instance has already been initialized"
);
bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}
_;
if (isTopLevelCall) {
initializing = false;
}
}
/// @dev Returns true if and only if the function is running in the constructor
function isConstructor() private view returns (bool) {
// extcodesize checks the size of the code stored in an address, and
// address returns the current address. Since the code is still not
// deployed when running a constructor, any checks on its code size will
// yield zero, making it an effective way to detect if a contract is
// under construction or not.
address self = address(this);
uint256 cs;
assembly {
cs := extcodesize(self)
}
return cs == 0;
}
// Reserved storage space to allow for layout changes in the future.
uint256[47] private ______gap;
}
将 initialized 和 initializing 变量后移,预留出空间解决内存冲突
initializer 修饰符中同时添加只能 proxyAdmin 调用的限制,双重保险
由于前面添加了三个变量,因此最后的 ______gap 预留位置要减少,由之前的 50 个减少为 47 个,这样做是为了兼容之前的数据。避免更新后老数据又冲突了。
一个内存插槽冲突引发的血案,警示我们在编写可升级合约时一定要注意这方面问题。
欢迎和我交流
No activity yet