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
PaymentSplitter 是 OpenZeppelin 合约库中的一个合约,简单来说目的就是为了分赃 :)
认真来说,就是在团队协作中,将利润分配这一步骤写在合约中,给每个团队成员提前定好分成,然后将其写在合约中,这样就会避免某一个成员贪污其他成员的币的情况发生。
画个图简单描述下:

// 所有成员的份额总和
uint256 private _totalShares;
// 已经分发的 ETH 总量
uint256 private _totalReleased;
// 每个成员对应其份额
mapping(address => uint256) private _shares;
// 每个成员对应其已经领取的数量
mapping(address => uint256) private _released;
// 成员列表
address[] private _payees;
// 已经分发的 ERC20 token 总量
mapping(IERC20 => uint256) private _erc20TotalReleased;
// ERC20 token -> 成员地址 -> 已经分发的数量
mapping(IERC20 => mapping(address => uint256)) private _erc20Released;
从数据结构可以看出,该合约同时支持 ETH 和 ERC20 币种的分发。
// 在构造方法中初始化成员地址与份额,这两个信息只能在构造方法中添加,不能后面再次添加
constructor(address[] memory payees, uint256[] memory shares_) payable {
require(payees.length == shares_.length, "PaymentSplitter: payees and shares length mismatch");
require(payees.length > 0, "PaymentSplitter: no payees");
for (uint256 i = 0; i < payees.length; i++) {
_addPayee(payees[i], shares_[i]);
}
}
function _addPayee(address account, uint256 shares_) private {
// 不能是0地址
require(account != address(0), "PaymentSplitter: account is the zero address");
// 份额不能是0
require(shares_ > 0, "PaymentSplitter: shares are 0");
// 不能重复
require(_shares[account] == 0, "PaymentSplitter: account already has shares");
_payees.push(account);
_shares[account] = shares_;
_totalShares = _totalShares + shares_;
emit PayeeAdded(account, shares_);
}
添加成员时,并没有要求所有成员的份额总和是 100,但是我个人还是推荐使用总和 100 的数据比较好,这样计算方便一点。
receive() external payable virtual {
emit PaymentReceived(_msgSender(), msg.value);
}
当合约接收到 ETH 的时候,会发送 PaymentReceived 事件。注意,这里的事件发送并不是可靠的,因为有一些情况,比如合约 selfdestruct 的时候,如果要给合约发送 ETH,也是不会触发这个事件的。
不过这里并不影响分配的逻辑,只是说不要过度依赖于事件。
// 参数是成员的地址
function release(address payable account) public virtual {
// 校验成员地址有效
require(_shares[account] > 0, "PaymentSplitter: account has no shares");
// 合约一共接收的 ETH 的数量
uint256 totalReceived = address(this).balance + totalReleased();
// 计算该给成员地址打多少币
uint256 payment = _pendingPayment(account, totalReceived, released(account));
require(payment != 0, "PaymentSplitter: account is not due payment");
// 更新该地址的分成数量
_released[account] += payment;
// 更新总分发数量
_totalReleased += payment;
// 打币
Address.sendValue(account, payment);
emit PaymentReleased(account, payment);
}
function _pendingPayment(
address account,
uint256 totalReceived,
uint256 alreadyReleased
) private view returns (uint256) {
return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
}
_pendingPayment 中的计算逻辑是,用合约接收到的总量以及该地址的份额,计算出该地址应该接收的数量,然后再减去该地址已经接收到的数量,就是这次应该接收的数量。
这里需要减去 alreadyReleased 的原因是,合约可能会不止一次接收到外部打入的币种,如果该地址上次已经领取过币,那么需要去除上次的数量,才是这次的增量。
ERC20 的 release 方法与上面 ETH 的大同小异,这里就不再赘述了。
PaymentSplitter 合约本身逻辑比较简单,是一个公平的分配方法。如果团队的利润分配地址是由一个 EOA 管理,那么就无法避免管理该地址的成员作恶。
该合约本身试用场景也挺多,比如在最近很火的 NFT 方向。项目方要将售卖的 ETH 从合约中取出来,那么就可以在售卖合约中将接收人地址设置为 PaymentSplitter 合约地址,然后由团队成员自行领取利润。
PaymentSplitter 是 OpenZeppelin 合约库中的一个合约,简单来说目的就是为了分赃 :)
认真来说,就是在团队协作中,将利润分配这一步骤写在合约中,给每个团队成员提前定好分成,然后将其写在合约中,这样就会避免某一个成员贪污其他成员的币的情况发生。
画个图简单描述下:

// 所有成员的份额总和
uint256 private _totalShares;
// 已经分发的 ETH 总量
uint256 private _totalReleased;
// 每个成员对应其份额
mapping(address => uint256) private _shares;
// 每个成员对应其已经领取的数量
mapping(address => uint256) private _released;
// 成员列表
address[] private _payees;
// 已经分发的 ERC20 token 总量
mapping(IERC20 => uint256) private _erc20TotalReleased;
// ERC20 token -> 成员地址 -> 已经分发的数量
mapping(IERC20 => mapping(address => uint256)) private _erc20Released;
从数据结构可以看出,该合约同时支持 ETH 和 ERC20 币种的分发。
// 在构造方法中初始化成员地址与份额,这两个信息只能在构造方法中添加,不能后面再次添加
constructor(address[] memory payees, uint256[] memory shares_) payable {
require(payees.length == shares_.length, "PaymentSplitter: payees and shares length mismatch");
require(payees.length > 0, "PaymentSplitter: no payees");
for (uint256 i = 0; i < payees.length; i++) {
_addPayee(payees[i], shares_[i]);
}
}
function _addPayee(address account, uint256 shares_) private {
// 不能是0地址
require(account != address(0), "PaymentSplitter: account is the zero address");
// 份额不能是0
require(shares_ > 0, "PaymentSplitter: shares are 0");
// 不能重复
require(_shares[account] == 0, "PaymentSplitter: account already has shares");
_payees.push(account);
_shares[account] = shares_;
_totalShares = _totalShares + shares_;
emit PayeeAdded(account, shares_);
}
添加成员时,并没有要求所有成员的份额总和是 100,但是我个人还是推荐使用总和 100 的数据比较好,这样计算方便一点。
receive() external payable virtual {
emit PaymentReceived(_msgSender(), msg.value);
}
当合约接收到 ETH 的时候,会发送 PaymentReceived 事件。注意,这里的事件发送并不是可靠的,因为有一些情况,比如合约 selfdestruct 的时候,如果要给合约发送 ETH,也是不会触发这个事件的。
不过这里并不影响分配的逻辑,只是说不要过度依赖于事件。
// 参数是成员的地址
function release(address payable account) public virtual {
// 校验成员地址有效
require(_shares[account] > 0, "PaymentSplitter: account has no shares");
// 合约一共接收的 ETH 的数量
uint256 totalReceived = address(this).balance + totalReleased();
// 计算该给成员地址打多少币
uint256 payment = _pendingPayment(account, totalReceived, released(account));
require(payment != 0, "PaymentSplitter: account is not due payment");
// 更新该地址的分成数量
_released[account] += payment;
// 更新总分发数量
_totalReleased += payment;
// 打币
Address.sendValue(account, payment);
emit PaymentReleased(account, payment);
}
function _pendingPayment(
address account,
uint256 totalReceived,
uint256 alreadyReleased
) private view returns (uint256) {
return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
}
_pendingPayment 中的计算逻辑是,用合约接收到的总量以及该地址的份额,计算出该地址应该接收的数量,然后再减去该地址已经接收到的数量,就是这次应该接收的数量。
这里需要减去 alreadyReleased 的原因是,合约可能会不止一次接收到外部打入的币种,如果该地址上次已经领取过币,那么需要去除上次的数量,才是这次的增量。
ERC20 的 release 方法与上面 ETH 的大同小异,这里就不再赘述了。
PaymentSplitter 合约本身逻辑比较简单,是一个公平的分配方法。如果团队的利润分配地址是由一个 EOA 管理,那么就无法避免管理该地址的成员作恶。
该合约本身试用场景也挺多,比如在最近很火的 NFT 方向。项目方要将售卖的 ETH 从合约中取出来,那么就可以在售卖合约中将接收人地址设置为 PaymentSplitter 合约地址,然后由团队成员自行领取利润。
No activity yet