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
这两天 XEN 特别火,看了看代码,相对比较简单。这篇文章就来结合文档来解读一下合约代码,仅做学习交流用。对于玩法还不熟悉的朋友可以先看看我昨天发的推文。
整个玩法分成两部分,我这里将其区别为:
时间挖矿(claim to mint),也就是在参与时指定时间,时间到期后即可领取对应的 XEN,唯一付出的成本就是 gas 费用和等待的时间
stake 挖矿,通过质押 XEN 来挖矿
先来看第一部分,时间挖矿。用户通过调用 claimRank(uint256 term) 来参与,term 代表用户想要挖矿的天数,在这个时间到期之后才能领取 XEN 奖励。
function claimRank(uint256 term) external {
// SECONDS_IN_DAY = 3_600 * 24;
// 将天数转化为秒数
uint256 termSec = term * SECONDS_IN_DAY;
// 最短挖一天
// MIN_TERM = 1 * SECONDS_IN_DAY - 1;
require(termSec > MIN_TERM, "CRank: Term less than min");
// 最长可参与的天数需要根据参与人数实时计算
require(termSec < _calculateMaxTerm() + 1, "CRank: Term more than current max term");
require(userMints[_msgSender()].rank == 0, "CRank: Mint already in progress");
// create and store new MintInfo
MintInfo memory mintInfo = MintInfo({
user: _msgSender(),
term: term,
maturityTs: block.timestamp + termSec,
rank: globalRank,
amplifier: _calculateRewardAmplifier(),
eaaRate: _calculateEAARate()
});
userMints[_msgSender()] = mintInfo;
activeMinters++;
emit RankClaimed(_msgSender(), term, globalRank++);
}
其中全局变量 globalRank 代表的是全局参与的总人数,只增不减。activeMinters 代表正在参与挖矿的人数,当用户参与时间挖矿时增加 1,到期领取奖励后减少 1。userMints 代表用户的挖矿参数。我们看到,这里最短需要参与一天,最多参与的天数是通过 _calculateMaxTerm() 实时计算出来的。
function _calculateMaxTerm() private view returns (uint256) {
// TERM_AMPLIFIER_THRESHOLD = 5_000;
// TERM_AMPLIFIER = 15;
// MAX_TERM_START = 100 * SECONDS_IN_DAY;
// MAX_TERM_END = 1_000 * SECONDS_IN_DAY;
if (globalRank > TERM_AMPLIFIER_THRESHOLD) {
// 如果参与的人数大于 5000
// globalRank.fromUInt(),先将 globalRank 转换为 int128 类型
// 然后再对其进行对数 log_2() 计算
// 即 log_2_globalRank * 15
uint256 delta = globalRank.fromUInt().log_2().mul(TERM_AMPLIFIER.fromUInt()).toUInt();
// newMax = 100 天 + delta 天
uint256 newMax = MAX_TERM_START + delta * SECONDS_IN_DAY;
// 取 1000 天 和 newMax 的最小值,也就是最多只能挖 1000 天
return Math.min(newMax, MAX_TERM_END);
}
// 如果参与的人数没有超过 5000,则最大只能挖 100 天
return MAX_TERM_START;
}
首先如果全部参与人数没有超过 5000,那么最多只能挖 100 天。如果达到了 5000,通过对参与人数进行对数运算,计算出对应的最大天数。对应于文档中的:

