Curve 是 DeFi 领先的**AMM** (自动做市商)。通过 Curve 的工厂启动了数百个流动资金池,并由 Curve 的 DAO 进行激励。用户依靠 Curve 的专有公式在 ERC-20 代币之间提供高流动性、低滑点、低费用的交易。
Curve DAO 代币的主要目的是激励 Curve Finance 平台上的流动性提供者,以及让尽可能多的用户参与协议的治理。
目前,CRV 主要有三种用途:投票、质押和助推。这三件事将要求您投票锁定您的 CRV 并获得 veCRV。
- 锁定CRV,获得才可以VeCRV,也才能参与投票。
veCRV 数量取决于你锁定 CRV 的时间,最短锁定时间为一周,最长锁定时间为四年您锁定 CRV 的时间越长,您拥有的投票权就越大,你可以投票锁定 1,000 CRV 一年以获得 250 veCRV 权重。
随着您的托管代币接近锁定到期,您的 veCRV 权重会逐渐减少。(锁定CRV,获得VeCRV, 才可以参与投票)
总结:锁定投票CRV,获得veCRV,锁定的CRV时间越长,拥有的投票权就越大
例如: 投票锁定1000crv, 一年以内获得250veCRV权重,随着代币接近锁仓到期,权重就会逐渐减少。
用户投票权重从锁定时刻开始线性递减。总投票权也是如此. 为了避免周期性签到,每次用户充值、提现或更改锁定时间时,我们记录用户的斜率和偏差为线性函数在公共映射`user_point_history`中。我们还改变了总投票权的斜率和偏差并将其记录在`point_history`. 此外,当一个用户的锁定计划结束时,我们计划改变斜率在未来`slope_changes`。每个更改都涉及将`epoch` 增加。
这样我们就不必遍历所有用户来计算,应该多少改变,我们也不需要用户定期检查。但是,我们将用户锁定的结束时间限制为整周四舍五入的时间。
当用户存入和锁定治理令牌以及锁定时间到期时,斜率和偏差都会发生变化。所有可能的到期时间都四舍五入到整周,以使区块链的读取次数最多错过的周数成正比,而不是用户数量(可能很大)。
1.每次用户充值提现更改锁定时间时,就更新斜和偏差为线性函数,在公共映射`user_point_history`中
2.再改变总投票权的斜率和偏差并将其记录在`point_history`
3.当用户锁定技术结束时,改变写斜率还有偏差,`slope_changes`。每个更改都涉及将`epoch`1 增加
4.最后如果存入的锁定治理令牌锁定时间到期,斜率和偏差都会变化.然后更新可以看到用户的权重衰减
(只分析部分重要的方法)-
uint256 private constant DEPOSIT_FOR_TYPE = 0;
uint256 private constant CREATE_LOCK_TYPE = 1;
uint256 private constant INCREASE_LOCK_AMOUNT = 2;
uint256 private constant INCREASE_UNLOCK_TIME = 3;
uint256 public constant WEEK = 7 * 86400; // all future times are rounded by week
uint256 public constant MAXTIME = 4 * 365 * 86400; // 4 years
uint256 public constant MULTIPLIER = 10**18;
address public token;
uint256 public supply;
mapping(address => LockedBalance) public locked;
//everytime user deposit/withdraw/change_locktime, these values will be updated;
uint256 public override epoch;
mapping(uint256 => Point) public supplyPointHistory; // epoch -> unsigned point.
mapping(address => mapping(uint256 => Point)) public userPointHistory; // user ->
Point[user_epoch]
mapping(address => uint256) public userPointEpoch;
mapping(uint256 => int256) public slopeChanges; // time -> signed slope change
string public name;
string public symbol;
uint256 public decimals;
//1.衰减主要的变量值
supplyPointHistory[0] = Point({
bias: 0,
slope: 0,
ts: block.timestamp,
blk: block.number
});
decimals = 18;
name = "Vote-escrowed BEND";
symbol = "veBEND";
}
执行方法createLockFor创建锁定CRV换取veCRV,锁定的最长期限为四年(此方法存入CRV,并且给用户生产veCrv,并且更新公式的系数)
function createLockFor(
address _beneficiary,
uint256 _value,
uint256 _unlockTime
) external override {
_createLock(_beneficiary, _value, _unlockTime);
}
/***
*@dev Deposit `_value` tokens for `msg.sender` and lock until `_unlockTime`
*@param _value Amount to deposit
*@param _unlockTime Epoch time when tokens unlock, rounded down to whole weeks
*/
function _createLock(
address _beneficiary,
uint256 _value,
uint256 _unlockTime
) internal nonReentrant {
_unlockTime = (_unlockTime / WEEK) * WEEK; // Locktime is rounded down to weeks
LockedBalance memory _locked = locked[_beneficiary];
require(_value > 0, "Can't lock zero value");
require(_locked.amount == 0, "Withdraw old tokens first");
require(
_unlockTime > block.timestamp,
"Can only lock until time in the future"
);
require(
_unlockTime <= block.timestamp + MAXTIME,
"Voting lock can be 4 years max"
);
_depositFor(
msg.sender,
_beneficiary,
_value,
_unlockTime,
_locked,
CREATE_LOCK_TYPE
);
}
一共6组时间锁定,最大四年,最小一个星期。
1周0.48% 1个月0.21% 3个月0.63% 6个月12.7% 1年25% 4年100%
维护系数会在每次调用lock,或者提取token操作,都会更新调用CheckPoint此方法,更新系数值(checkPoint比较重要的函数)
/***
*@dev Record global and per-user data to checkpoint
*@param _addr User's wallet address. No user checkpoint if 0x0
*@param _oldLocked Pevious locked amount / end lock time for the user
*@param _newLocked New locked amount / end lock time for the user
*/
function _checkpoint(
address _addr,
LockedBalance memory _oldLocked,
LockedBalance memory _newLocked
) internal {
CheckpointParameters memory _st;
_st.epoch = epoch;
if (_addr != address(0)) {
// Calculate slopes and biases
// Kept at zero when they have to
if (_oldLocked.end > block.timestamp && _oldLocked.amount > 0) {
_st.userOldPoint.slope = _oldLocked.amount / int256(MAXTIME);
_st.userOldPoint.bias =
_st.userOldPoint.slope *
int256(_oldLocked.end - block.timestamp);
}
if (_newLocked.end > block.timestamp && _newLocked.amount > 0) {
_st.userNewPoint.slope = _newLocked.amount / int256(MAXTIME);
_st.userNewPoint.bias =
_st.userNewPoint.slope *
int256(_newLocked.end - block.timestamp);
}
// Read values of scheduled changes in the slope
// _oldLocked.end can be in the past and in the future
// _newLocked.end can ONLY by in the FUTURE unless everything expired than zeros
_st.oldDslope = slopeChanges[_oldLocked.end];
if (_newLocked.end != 0) {
if (_newLocked.end == _oldLocked.end) {
_st.newDslope = _st.oldDslope;
} else {
_st.newDslope = slopeChanges[_newLocked.end];
}
}
}
Point memory _lastPoint = Point({
bias: 0,
slope: 0,
ts: block.timestamp,
blk: block.number
});
if (_st.epoch > 0) {
_lastPoint = supplyPointHistory[_st.epoch];
}
uint256 _lastCheckPoint = _lastPoint.ts;
// _initialLastPoint is used for extrapolation to calculate block number
// (approximately, for *At methods) and save them
// as we cannot figure that out exactly from inside the contract
// Point memory _initialLastPoint = _lastPoint;
uint256 _initBlk = _lastPoint.blk;
uint256 _initTs = _lastPoint.ts;
uint256 _blockSlope = 0; // dblock/dt
if (block.timestamp > _lastPoint.ts) {
_blockSlope =
(MULTIPLIER * (block.number - _lastPoint.blk)) /
(block.timestamp - _lastPoint.ts);
}
// If last point is already recorded in this block, slope=0
// But that's ok b/c we know the block in such case
// Go over weeks to fill history and calculate what the current point is
uint256 _ti = (_lastCheckPoint / WEEK) * WEEK;
for (uint256 i; i < 255; i++) {
// Hopefully it won't happen that this won't get used in 5 years!
// If it does, users will be able to withdraw but vote weight will be broken
_ti += WEEK;
int256 d_slope = 0;
if (_ti > block.timestamp) {
// reach future time, reset to blok time
_ti = block.timestamp;
} else {
d_slope = slopeChanges[_ti];
}
_lastPoint.bias =
_lastPoint.bias -
_lastPoint.slope *
int256(_ti - _lastCheckPoint);
_lastPoint.slope += d_slope;
if (_lastPoint.bias < 0) {
// This can happen
_lastPoint.bias = 0;
}
if (_lastPoint.slope < 0) {
// This cannot happen - just in case
_lastPoint.slope = 0;
}
_lastCheckPoint = _ti;
_lastPoint.ts = _ti;
_lastPoint.blk =
_initBlk +
((_blockSlope * (_ti - _initTs)) / MULTIPLIER);
_st.epoch += 1;
if (_ti == block.timestamp) {
// history filled over, break loop
_lastPoint.blk = block.number;
break;
} else {
supplyPointHistory[_st.epoch] = _lastPoint;
}
}
epoch = _st.epoch;
// Now supplyPointHistory is filled until t=now
if (_addr != address(0)) {
// If last point was in this block, the slope change has been applied already
// But in such case we have 0 slope(s)
_lastPoint.slope += _st.userNewPoint.slope - _st.userOldPoint.slope;
_lastPoint.bias += _st.userNewPoint.bias - _st.userOldPoint.bias;
if (_lastPoint.slope < 0) {
_lastPoint.slope = 0;
}
if (_lastPoint.bias < 0) {
_lastPoint.bias = 0;
}
}
// Record the changed point into history
supplyPointHistory[_st.epoch] = _lastPoint;
if (_addr != address(0)) {
// Schedule the slope changes (slope is going down)
// We subtract new_user_slope from [_newLocked.end]
// and add old_user_slope to [_oldLocked.end]
if (_oldLocked.end > block.timestamp) {
// _oldDslope was - _userOldPoint.slope, so we cancel that
_st.oldDslope += _st.userOldPoint.slope;
if (_newLocked.end == _oldLocked.end) {
_st.oldDslope -= _st.userNewPoint.slope; // It was a new deposit, not extension
}
slopeChanges[_oldLocked.end] = _st.oldDslope;
}
if (_newLocked.end > block.timestamp) {
if (_newLocked.end > _oldLocked.end) {
_st.newDslope -= _st.userNewPoint.slope; // old slope disappeared at this point
slopeChanges[_newLocked.end] = _st.newDslope;
}
// else we recorded it already in _oldDslope
}
// Now handle user history
uint256 _userEpoch = userPointEpoch[_addr] + 1;
userPointEpoch[_addr] = _userEpoch;
_st.userNewPoint.ts = block.timestamp;
_st.userNewPoint.blk = block.number;
userPointHistory[_addr][_userEpoch] = _st.userNewPoint;
}
}
/***
*@dev Withdraw all tokens for `msg.sender`
*@dev Only possible if the lock has expired
*/
function withdraw() external override nonReentrant {
LockedBalance memory _locked = LockedBalance(
locked[msg.sender].amount,
locked[msg.sender].end
);
require(block.timestamp >= _locked.end, "The lock didn't expire");
uint256 _value = uint256(_locked.amount);
LockedBalance memory _oldLocked = LockedBalance(
locked[msg.sender].amount,
locked[msg.sender].end
);
_locked.end = 0;
_locked.amount = 0;
locked[msg.sender] = _locked;
uint256 _supplyBefore = supply;
supply = _supplyBefore - _value;
// _oldLocked can have either expired <= timestamp or zero end
// _locked has only 0 end
// Both can have >= 0 amount
_checkpoint(msg.sender, _oldLocked, _locked);
IERC20Upgradeable(token).safeTransfer(msg.sender, _value);
emit Withdraw(msg.sender, _value, block.timestamp);
emit Supply(_supplyBefore, _supplyBefore - _value);
}
/*** *@notice Get the current voting power for `msg.sender` *@dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility *@param _addr User wallet address *@param _t Epoch time to return voting power at *@return User voting power *@dev return the present voting power if _t is 0 */ function balanceOf(address _addr, uint256 _t) external view returns (uint256) { if (_t == 0) { _t = block.timestamp; } uint256 _epoch = userPointEpoch[_addr]; if (_epoch == 0) { return 0; } else { Point memory _lastPoint = userPointHistory[_addr][_epoch]; **_lastPoint.bias -= _lastPoint.slope * int256(_t - _lastPoint.ts);** if (_lastPoint.bias < 0) { _lastPoint.bias = 0; } return uint256(_lastPoint.bias); } }
以上调研的相关参考资料
Curve DAO: Vote-Escrowed CRV — Curve 1.0.0 documentation 官方文档
bend-incentive/VeBend.sol at main · BendDAO/bend-incentive · GitHub 源码
https://dao.curve.fi/locker 前端
https://curve.fi/files/stableswap-paper.pdf 白皮书