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 还有一个优点,相...
Share Dialog
Share Dialog
Smart Contract Developer

Subscribe to xyyme.eth

Subscribe to xyyme.eth
Solidity 中的 selfdestruct 函数是一个内置函数,需要接收一个参数。我们先来看看文档是怎么描述 selfdestruct 的:
The only way to remove code from the blockchain is when a contract at that address performs the
selfdestructoperation. The remaining Ether stored at that address is sent to a designated target and then the storage and code is removed from the state. Removing the contract in theory sounds like a good idea, but it is potentially dangerous, as if someone sends Ether to removed contracts, the Ether is forever lost.
selfdestruct 可以清除合约的代码,且是唯一清除代码的方法。当调用 selfdestruct 时,合约的所有 ETH 余额会发送给 selfdestruct 的参数地址,然后合约的代码和内存都被移除。销毁之后,如果给这个合约地址发送 ETH,那么就永久销毁了。
我们来写段代码测试一下:
pragma solidity 0.8.13;
contract TestDestruct {
uint public age;
string public name;
// 构造标记为payable,我们直接在部署的时候就打入ETH
constructor(uint _age, string memory _name) payable {
age = _age;
name = _name;
}
function destruct() external {
selfdestruct(payable(msg.sender));
}
function balance() external view returns (uint) {
return address(this).balance;
}
}
Solidity 中的 selfdestruct 函数是一个内置函数,需要接收一个参数。我们先来看看文档是怎么描述 selfdestruct 的:
The only way to remove code from the blockchain is when a contract at that address performs the
selfdestructoperation. The remaining Ether stored at that address is sent to a designated target and then the storage and code is removed from the state. Removing the contract in theory sounds like a good idea, but it is potentially dangerous, as if someone sends Ether to removed contracts, the Ether is forever lost.
selfdestruct 可以清除合约的代码,且是唯一清除代码的方法。当调用 selfdestruct 时,合约的所有 ETH 余额会发送给 selfdestruct 的参数地址,然后合约的代码和内存都被移除。销毁之后,如果给这个合约地址发送 ETH,那么就永久销毁了。
我们来写段代码测试一下:
pragma solidity 0.8.13;
contract TestDestruct {
uint public age;
string public name;
// 构造标记为payable,我们直接在部署的时候就打入ETH
constructor(uint _age, string memory _name) payable {
age = _age;
name = _name;
}
function destruct() external {
selfdestruct(payable(msg.sender));
}
function balance() external view returns (uint) {
return address(this).balance;
}
}
部署合约,参数为(88,Tom),同时传入 1 ETH,查看状态变量与合约余额分别为:
age: 88
name: Tom
balance: 1 ETH
没有问题。同时注意到,Etherscan中的合约界面为(没有上传代码):

那么我们再调用一下 destruct 函数,此时再查看状态变量与合约余额分别为:
age: 0
name: (为空)
balance: 0
同时再看 Etherscan 的这笔交易:

很明显能看出,销毁时,合约余额转给了 selfdestruct 的参数地址,也就是 msg.sender。
此时,我们再看 Etherscan 的合约页面:

可以看到合约代码已经被销毁了。
其实,当合约调用 selfdestruct 被销毁时,由于合约本身的代码和数据都已经销毁,因此合约地址此时就已经变成了一个 EOA 地址。那么如果向这个地址发送 ETH,除非能够找到这个地址所对应的私钥,那么这些 ETH 便是永久销毁了。但是这里有一个特例,就是与 create2 相结合,就可以碰撞出神奇的火花。
之前的文章我们介绍过 create2 操作码,对于同一个合约,如果参数与盐都相同,那么工厂合约部署的时候,得到的一定是相同的地址。但是由于第一次已经部署了,所以第二次部署是会失败的。但是如果第一次部署的合约中可以 selfdestruct,那么按照我们前面的结论,selfdestruct 之后这个地址已经变成 EOA 地址了,那么能不能再在相同地址上部署一下呢。来试试:
pragma solidity 0.8.13;
import "@openzeppelin/contracts/utils/Create2.sol";
contract Create2WithDestruct {
event Deployed(address addr);
function deploy() public {
// 部署合约,参数传88
address addr = Create2.deploy(
0,
keccak256("Here is salt"),
abi.encodePacked(type(DemoOne).creationCode, abi.encode(88))
);
// 记录地址
emit Deployed(addr);
}
}
contract DemoOne {
uint public age;
constructor(uint _age) {
age = _age;
}
function destruct() external {
selfdestruct(payable(msg.sender));
}
}
这段代码中,我们在 Create2WithDestruct 合约中,使用 create2 操作码部署 DemoOne 合约,参数传 88。部署后,事件记录为:

即通过 create2 部署得到的 DemoOne 地址为:
0x10CEBC6a50B65320578bBfA6b96503c75f745424
通过这个地址来读取合约数据:

数据正确。我们尝试通过 create2 再次部署 DemoOne 合约:

交易失败,这是因为这个地址上已经有数据了,因此不能再次部署,这与我们的预期一致。
现在我们尝试调用 DemoOne 合约的 selfdestruct 函数。调用成功,同时,age已经变成0:

按照我们之前的结论,此时之前的合约地址已经变成了 EOA,而在任何一个 EOA 地址上都是可以部署合约的,那么我们再来试试调用 deploy 部署一次合约:

部署成功,同时得到的地址为:
0x10CEBC6a50B65320578bBfA6b96503c75f745424
这与我们之前第一次部署的时候得到的地址相同。也就是说,我们可以通过 selfdestruct 与 create2 相结合在同一个地址上多次部署合约。这样,我们前面说的当合约销毁后,向合约发送 ETH 将被永久销毁的说法在这里就不准确了。如果合约是由 create2 部署的,那么在合约被销毁之后,仍然可以通过再次部署合约得到这个地址的控制权。
前面我们说到,selfdestruct 中的参数地址会接收到合约的余额,而这个行为是强制性的,也就是说,即使这个参数地址是个合约,并且合约中通过 receive 或者 fallback 限制了接收 ETH,而 selfdestruct 中的发送行为仍然是可以发送给这个地址的。
来看段代码:
pragma solidity 0.8.13;
contract DestructTransfer {
constructor() payable {
}
function destruct(address recipient) external {
selfdestruct(payable(recipient));
}
}
contract DemoOne {
constructor() {
}
fallback() external payable {
revert("You can not send ether");
}
receive() external payable {
revert("You can not send ether");
}
function balance() external view returns (uint) {
return address(this).balance;
}
}
DemoOne 合约通过 fallback 和 receive 的限制禁止接收 ETH。我们部署并转一点 ETH 试试:

交易失败,报错原因也是我们设定的信息。现在我们通过 DestructTransfer 来强行转入 ETH。部署合约的同时打入 1 ETH,并且调用 destruct 函数,参数为 DemoOne 的地址,交易成功后查看 DemoOne 的余额:

可以看到即使合约中有接收限制,我们仍然将 ETH 强行打入了合约中。
实际上,一共有两种方法来强行向一个地址转入 ETH:
ETH 挖矿时区块头中的 coinbase 字段设置为接收地址
selfdestruct 的参数地址设置为接收地址
关于强行转入 ETH 这个话题,还有一些黑客游戏就是利用这个原理,比如这道 Ethernaut 中的题目。
selfdestruct 是可以销毁合约的代码和数据,接收一个参数,销毁时会将合约中剩余的 ETH 全部打入改地址,并且是强制性的。selfdestruct 与 create2 结合可以实现在同一个地址上多次部署合约。
部署合约,参数为(88,Tom),同时传入 1 ETH,查看状态变量与合约余额分别为:
age: 88
name: Tom
balance: 1 ETH
没有问题。同时注意到,Etherscan中的合约界面为(没有上传代码):

那么我们再调用一下 destruct 函数,此时再查看状态变量与合约余额分别为:
age: 0
name: (为空)
balance: 0
同时再看 Etherscan 的这笔交易:

很明显能看出,销毁时,合约余额转给了 selfdestruct 的参数地址,也就是 msg.sender。
此时,我们再看 Etherscan 的合约页面:

可以看到合约代码已经被销毁了。
其实,当合约调用 selfdestruct 被销毁时,由于合约本身的代码和数据都已经销毁,因此合约地址此时就已经变成了一个 EOA 地址。那么如果向这个地址发送 ETH,除非能够找到这个地址所对应的私钥,那么这些 ETH 便是永久销毁了。但是这里有一个特例,就是与 create2 相结合,就可以碰撞出神奇的火花。
之前的文章我们介绍过 create2 操作码,对于同一个合约,如果参数与盐都相同,那么工厂合约部署的时候,得到的一定是相同的地址。但是由于第一次已经部署了,所以第二次部署是会失败的。但是如果第一次部署的合约中可以 selfdestruct,那么按照我们前面的结论,selfdestruct 之后这个地址已经变成 EOA 地址了,那么能不能再在相同地址上部署一下呢。来试试:
pragma solidity 0.8.13;
import "@openzeppelin/contracts/utils/Create2.sol";
contract Create2WithDestruct {
event Deployed(address addr);
function deploy() public {
// 部署合约,参数传88
address addr = Create2.deploy(
0,
keccak256("Here is salt"),
abi.encodePacked(type(DemoOne).creationCode, abi.encode(88))
);
// 记录地址
emit Deployed(addr);
}
}
contract DemoOne {
uint public age;
constructor(uint _age) {
age = _age;
}
function destruct() external {
selfdestruct(payable(msg.sender));
}
}
这段代码中,我们在 Create2WithDestruct 合约中,使用 create2 操作码部署 DemoOne 合约,参数传 88。部署后,事件记录为:

即通过 create2 部署得到的 DemoOne 地址为:
0x10CEBC6a50B65320578bBfA6b96503c75f745424
通过这个地址来读取合约数据:

数据正确。我们尝试通过 create2 再次部署 DemoOne 合约:

交易失败,这是因为这个地址上已经有数据了,因此不能再次部署,这与我们的预期一致。
现在我们尝试调用 DemoOne 合约的 selfdestruct 函数。调用成功,同时,age已经变成0:

按照我们之前的结论,此时之前的合约地址已经变成了 EOA,而在任何一个 EOA 地址上都是可以部署合约的,那么我们再来试试调用 deploy 部署一次合约:

部署成功,同时得到的地址为:
0x10CEBC6a50B65320578bBfA6b96503c75f745424
这与我们之前第一次部署的时候得到的地址相同。也就是说,我们可以通过 selfdestruct 与 create2 相结合在同一个地址上多次部署合约。这样,我们前面说的当合约销毁后,向合约发送 ETH 将被永久销毁的说法在这里就不准确了。如果合约是由 create2 部署的,那么在合约被销毁之后,仍然可以通过再次部署合约得到这个地址的控制权。
前面我们说到,selfdestruct 中的参数地址会接收到合约的余额,而这个行为是强制性的,也就是说,即使这个参数地址是个合约,并且合约中通过 receive 或者 fallback 限制了接收 ETH,而 selfdestruct 中的发送行为仍然是可以发送给这个地址的。
来看段代码:
pragma solidity 0.8.13;
contract DestructTransfer {
constructor() payable {
}
function destruct(address recipient) external {
selfdestruct(payable(recipient));
}
}
contract DemoOne {
constructor() {
}
fallback() external payable {
revert("You can not send ether");
}
receive() external payable {
revert("You can not send ether");
}
function balance() external view returns (uint) {
return address(this).balance;
}
}
DemoOne 合约通过 fallback 和 receive 的限制禁止接收 ETH。我们部署并转一点 ETH 试试:

交易失败,报错原因也是我们设定的信息。现在我们通过 DestructTransfer 来强行转入 ETH。部署合约的同时打入 1 ETH,并且调用 destruct 函数,参数为 DemoOne 的地址,交易成功后查看 DemoOne 的余额:

可以看到即使合约中有接收限制,我们仍然将 ETH 强行打入了合约中。
实际上,一共有两种方法来强行向一个地址转入 ETH:
ETH 挖矿时区块头中的 coinbase 字段设置为接收地址
selfdestruct 的参数地址设置为接收地址
关于强行转入 ETH 这个话题,还有一些黑客游戏就是利用这个原理,比如这道 Ethernaut 中的题目。
selfdestruct 是可以销毁合约的代码和数据,接收一个参数,销毁时会将合约中剩余的 ETH 全部打入改地址,并且是强制性的。selfdestruct 与 create2 结合可以实现在同一个地址上多次部署合约。
<100 subscribers
<100 subscribers
No activity yet