代码中的 fromUInt() 和 log_2() 都来自于 ABDKMath64x64 库(代码)。其中 fromUInt() 的代码:
function fromUInt (uint256 x) internal pure returns (int128) {
unchecked {
require (x <= 0x7FFFFFFFFFFFFFFF);
return int128 (int256 (x << 64));
}
}
入参 x 有限制,这个最大值转换为 10 进制是 9223372036854775807,全部参与人数不可能超过这个数,所有可以安全使用。
在构造的挖矿系数 mintInfo 中,_calculateRewardAmplifier() 和 _calculateEAARate() 也是实时计算的。
function _calculateRewardAmplifier() private view returns (uint256) {
// genesisTs 是合约部署时间,也就是初始时间
// 这里是计算出距离开始时间过去了多少天
uint256 amplifierDecrease = (block.timestamp - genesisTs) / SECONDS_IN_DAY;
// REWARD_AMPLIFIER_START = 3000
// REWARD_AMPLIFIER_END = 1;
if (amplifierDecrease < REWARD_AMPLIFIER_START) {
// 如果是在 3000 天以内
return Math.max(REWARD_AMPLIFIER_START - amplifierDecrease, REWARD_AMPLIFIER_END);
} else {
// 如果超过 3000 天,则使用 1
return REWARD_AMPLIFIER_END;
}
}
可以看到,越早参与,可以获得到的 AMP 就越多,最开始一天是 3000,每过一天会减少 1,最终超过 3000 天就会恒定为 1。
对应于文档中 AMP 的计算方式:

function _calculateEAARate() private view returns (uint256) {
// EAA_PM_STEP = 1
// EAA_RANK_STEP = 100000
uint256 decrease = (EAA_PM_STEP * globalRank) / EAA_RANK_STEP;
// EAA_PM_START = 100
// 也就是说,如果参与人数大于 1000 万,返回 0
if (decrease > EAA_PM_START) return 0;
// 否则返回 100 - 人数 / 100000
return EAA_PM_START - decrease;
}
从 100 开始,每当有 十万人 参与时,下降 1,最终若达到 一千万人 参与,则恒定为 0。同样也是越早参与越好。对应于文档:

由于 Solidity 中没有小数,因此在代码中将其放大了 1000 倍,后面在 getGrossReward 方法中会再缩小 1000 倍。
到这里,我们可以看到,在用户参与时间挖矿时,已经确定的数据有
用户在全局中的位置(rank)
参与时长(term),由用户在参与时指定
AMP,越早参与越大
EAA,越早参与越大
接下来我们来看用户领取奖励时的方法 claimMintReward():
function claimMintReward() external {
// 获取用户的挖矿系数
MintInfo memory mintInfo = userMints[_msgSender()];
require(mintInfo.rank > 0, "CRank: No mint exists");
// 要求满足时间限制
require(block.timestamp > mintInfo.maturityTs, "CRank: Mint maturity not reached");
// calculate reward and mint tokens
uint256 rewardAmount = _calculateMintReward(
mintInfo.rank,
mintInfo.term,
mintInfo.maturityTs,
mintInfo.amplifier,
mintInfo.eaaRate
) * 1 ether;
_mint(_msgSender(), rewardAmount);
// 清除掉用户的挖矿数据
_cleanUpUserMint();
emit MintClaimed(_msgSender(), rewardAmount);
}
校验限制后,计算可得奖励数量,然后 _mint 给用户,计算奖励数量的主要计算逻辑在 _calculateMintReward() 中:
function _calculateMintReward(
uint256 cRank,
uint256 term,
uint256 maturityTs,
uint256 amplifier,
uint256 eeaRate
) private view returns (uint256) {
// 计算当前时间与到期时间的差值
uint256 secsLate = block.timestamp - maturityTs;
// 根据时间差值计算需要扣除的数量
uint256 penalty = _penalty(secsLate);
// 计算用户后面还有多少人参与游戏
uint256 rankDelta = Math.max(globalRank - cRank, 2);
uint256 EAA = (1_000 + eeaRate);
uint256 reward = getGrossReward(rankDelta, amplifier, term, EAA);
return (reward * (100 - penalty)) / 100;
}
function getGrossReward(
uint256 rankDelta,
uint256 amplifier,
uint256 term,
uint256 eaa
) public pure returns (uint256) {
// log_2_rankDelta
int128 log128 = rankDelta.fromUInt().log_2();
int128 reward128 = log128.mul(amplifier.fromUInt()).mul(term.fromUInt()).mul(eaa.fromUInt());
return reward128.div(uint256(1_000).fromUInt()).toUInt();
}
这里我们先忽略 penalty 这一块,其他部分的计算正好对应于文档中的:

在计算最终奖励数量的时候,自己参与的位置越靠前,后面的人越多,那么
cRG - cRu
就会越大,同样说明越早参与越好。
我们再来看 penalty 这部分,这块其实就是系统限制用户必须在到期后一定时间内领取走,如果没有领取则会随着时间越来越少,最终归零。
function _penalty(uint256 secsLate) private pure returns (uint256) {
// =MIN(2^(daysLate+3)/window-1,99)
// 计算这个差值有多少天
uint256 daysLate = secsLate / SECONDS_IN_DAY;
// WITHDRAWAL_WINDOW_DAYS = 7
// 如果这个差值在七天及以上,返回 99
if (daysLate > WITHDRAWAL_WINDOW_DAYS - 1) return MAX_PENALTY_PCT;
// 也就是 2 ^ (daysLate + 3) / 7 - 1
uint256 penalty = (uint256(1) << (daysLate + 3)) / WITHDRAWAL_WINDOW_DAYS - 1;
return Math.min(penalty, MAX_PENALTY_PCT);
}
对应于文档中的扣除比例:

文档中显示超过七天就全部不能领取,但是代码中显示最多只会扣除 99%。
到这里,我们就介绍完了时间挖矿的代码部分,接下来我们来看看 stake 挖矿的部分。
这里的 stake 其实比常见的挖矿计算逻辑要简单。常见的挖矿 APY 是根据用户质押数量占比以及参与时间来计算的,属于随挖随走类型的。而这里的 stake 挖矿的 APY 在参与时就已经固定了,且需要在参与时就指定参与时间,在时间到期后才能领取奖励,如果没有到期就领取,只能取回本金,没有任何的奖励。
用户可以在前面时间挖矿到期时调用 claimMintRewardAndStake 同时领取奖励并进行 stake,或者单独调用 stake(uint256 amount, uint256 term) 进行 stake 挖矿:
function stake(uint256 amount, uint256 term) external {
require(balanceOf(_msgSender()) >= amount, "XEN: not enough balance");
// XEN_MIN_STAKE = 0
require(amount > XEN_MIN_STAKE, "XEN: Below min stake");
// 最少 stake 1 天,最多 stake 1000 天
require(term * SECONDS_IN_DAY > MIN_TERM, "XEN: Below min stake term");
require(term * SECONDS_IN_DAY < MAX_TERM_END + 1, "XEN: Above max stake term");
require(userStakes[_msgSender()].amount == 0, "XEN: stake exists");
// burn staked XEN
// 直接 burn,而不是从用户手里转账
_burn(_msgSender(), amount);
// create XEN Stake
_createStake(amount, term);
emit Staked(_msgSender(), amount, term);
}
function _createStake(uint256 amount, uint256 term) private {
userStakes[_msgSender()] = StakeInfo({
term: term,
maturityTs: block.timestamp + term * SECONDS_IN_DAY,
amount: amount,
apy: _calculateAPY()
});
activeStakes++;
totalXenStaked += amount;
}
整体的逻辑也比较简单,参与的时候需要指定时间 term。有一个小细节是在 stake 的时候直接 burn 掉了用户的 token,而不是通过转账的方法,这样可以少一步授权操作。由于合约本身既包含了挖矿操作,同时也是 ERC20,因此可以实现这个逻辑。
接下来我们看看计算 APY 的方法 _calculateAPY():
function _calculateAPY() private view returns (uint256) {
// 这里 genesisTs 是合约部署时间
// (SECONDS_IN_DAY * XEN_APY_DAYS_STEP) -> 86400 * 90,也就是 90 天
// 也就是计算,当前时间距离最开始的合约部署时间有多少个 90 天的差距
uint256 decrease = (block.timestamp - genesisTs) / (SECONDS_IN_DAY * XEN_APY_DAYS_STEP);
// XEN_APY_START = 20
// XEN_APY_END = 2
// 如果 decrease 差值大于 (20 - 2),也就是上面的时间差值大于 18 * 90 = 1620 天
// 则返回 2
if (XEN_APY_START - XEN_APY_END < decrease) return XEN_APY_END;
// 否则返回 20 - decrease
return XEN_APY_START - decrease;
}
基本逻辑也是类似于上面计算 EAA 的方法,一次函数递减,参与的时间越早,相对应的 APY 就越大。初始值为 20,每过 90 天,减少 1。最终在 1620 天后,恒定为 2。对应于文档:

最终在 stake 到期后,可以调用 withdraw() 取出本金和奖励:
function withdraw() external {
StakeInfo memory userStake = userStakes[_msgSender()];
require(userStake.amount > 0, "XEN: no stake exists");
uint256 xenReward = _calculateStakeReward(
userStake.amount,
userStake.term,
userStake.maturityTs,
userStake.apy
);
activeStakes--;
totalXenStaked -= userStake.amount;
// mint staked XEN (+ reward)
_mint(_msgSender(), userStake.amount + xenReward);
emit Withdrawn(_msgSender(), userStake.amount, xenReward);
delete userStakes[_msgSender()];
}
其中计算奖励数量的方法 _calculateStakeReward():
function _calculateStakeReward(
uint256 amount,
uint256 term,
uint256 maturityTs,
uint256 apy
) private view returns (uint256) {
if (block.timestamp > maturityTs) {
// DAYS_IN_YEAR = 365
uint256 rate = (apy * term * 1_000_000) / DAYS_IN_YEAR;
return (amount * rate) / 100_000_000;
}
return 0;
}
对应于文档中的:

对于 stake 挖矿而言,没有领取的限制,奖励数量不会变化。
到这里我们就看完了主要的逻辑代码。这个玩法有意思的地方在于越早参与获得的奖励越多,相当于普通的挖头矿,但是同时也取决于总体的参与人数,如果后面没有人参与,那么也没啥意义。必须是参与的早且后面还有更多人参与的情况下,奖励才会更多。目前时刻总参与人数已经快达到 50 万了,热度确实很高。
同时,前面的时间挖矿和后面的 stake 挖矿也存在博弈关系,如果前面选择的时间越长,获得的奖励就越多,但是来到后面的 stake 挖矿的 APY 就会降低,需要大家自行抉择。
合约本身代码没啥难度,但是整体机制比较有趣,值得花点时间了解。
欢迎和我交流
这两天 XEN 特别火,看了看代码,相对比较简单。这篇文章就来结合文档来解读一下合约代码,仅做学习交流用。对于玩法还不熟悉的朋友可以先看看我昨天发的推文。
整个玩法分成两部分,我这里将其区别为:
时间挖矿(claim to mint),也就是在参与时指定时间,时间到期后即可领取对应的 XEN,唯一付出的成本就是 gas 费用和等待的时间
stake 挖矿,通过质押 XEN 来挖矿
先来看第一部分,时间挖矿。用户通过调用 claimRank(uint256 term) 来参与,term 代表用户想要挖矿的天数,在这个时间到期之后才能领取 XEN 奖励。
function claimRank(uint256 term) external {
// SECONDS_IN_DAY = 3_600 * 24;
// 将天数转化为秒数
uint256 termSec = term * SECONDS_IN_DAY;
// 最短挖一天
// MIN_TERM = 1 * SECONDS_IN_DAY - 1;
require(termSec > MIN_TERM, "CRank: Term less than min");
// 最长可参与的天数需要根据参与人数实时计算
require(termSec < _calculateMaxTerm() + 1, "CRank: Term more than current max term");
require(userMints[_msgSender()].rank == 0, "CRank: Mint already in progress");
// create and store new MintInfo
MintInfo memory mintInfo = MintInfo({
user: _msgSender(),
term: term,
maturityTs: block.timestamp + termSec,
rank: globalRank,
amplifier: _calculateRewardAmplifier(),
eaaRate: _calculateEAARate()
});
userMints[_msgSender()] = mintInfo;
activeMinters++;
emit RankClaimed(_msgSender(), term, globalRank++);
}
其中全局变量 globalRank 代表的是全局参与的总人数,只增不减。activeMinters 代表正在参与挖矿的人数,当用户参与时间挖矿时增加 1,到期领取奖励后减少 1。userMints 代表用户的挖矿参数。我们看到,这里最短需要参与一天,最多参与的天数是通过 _calculateMaxTerm() 实时计算出来的。
function _calculateMaxTerm() private view returns (uint256) {
// TERM_AMPLIFIER_THRESHOLD = 5_000;
// TERM_AMPLIFIER = 15;
// MAX_TERM_START = 100 * SECONDS_IN_DAY;
// MAX_TERM_END = 1_000 * SECONDS_IN_DAY;
if (globalRank > TERM_AMPLIFIER_THRESHOLD) {
// 如果参与的人数大于 5000
// globalRank.fromUInt(),先将 globalRank 转换为 int128 类型
// 然后再对其进行对数 log_2() 计算
// 即 log_2_globalRank * 15
uint256 delta = globalRank.fromUInt().log_2().mul(TERM_AMPLIFIER.fromUInt()).toUInt();
// newMax = 100 天 + delta 天
uint256 newMax = MAX_TERM_START + delta * SECONDS_IN_DAY;
// 取 1000 天 和 newMax 的最小值,也就是最多只能挖 1000 天
return Math.min(newMax, MAX_TERM_END);
}
// 如果参与的人数没有超过 5000,则最大只能挖 100 天
return MAX_TERM_START;
}
首先如果全部参与人数没有超过 5000,那么最多只能挖 100 天。如果达到了 5000,通过对参与人数进行对数运算,计算出对应的最大天数。对应于文档中的:

代码中的 fromUInt() 和 log_2() 都来自于 ABDKMath64x64 库(代码)。其中 fromUInt() 的代码:
function fromUInt (uint256 x) internal pure returns (int128) {
unchecked {
require (x <= 0x7FFFFFFFFFFFFFFF);
return int128 (int256 (x << 64));
}
}
入参 x 有限制,这个最大值转换为 10 进制是 9223372036854775807,全部参与人数不可能超过这个数,所有可以安全使用。
在构造的挖矿系数 mintInfo 中,_calculateRewardAmplifier() 和 _calculateEAARate() 也是实时计算的。
function _calculateRewardAmplifier() private view returns (uint256) {
// genesisTs 是合约部署时间,也就是初始时间
// 这里是计算出距离开始时间过去了多少天
uint256 amplifierDecrease = (block.timestamp - genesisTs) / SECONDS_IN_DAY;
// REWARD_AMPLIFIER_START = 3000
// REWARD_AMPLIFIER_END = 1;
if (amplifierDecrease < REWARD_AMPLIFIER_START) {
// 如果是在 3000 天以内
return Math.max(REWARD_AMPLIFIER_START - amplifierDecrease, REWARD_AMPLIFIER_END);
} else {
// 如果超过 3000 天,则使用 1
return REWARD_AMPLIFIER_END;
}
}
可以看到,越早参与,可以获得到的 AMP 就越多,最开始一天是 3000,每过一天会减少 1,最终超过 3000 天就会恒定为 1。
对应于文档中 AMP 的计算方式:

function _calculateEAARate() private view returns (uint256) {
// EAA_PM_STEP = 1
// EAA_RANK_STEP = 100000
uint256 decrease = (EAA_PM_STEP * globalRank) / EAA_RANK_STEP;
// EAA_PM_START = 100
// 也就是说,如果参与人数大于 1000 万,返回 0
if (decrease > EAA_PM_START) return 0;
// 否则返回 100 - 人数 / 100000
return EAA_PM_START - decrease;
}
从 100 开始,每当有 十万人 参与时,下降 1,最终若达到 一千万人 参与,则恒定为 0。同样也是越早参与越好。对应于文档:

由于 Solidity 中没有小数,因此在代码中将其放大了 1000 倍,后面在 getGrossReward 方法中会再缩小 1000 倍。
到这里,我们可以看到,在用户参与时间挖矿时,已经确定的数据有
用户在全局中的位置(rank)
参与时长(term),由用户在参与时指定
AMP,越早参与越大
EAA,越早参与越大
接下来我们来看用户领取奖励时的方法 claimMintReward():
function claimMintReward() external {
// 获取用户的挖矿系数
MintInfo memory mintInfo = userMints[_msgSender()];
require(mintInfo.rank > 0, "CRank: No mint exists");
// 要求满足时间限制
require(block.timestamp > mintInfo.maturityTs, "CRank: Mint maturity not reached");
// calculate reward and mint tokens
uint256 rewardAmount = _calculateMintReward(
mintInfo.rank,
mintInfo.term,
mintInfo.maturityTs,
mintInfo.amplifier,
mintInfo.eaaRate
) * 1 ether;
_mint(_msgSender(), rewardAmount);
// 清除掉用户的挖矿数据
_cleanUpUserMint();
emit MintClaimed(_msgSender(), rewardAmount);
}
校验限制后,计算可得奖励数量,然后 _mint 给用户,计算奖励数量的主要计算逻辑在 _calculateMintReward() 中:
function _calculateMintReward(
uint256 cRank,
uint256 term,
uint256 maturityTs,
uint256 amplifier,
uint256 eeaRate
) private view returns (uint256) {
// 计算当前时间与到期时间的差值
uint256 secsLate = block.timestamp - maturityTs;
// 根据时间差值计算需要扣除的数量
uint256 penalty = _penalty(secsLate);
// 计算用户后面还有多少人参与游戏
uint256 rankDelta = Math.max(globalRank - cRank, 2);
uint256 EAA = (1_000 + eeaRate);
uint256 reward = getGrossReward(rankDelta, amplifier, term, EAA);
return (reward * (100 - penalty)) / 100;
}
function getGrossReward(
uint256 rankDelta,
uint256 amplifier,
uint256 term,
uint256 eaa
) public pure returns (uint256) {
// log_2_rankDelta
int128 log128 = rankDelta.fromUInt().log_2();
int128 reward128 = log128.mul(amplifier.fromUInt()).mul(term.fromUInt()).mul(eaa.fromUInt());
return reward128.div(uint256(1_000).fromUInt()).toUInt();
}
这里我们先忽略 penalty 这一块,其他部分的计算正好对应于文档中的:

在计算最终奖励数量的时候,自己参与的位置越靠前,后面的人越多,那么
cRG - cRu
就会越大,同样说明越早参与越好。
我们再来看 penalty 这部分,这块其实就是系统限制用户必须在到期后一定时间内领取走,如果没有领取则会随着时间越来越少,最终归零。
function _penalty(uint256 secsLate) private pure returns (uint256) {
// =MIN(2^(daysLate+3)/window-1,99)
// 计算这个差值有多少天
uint256 daysLate = secsLate / SECONDS_IN_DAY;
// WITHDRAWAL_WINDOW_DAYS = 7
// 如果这个差值在七天及以上,返回 99
if (daysLate > WITHDRAWAL_WINDOW_DAYS - 1) return MAX_PENALTY_PCT;
// 也就是 2 ^ (daysLate + 3) / 7 - 1
uint256 penalty = (uint256(1) << (daysLate + 3)) / WITHDRAWAL_WINDOW_DAYS - 1;
return Math.min(penalty, MAX_PENALTY_PCT);
}
对应于文档中的扣除比例:

文档中显示超过七天就全部不能领取,但是代码中显示最多只会扣除 99%。
到这里,我们就介绍完了时间挖矿的代码部分,接下来我们来看看 stake 挖矿的部分。
这里的 stake 其实比常见的挖矿计算逻辑要简单。常见的挖矿 APY 是根据用户质押数量占比以及参与时间来计算的,属于随挖随走类型的。而这里的 stake 挖矿的 APY 在参与时就已经固定了,且需要在参与时就指定参与时间,在时间到期后才能领取奖励,如果没有到期就领取,只能取回本金,没有任何的奖励。
用户可以在前面时间挖矿到期时调用 claimMintRewardAndStake 同时领取奖励并进行 stake,或者单独调用 stake(uint256 amount, uint256 term) 进行 stake 挖矿:
function stake(uint256 amount, uint256 term) external {
require(balanceOf(_msgSender()) >= amount, "XEN: not enough balance");
// XEN_MIN_STAKE = 0
require(amount > XEN_MIN_STAKE, "XEN: Below min stake");
// 最少 stake 1 天,最多 stake 1000 天
require(term * SECONDS_IN_DAY > MIN_TERM, "XEN: Below min stake term");
require(term * SECONDS_IN_DAY < MAX_TERM_END + 1, "XEN: Above max stake term");
require(userStakes[_msgSender()].amount == 0, "XEN: stake exists");
// burn staked XEN
// 直接 burn,而不是从用户手里转账
_burn(_msgSender(), amount);
// create XEN Stake
_createStake(amount, term);
emit Staked(_msgSender(), amount, term);
}
function _createStake(uint256 amount, uint256 term) private {
userStakes[_msgSender()] = StakeInfo({
term: term,
maturityTs: block.timestamp + term * SECONDS_IN_DAY,
amount: amount,
apy: _calculateAPY()
});
activeStakes++;
totalXenStaked += amount;
}
整体的逻辑也比较简单,参与的时候需要指定时间 term。有一个小细节是在 stake 的时候直接 burn 掉了用户的 token,而不是通过转账的方法,这样可以少一步授权操作。由于合约本身既包含了挖矿操作,同时也是 ERC20,因此可以实现这个逻辑。
接下来我们看看计算 APY 的方法 _calculateAPY():
function _calculateAPY() private view returns (uint256) {
// 这里 genesisTs 是合约部署时间
// (SECONDS_IN_DAY * XEN_APY_DAYS_STEP) -> 86400 * 90,也就是 90 天
// 也就是计算,当前时间距离最开始的合约部署时间有多少个 90 天的差距
uint256 decrease = (block.timestamp - genesisTs) / (SECONDS_IN_DAY * XEN_APY_DAYS_STEP);
// XEN_APY_START = 20
// XEN_APY_END = 2
// 如果 decrease 差值大于 (20 - 2),也就是上面的时间差值大于 18 * 90 = 1620 天
// 则返回 2
if (XEN_APY_START - XEN_APY_END < decrease) return XEN_APY_END;
// 否则返回 20 - decrease
return XEN_APY_START - decrease;
}
基本逻辑也是类似于上面计算 EAA 的方法,一次函数递减,参与的时间越早,相对应的 APY 就越大。初始值为 20,每过 90 天,减少 1。最终在 1620 天后,恒定为 2。对应于文档:

最终在 stake 到期后,可以调用 withdraw() 取出本金和奖励:
function withdraw() external {
StakeInfo memory userStake = userStakes[_msgSender()];
require(userStake.amount > 0, "XEN: no stake exists");
uint256 xenReward = _calculateStakeReward(
userStake.amount,
userStake.term,
userStake.maturityTs,
userStake.apy
);
activeStakes--;
totalXenStaked -= userStake.amount;
// mint staked XEN (+ reward)
_mint(_msgSender(), userStake.amount + xenReward);
emit Withdrawn(_msgSender(), userStake.amount, xenReward);
delete userStakes[_msgSender()];
}
其中计算奖励数量的方法 _calculateStakeReward():
function _calculateStakeReward(
uint256 amount,
uint256 term,
uint256 maturityTs,
uint256 apy
) private view returns (uint256) {
if (block.timestamp > maturityTs) {
// DAYS_IN_YEAR = 365
uint256 rate = (apy * term * 1_000_000) / DAYS_IN_YEAR;
return (amount * rate) / 100_000_000;
}
return 0;
}
对应于文档中的:

对于 stake 挖矿而言,没有领取的限制,奖励数量不会变化。
到这里我们就看完了主要的逻辑代码。这个玩法有意思的地方在于越早参与获得的奖励越多,相当于普通的挖头矿,但是同时也取决于总体的参与人数,如果后面没有人参与,那么也没啥意义。必须是参与的早且后面还有更多人参与的情况下,奖励才会更多。目前时刻总参与人数已经快达到 50 万了,热度确实很高。
同时,前面的时间挖矿和后面的 stake 挖矿也存在博弈关系,如果前面选择的时间越长,获得的奖励就越多,但是来到后面的 stake 挖矿的 APY 就会降低,需要大家自行抉择。
合约本身代码没啥难度,但是整体机制比较有趣,值得花点时间了解。
欢迎和我交流
No activity yet