Diff
checker
文本
文本
图像
文档
Excel
文件夹
Legal
Enterprise
桌面版
定价
登录
下载 Diffchecker 桌面版
比较文本
查找两个文本文件之间的差异
工具
历史
实时编辑器
折叠未更改行
关闭换行
视图
拆分
统一
比对精度
智能
单词
字符
语法高亮
选择语法
忽略
文本转换
转到第一个差异
编辑输入
Diffchecker Desktop
运行Diffchecker最安全的方式。获取Diffchecker桌面应用:您的差异永远不会离开您的电脑!
获取桌面版
VotingEscrow
创建于
4年前
差异永不过期
清除
导出
分享
解释
483 删除
行
总计
删除
字符
总计
删除
要继续使用此功能,请升级到
Diff
checker
Pro
查看价格
691 行
全部复制
454 添加
行
总计
添加
字符
总计
添加
要继续使用此功能,请升级到
Diff
checker
Pro
查看价格
661 行
全部复制
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: AGPL-3.0-or-later
复制
已复制
复制
已复制
pragma solidity
0.8.
6;
pragma solidity
^
0.8.
3;
import { IBasicToken } from "../shared/IBasicToken.sol";
import { IIncentivisedVotingLockup } from "../interfaces/IIncentivisedVotingLockup.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import { RewardsDistributionRecipient } from "../rewards/RewardsDistributionRecipient.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { StableMath } from "../shared/StableMath.sol";
import { Root } from "../shared/Root.sol";
复制
已复制
复制
已复制
/**
import {
* @title IncentivisedVotingLockup
ReentrancyGuard
* @author Voting Weight tracking & Decay
} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
* -> Curve Finance (MIT) - forked & ported to Solidity
import { IERC20 } from "./interfaces/IERC20.sol";
* -> https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy
import { IVotingEscrow } from "./interfaces/IVotingEscrow.sol";
* osolmaz - Research & Reward distributions
import { IBlocklist } from "./interfaces/IBlocklist.sol";
* alsco77 - Solidity implementation
* @notice Lockup MTA, receive vMTA (voting weight that decays over time), and earn
* rewards based on staticWeight
* @dev Supports:
* 1) Tracking MTA Locked up (LockedBalance)
* 2) Pull Based Reward allocations based on Lockup (Static Balance)
* 3) Decaying voting weight lookup through CheckpointedERC20 (balanceOf)
* 4) Ejecting fully decayed participants from reward allocation (eject)
* 5) Migration of points to v2 (used as multiplier in future) ***** (rewardsPaid)
* 6) Closure of contract (expire)
*/
contract IncentivisedVotingLockup is
IIncentivisedVotingLockup,
ReentrancyGuard,
RewardsDistributionRecipient
{
using StableMath for uint256;
using SafeERC20 for IERC20;
复制
已复制
复制
已复制
/**
Shared Events
*/
/// @title VotingEscrow
/// @author Curve Finance (MIT) - original concept and implementation in Vyper
/// (see https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy)
/// mStable (AGPL) - forking Curve's Vyper contract and porting to Solidity
/// (see https://github.com/mstable/mStable-contracts/blob/master/contracts/governance/IncentivisedVotingLockup.sol)
/// FIAT DAO (AGPL) - this version
/// @notice Plain Curve VotingEscrow mechanics with following adjustments:
/// 1) Delegation of lock and voting power
/// 2) Quit an existing lock and pay a penalty
/// 3) Whitelisting of SmartWallets outside the VotingEscrow
/// 4) Reduced pointHistory array size and, as a result, lifetime of the contract
/// 5) Removed public deposit_for and Aragon compatibility (no use case)
contract VotingEscrow is IVotingEscrow, ReentrancyGuard {
//
Shared Events
event Deposit(
event Deposit(
address indexed provider,
address indexed provider,
uint256 value,
uint256 value,
uint256 locktime,
uint256 locktime,
LockAction indexed action,
LockAction indexed action,
uint256 ts
uint256 ts
);
);
复制
已复制
复制
已复制
event Withdraw(
address indexed provider,
uint256 value,
uint256 ts
);
event Withdraw(
event
Ejected
(address
indexed ejected,
address
ejector, uint256 ts
);
address indexed provider,
event
Expired(
);
uint256 value,
event
RewardAdded
(uint256
reward
);
LockAction indexed action,
event
RewardPaid(address indexed user, uint256 reward
);
uint256 ts
);
event
TransferOwnership
(address
owner);
event UpdateBlocklist(
address
blocklist
);
event
UpdatePenaltyRecipient(address recipient
);
event
CollectPenalty
(uint256
amount, address recipient
);
event
Unlock(
);
复制
已复制
复制
已复制
/
**
Shared
G
lobal
s */
/
/
Shared
g
lobal
state
IERC20 public
stakingToken
;
IERC20 public
token
;
uint256
private
constant WEEK = 7 days;
uint256
public
constant WEEK = 7 days;
uint256 public constant MAXTIME = 365 days;
uint256 public constant MAXTIME = 365 days;
复制
已复制
复制
已复制
uint256 public
END
;
uint256 public
constant MULTIPLIER = 10**18
;
bool public expired = false;
address public owner;
address public penaltyRecipient; // receives collected penalty payments
uint256 public maxPenalty = 10**18; // penalty for quitters with MAXTIME remaining lock
uint256 public penaltyAccumulated; // accumulated and unwithdrawn penalty payments
address public blocklist;
复制
已复制
复制
已复制
/
**
Lock
up */
/
/
Lock
state
uint256 public globalEpoch;
uint256 public globalEpoch;
复制
已复制
复制
已复制
Point[
] public pointHistory;
Point[
1000000000000000000
] public pointHistory;
// 1e9 * userPointHistory-length, so sufficient for 1e9 users
mapping(address => Point[
]) public userPointHistory;
mapping(address => Point[
1000000000
]) public userPointHistory;
mapping(address => uint256) public userPointEpoch;
mapping(address => uint256) public userPointEpoch;
mapping(uint256 => int128) public slopeChanges;
mapping(uint256 => int128) public slopeChanges;
mapping(address => LockedBalance) public locked;
mapping(address => LockedBalance) public locked;
复制
已复制
复制
已复制
// Voting token
- Checkpointed view only ERC20
// Voting token
string public name;
string public name;
string public symbol;
string public symbol;
uint256 public decimals = 18;
uint256 public decimals = 18;
复制
已复制
复制
已复制
/** Rewards */
//
Structs
// Updated upon admin deposit
uint256 public periodFinish = 0;
uint256 public rewardRate = 0;
// Globals updated per stake/deposit/withdrawal
uint256 public totalStaticWeight = 0;
uint256 public lastUpdateTime = 0;
uint256 public rewardPerTokenStored = 0;
// Per user storage updated per stake/deposit/withdrawal
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
mapping(address => uint256) public rewardsPaid;
/**
Structs
*/
struct Point {
struct Point {
int128 bias;
int128 bias;
int128 slope;
int128 slope;
uint256 ts;
uint256 ts;
uint256 blk;
uint256 blk;
}
}
复制
已复制
复制
已复制
struct LockedBalance {
struct LockedBalance {
int128 amount;
int128 amount;
uint256 end;
uint256 end;
复制
已复制
复制
已复制
int128 delegated;
address delegatee;
}
}
复制
已复制
复制
已复制
// Miscellaneous
enum LockAction {
enum LockAction {
复制
已复制
复制
已复制
CREATE
_LOCK
,
CREATE
,
INCREASE_
LOCK_
AMOUNT,
INCREASE_
AMOUNT,
INCREASE_
LOCK_TIME
INCREASE_
AMOUNT_AND_DELEGATION,
INCREASE_TIME,
WITHDRAW,
QUIT,
DELEGATE,
UNDELEGATE
}
}
复制
已复制
复制
已复制
/// @notice Initializes state
/// @param _owner The owner is able to update `owner`, `penaltyRecipient` and `penaltyRate`
/// @param _penaltyRecipient The recipient of penalty paid by lock quitters
/// @param _token The token locked in order to obtain voting power
/// @param _name The name of the voting token
/// @param _symbol The symbol of the voting token
constructor(
constructor(
复制
已复制
复制
已复制
address _
stakingToken
,
address _
owner,
address _penaltyRecipient,
address _token
,
string memory _name,
string memory _name,
复制
已复制
复制
已复制
string memory _symbol
,
string memory _symbol
address _nexus,
)
{
address _rewardsDistributor
token
= IERC20(_
token
);
)
RewardsDistributionRecipient(_nexus, _rewardsDistributor)
{
pointHistory[0]
= Point({
stakingToken
= IERC20(_
stakingToken
);
Point memory init
= Point({
bias: int128(0),
bias: int128(0),
slope: int128(0),
slope: int128(0),
ts: block.timestamp,
ts: block.timestamp,
blk: block.number
blk: block.number
});
});
复制
已复制
复制
已复制
pointHistory.push(init);
复制
已复制
复制
已复制
decimals =
IBasicToken(_stakingToken
).decimals();
decimals =
IERC20(_token
).decimals();
require(decimals <= 18, "
Cannot have more than 18
decimals");
require(decimals <= 18, "
Exceeds max
decimals");
name = _name;
name = _name;
symbol = _symbol;
symbol = _symbol;
复制
已复制
复制
已复制
owner = _owner;
END
=
block.timestamp + MAXTIME
;
penaltyRecipient
=
_penaltyRecipient
;
}
}
复制
已复制
复制
已复制
/** @dev Modifier to ensure contract has not yet expired */
modifier
checkBlocklist
() {
modifier
contractNotExpired
() {
require(
require(
!expired, "C
ontract
is expired"
);
!IBlocklist(blocklist).isBlocked(msg.sender),
"Blocked c
ontract
"
);
_;
_;
}
}
复制
已复制
复制
已复制
/**
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
* @dev Validates that the user has an expired lock && they still have capacity to earn
/// Owner Functions ///
* @param _addr User address to check
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
*/
modifier lockupIsOver(address _addr) {
/// @notice Transfers ownership to a new owner
LockedBalance memory userLock = locked[_addr];
/// @param _addr The new owner
require(userLock.amount > 0 && block.timestamp >= userLock.end, "Users lock didn't expire");
/// @dev Owner should always be a timelock contract
require(staticBalanceOf(
_addr
) > 0, "User must have existing bias")
;
function transferOwnership(address _addr) external {
_
;
require(msg.sender == owner, "Only owner");
owner =
_addr
;
emit TransferOwnership(_addr)
;
}
}
复制
已复制
复制
已复制
/***************************************
/// @notice Updates the blocklist contract
LOCKUP - GETTERS
function updateBlocklist(address _addr) external {
****************************************/
require(msg.sender == owner, "Only owner");
blocklist = _addr;
emit UpdateBlocklist(_addr);
}
复制
已复制
复制
已复制
/**
/// @notice Updates the recipient of the accumulated penalty paid by quitters
* @dev Gets
the last available user point
function updatePenaltyRecipient(address _addr) external {
*
@param _addr User address
require(msg.sender == owner, "Only owner");
*
@return bias i.e. y
penaltyRecipient = _addr;
*
@return slope i.e. linear gradient
emit UpdatePenaltyRecipient(_addr);
*
@return ts i.e. time point was logged
}
*/
/// @notice Removes quitlock penalty by setting it to zero
/// @dev This is an irreversible action
function unlock() external {
require(msg.sender == owner, "Only owner");
maxPenalty = 0;
emit Unlock();
}
/// @notice Forces an undelegation of virtual balance for a blocked locker
/// @dev Can only be called by the Blocklist contract (as part of a block)
/// @dev This is an irreversible action
function forceUndelegate(address _addr) external override {
require(msg.sender == blocklist, "Only Blocklist");
LockedBalance memory locked_ = locked[_addr];
address delegatee = locked_.delegatee;
int128 value = locked_.amount;
if (delegatee != _addr && value > 0) {
LockedBalance memory fromLocked;
locked_.delegatee = _addr;
fromLocked = locked[delegatee];
_delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE);
_delegate(_addr, locked_, value, LockAction.DELEGATE);
}
}
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
/// LOCK MANAGEMENT ///
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
/// @notice Returns a user's lock expiration
/// @param _addr The address of the user
/// @return Expiration of the user's lock
function lockEnd(address _addr) external view returns (uint256) {
return locked[_addr].end;
}
/// @notice Returns
the last available user point
for a user
///
@param _addr User address
///
@return bias i.e. y
///
@return slope i.e. linear gradient
///
@return ts i.e. time point was logged
function getLastUserPoint(address _addr)
function getLastUserPoint(address _addr)
external
external
view
view
复制
已复制
复制
已复制
override
returns (
returns (
int128 bias,
int128 bias,
int128 slope,
int128 slope,
uint256 ts
uint256 ts
)
)
{
{
uint256 uepoch = userPointEpoch[_addr];
uint256 uepoch = userPointEpoch[_addr];
if (uepoch == 0) {
if (uepoch == 0) {
return (0, 0, 0);
return (0, 0, 0);
}
}
Point memory point = userPointHistory[_addr][uepoch];
Point memory point = userPointHistory[_addr][uepoch];
return (point.bias, point.slope, point.ts);
return (point.bias, point.slope, point.ts);
}
}
复制
已复制
复制
已复制
/***************************************
/// @notice
Records a checkpoint of both individual and global slope
LOCKUP
///
@param _addr User address, or address(0) for only global
****************************************/
///
@param _oldLocked Old amount that user had locked, or null for global
///
@param _newLocked new amount that user has locked, or null for global
/**
* @dev
Records a checkpoint of both individual and global slope
*
@param _addr User address, or address(0) for only global
*
@param _oldLocked Old amount that user had locked, or null for global
*
@param _newLocked new amount that user has locked, or null for global
*/
function _checkpoint(
function _checkpoint(
address _addr,
address _addr,
LockedBalance memory _oldLocked,
LockedBalance memory _oldLocked,
LockedBalance memory _newLocked
LockedBalance memory _newLocked
) internal {
) internal {
Point memory userOldPoint;
Point memory userOldPoint;
Point memory userNewPoint;
Point memory userNewPoint;
int128 oldSlopeDelta = 0;
int128 oldSlopeDelta = 0;
int128 newSlopeDelta = 0;
int128 newSlopeDelta = 0;
uint256 epoch = globalEpoch;
uint256 epoch = globalEpoch;
if (_addr != address(0)) {
if (_addr != address(0)) {
// Calculate slopes and biases
// Calculate slopes and biases
// Kept at zero when they have to
// Kept at zero when they have to
复制
已复制
复制
已复制
if (_oldLocked.end > block.timestamp && _oldLocked.
amount
> 0) {
if (_oldLocked.end > block.timestamp && _oldLocked.
delegated
> 0) {
userOldPoint.slope =
_oldLocked.
amount / SafeCast.toI
nt128(int256(MAXTIME));
userOldPoint.slope =
_oldLocked.
delegated /
i
nt128(int256(MAXTIME));
userOldPoint.bias =
userOldPoint.bias =
userOldPoint.slope *
userOldPoint.slope *
复制
已复制
复制
已复制
SafeCast.toI
nt128(int256(_oldLocked.end - block.timestamp));
i
nt128(int256(_oldLocked.end - block.timestamp));
}
}
复制
已复制
复制
已复制
if (_newLocked.end > block.timestamp && _newLocked.
amount
> 0) {
if (_newLocked.end > block.timestamp && _newLocked.
delegated
> 0) {
userNewPoint.slope =
_newLocked.
amount / SafeCast.toI
nt128(int256(MAXTIME));
userNewPoint.slope =
_newLocked.
delegated /
i
nt128(int256(MAXTIME));
userNewPoint.bias =
userNewPoint.bias =
userNewPoint.slope *
userNewPoint.slope *
复制
已复制
复制
已复制
SafeCast.toI
nt128(int256(_newLocked.end - block.timestamp));
i
nt128(int256(_newLocked.end - block.timestamp));
}
}
// Moved from bottom final if statement to resolve stack too deep err
// Moved from bottom final if statement to resolve stack too deep err
// start {
// start {
// Now handle user history
// Now handle user history
uint256 uEpoch = userPointEpoch[_addr];
uint256 uEpoch = userPointEpoch[_addr];
if (uEpoch == 0) {
if (uEpoch == 0) {
复制
已复制
复制
已复制
userPointHistory[_addr].push(userOldPoint);
userPointHistory[_addr][uEpoch
+ 1] = userOldPoint;
}
// track the total static weight
uint256 newStatic = _staticBalance(userNewPoint.slope, block.timestamp, _newLocked.end);
uint256 additiveStaticWeight = totalStaticWeight + newStatic;
if (uEpoch > 0) {
uint256 oldStatic = _staticBalance(
userPointHistory[_addr][uEpoch
].slope,
userPointHistory[_addr][uEpoch].ts,
_oldLocked.end
);
additiveStaticWeight = additiveStaticWeight - oldStatic;
}
}
复制
已复制
复制
已复制
totalStaticWeight = additiveStaticWeight;
userPointEpoch[_addr] = uEpoch + 1;
userPointEpoch[_addr] = uEpoch + 1;
userNewPoint.ts = block.timestamp;
userNewPoint.ts = block.timestamp;
userNewPoint.blk = block.number;
userNewPoint.blk = block.number;
复制
已复制
复制
已复制
userPointHistory[_addr]
.push(
userNewPoint
)
;
userPointHistory[_addr]
[uEpoch + 1] =
userNewPoint
;
// } end
// } end
// Read values of scheduled changes in the slope
// Read values of scheduled changes in the slope
// oldLocked.end can be in the past and in the future
// oldLocked.end can be in the past and in the future
// newLocked.end can ONLY by in the FUTURE unless everything expired: than zeros
// newLocked.end can ONLY by in the FUTURE unless everything expired: than zeros
oldSlopeDelta = slopeChanges[_oldLocked.end];
oldSlopeDelta = slopeChanges[_oldLocked.end];
if (_newLocked.end != 0) {
if (_newLocked.end != 0) {
if (_newLocked.end == _oldLocked.end) {
if (_newLocked.end == _oldLocked.end) {
newSlopeDelta = oldSlopeDelta;
newSlopeDelta = oldSlopeDelta;
} else {
} else {
newSlopeDelta = slopeChanges[_newLocked.end];
newSlopeDelta = slopeChanges[_newLocked.end];
}
}
}
}
}
}
复制
已复制
复制
已复制
Point memory lastPoint =
Point({
Point memory lastPoint =
bias: 0,
Point({
slope: 0,
bias: 0,
ts: block.timestamp,
slope: 0,
blk: block.number
ts: block.timestamp,
});
blk: block.number
});
if (epoch > 0) {
if (epoch > 0) {
lastPoint = pointHistory[epoch];
lastPoint = pointHistory[epoch];
}
}
uint256 lastCheckpoint = lastPoint.ts;
uint256 lastCheckpoint = lastPoint.ts;
// initialLastPoint is used for extrapolation to calculate block number
// initialLastPoint is used for extrapolation to calculate block number
// (approximately, for *At methods) and save them
// (approximately, for *At methods) and save them
// as we cannot figure that out exactly from inside the contract
// as we cannot figure that out exactly from inside the contract
复制
已复制
复制
已复制
Point memory initialLastPoint =
Point({
Point memory initialLastPoint =
bias: 0,
Point({
bias: 0,
slope: 0,
ts: lastPoint.ts,
blk: lastPoint.blk
});
slope: 0,
ts: lastPoint.ts,
blk: lastPoint.blk
});
uint256 blockSlope = 0; // dblock/dt
uint256 blockSlope = 0; // dblock/dt
if (block.timestamp > lastPoint.ts) {
if (block.timestamp > lastPoint.ts) {
blockSlope =
blockSlope =
复制
已复制
复制
已复制
StableMath.scaleInteger
(block.number - lastPoint.blk)
/
(MULTIPLIER *
(block.number - lastPoint.blk)
)
/
(block.timestamp - lastPoint.ts);
(block.timestamp - lastPoint.ts);
}
}
// If last point is already recorded in this block, slope=0
// If last point is already recorded in this block, slope=0
// But that's ok b/c we know the block in such case
// 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
// Go over weeks to fill history and calculate what the current point is
uint256 iterativeTime = _floorToWeek(lastCheckpoint);
uint256 iterativeTime = _floorToWeek(lastCheckpoint);
for (uint256 i = 0; i < 255; i++) {
for (uint256 i = 0; i < 255; i++) {
// Hopefully it won't happen that this won't get used in 5 years!
// 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
// If it does, users will be able to withdraw but vote weight will be broken
iterativeTime = iterativeTime + WEEK;
iterativeTime = iterativeTime + WEEK;
int128 dSlope = 0;
int128 dSlope = 0;
if (iterativeTime > block.timestamp) {
if (iterativeTime > block.timestamp) {
iterativeTime = block.timestamp;
iterativeTime = block.timestamp;
} else {
} else {
dSlope = slopeChanges[iterativeTime];
dSlope = slopeChanges[iterativeTime];
}
}
复制
已复制
复制
已复制
int128 biasDelta =
lastPoint.slope *
int128 biasDelta =
SafeCast.toI
nt128(int256((iterativeTime - lastCheckpoint)));
lastPoint.slope *
i
nt128(int256((iterativeTime - lastCheckpoint)));
lastPoint.bias = lastPoint.bias - biasDelta;
lastPoint.bias = lastPoint.bias - biasDelta;
lastPoint.slope = lastPoint.slope + dSlope;
lastPoint.slope = lastPoint.slope + dSlope;
// This can happen
// This can happen
if (lastPoint.bias < 0) {
if (lastPoint.bias < 0) {
lastPoint.bias = 0;
lastPoint.bias = 0;
}
}
// This cannot happen - just in case
// This cannot happen - just in case
if (lastPoint.slope < 0) {
if (lastPoint.slope < 0) {
lastPoint.slope = 0;
lastPoint.slope = 0;
}
}
lastCheckpoint = iterativeTime;
lastCheckpoint = iterativeTime;
lastPoint.ts = iterativeTime;
lastPoint.ts = iterativeTime;
lastPoint.blk =
lastPoint.blk =
initialLastPoint.blk +
initialLastPoint.blk +
复制
已复制
复制
已复制
blockSlope
.mulTruncate
(iterativeTime - initialLastPoint.ts)
;
(
blockSlope
*
(iterativeTime - initialLastPoint.ts)
) /
MULTIPLIER
;
// when epoch is incremented, we either push here or after slopes updated below
// when epoch is incremented, we either push here or after slopes updated below
epoch = epoch + 1;
epoch = epoch + 1;
if (iterativeTime == block.timestamp) {
if (iterativeTime == block.timestamp) {
lastPoint.blk = block.number;
lastPoint.blk = block.number;
break;
break;
} else {
} else {
复制
已复制
复制
已复制
//
pointHistory[epoch] = lastPoint;
pointHistory[epoch] = lastPoint;
pointHistory.push(lastPoint);
}
}
}
}
globalEpoch = epoch;
globalEpoch = epoch;
// Now pointHistory is filled until t=now
// Now pointHistory is filled until t=now
if (_addr != address(0)) {
if (_addr != address(0)) {
// If last point was in this block, the slope change has been applied already
// If last point was in this block, the slope change has been applied already
// But in such case we have 0 slope(s)
// But in such case we have 0 slope(s)
复制
已复制
复制
已复制
lastPoint.slope =
lastPoint.slope +
userNewPoint.slope -
userOldPoint.slope;
lastPoint.slope =
lastPoint.bias =
lastPoint.bias +
userNewPoint.bias -
userOldPoint.bias;
lastPoint.slope +
userNewPoint.slope -
userOldPoint.slope;
lastPoint.bias =
lastPoint.bias +
userNewPoint.bias -
userOldPoint.bias;
if (lastPoint.slope < 0) {
if (lastPoint.slope < 0) {
lastPoint.slope = 0;
lastPoint.slope = 0;
}
}
if (lastPoint.bias < 0) {
if (lastPoint.bias < 0) {
lastPoint.bias = 0;
lastPoint.bias = 0;
}
}
}
}
// Record the changed point into history
// Record the changed point into history
复制
已复制
复制
已复制
//
pointHistory[epoch] = lastPoint;
pointHistory[epoch] = lastPoint;
pointHistory.push(lastPoint);
if (_addr != address(0)) {
if (_addr != address(0)) {
// Schedule the slope changes (slope is going down)
// Schedule the slope changes (slope is going down)
// We subtract new_user_slope from [new_locked.end]
// We subtract new_user_slope from [new_locked.end]
// and add old_user_slope to [old_locked.end]
// and add old_user_slope to [old_locked.end]
if (_oldLocked.end > block.timestamp) {
if (_oldLocked.end > block.timestamp) {
// oldSlopeDelta was <something> - userOldPoint.slope, so we cancel that
// oldSlopeDelta was <something> - userOldPoint.slope, so we cancel that
oldSlopeDelta = oldSlopeDelta + userOldPoint.slope;
oldSlopeDelta = oldSlopeDelta + userOldPoint.slope;
if (_newLocked.end == _oldLocked.end) {
if (_newLocked.end == _oldLocked.end) {
oldSlopeDelta = oldSlopeDelta - userNewPoint.slope; // It was a new deposit, not extension
oldSlopeDelta = oldSlopeDelta - userNewPoint.slope; // It was a new deposit, not extension
}
}
slopeChanges[_oldLocked.end] = oldSlopeDelta;
slopeChanges[_oldLocked.end] = oldSlopeDelta;
}
}
if (_newLocked.end > block.timestamp) {
if (_newLocked.end > block.timestamp) {
if (_newLocked.end > _oldLocked.end) {
if (_newLocked.end > _oldLocked.end) {
newSlopeDelta = newSlopeDelta - userNewPoint.slope; // old slope disappeared at this point
newSlopeDelta = newSlopeDelta - userNewPoint.slope; // old slope disappeared at this point
slopeChanges[_newLocked.end] = newSlopeDelta;
slopeChanges[_newLocked.end] = newSlopeDelta;
}
}
// else: we recorded it already in oldSlopeDelta
// else: we recorded it already in oldSlopeDelta
}
}
}
}
}
}
复制
已复制
复制
已复制
/**
/// @notice
Public function to trigger global checkpoint
* @dev Deposits or creates a stake for a given address
* @param _addr User address to assign the stake
* @param _value Total units of StakingToken to lockup
* @param _unlockTime Time at which the stake should unlock
* @param _oldLocked Previous amount staked by this user
* @param _action See LockAction enum
*/
function _depositFor(
address _addr,
uint256 _value,
uint256 _unlockTime,
LockedBalance memory _oldLocked,
LockAction _action
) internal {
LockedBalance memory newLocked = LockedBalance({
amount: _oldLocked.amount,
end: _oldLocked.end
});
// Adding to existing lock, or if a lock is expired - creating a new one
newLocked.amount = newLocked.amount + SafeCast.toInt128(int256(_value));
if (_unlockTime != 0) {
newLocked.end = _unlockTime;
}
locked[_addr] = newLocked;
// Possibilities:
// Both _oldLocked.end could be current or expired (>/< block.timestamp)
// value == 0 (extend lock) or value > 0 (add to lock or extend lock)
// newLocked.end > block.timestamp (always)
_checkpoint(_addr, _oldLocked, newLocked);
if (_value != 0) {
stakingToken.safeTransferFrom(_addr, address(this), _value);
}
emit Deposit(_addr, _value, newLocked.end, _action, block.timestamp);
}
/**
* @dev
Public function to trigger global checkpoint
*/
function checkpoint() external {
function checkpoint() external {
LockedBalance memory empty;
LockedBalance memory empty;
_checkpoint(address(0), empty, empty);
_checkpoint(address(0), empty, empty);
}
}
复制
已复制
复制
已复制
/**
// See IVotingEscrow for documentation
* @dev Creates a new lock
* @param _value Total units of StakingToken to lockup
* @param _unlockTime Time at which the stake should unlock
*/
function createLock(uint256 _value, uint256 _unlockTime)
function createLock(uint256 _value, uint256 _unlockTime)
external
external
override
override
nonReentrant
nonReentrant
复制
已复制
复制
已复制
contractNotExpired
checkBlocklist
updateReward(msg.sender)
{
{
uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks
uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks
复制
已复制
复制
已复制
LockedBalance memory locked_ =
LockedBalance({
LockedBalance memory locked_ =
locked[msg.sender]
;
amount:
locked[msg.sender]
.amount,
// Validate inputs
end: locked[msg.sender].end
require(_value > 0, "
Only
non zero amount");
});
require(locked_.amount == 0, "
Lock exists");
require(unlock_time >= locked_.end, "Only increase lock end"); // from using quitLock, user should increaseAmount instead
require(_value > 0, "
Must stake
non zero amount");
require(unlock_time > block.timestamp, "
O
nly
future
lock end
");
require(locked_.amount == 0, "
Withdraw old tokens first");
require(unlock_time <=
block.timestamp + MAXTIME, "Exceeds maxtime");
// Update lock and voting power (checkpoint)
require(unlock_time > block.timestamp, "
Can o
nly
lock until time in the
future
");
locked_.amount += int128(int256(_value));
require(unlock_time <=
END, "Voting lock can be 1 year max (until recol)");
locked_.end = unlock_time;
locked_.delegated += int128(int256(_value));
_depositFor(msg.sender, _value, unlock_time, locked_, LockAction.CREATE_LOCK);
locked_.delegatee = msg.sender;
locked[msg.sender] = locked_;
_checkpoint(msg.sender, LockedBalance(0, 0, 0, address(0)), locked_);
// Deposit locked tokens
require(
token.transferFrom(msg.sender, address(this), _value),
"Transfer failed"
);
emit Deposit(
msg.sender,
_value,
unlock_time,
LockAction.CREATE,
block.timestamp
);
}
}
复制
已复制
复制
已复制
/**
// See IVotingEscrow for documentation
* @dev Increases amount of stake thats locked up & resets decay
// @dev A lock is active until both lock.amount==0 and lock.end<=block.timestamp
* @param _value Additional units of StakingToken to add to exiting stake
function increase
Amount(uint256 _value)
*/
function increaseLock
Amount(uint256 _value)
external
external
override
override
nonReentrant
nonReentrant
复制
已复制
复制
已复制
contractNotExpired
checkBlocklist
updateReward(msg.sender)
{
{
复制
已复制
复制
已复制
LockedBalance memory locked_ =
LockedBalance({
LockedBalance memory locked_ =
locked[msg.sender];
amount: locked[msg.sender].amount,
// Validate inputs
end: locked[msg.sender].end
require(_value > 0, "Only non zero amount");
});
require(locked_.amount > 0, "No lock");
require(locked_.end > block.timestamp, "Lock expired");
require(_value > 0, "Must stake non zero amount");
// Update lock
require(locked_.amount > 0, "
No existing
lock
found
");
address delegatee = locked_.delegatee;
require(locked_.end > block.timestamp, "
Cannot add to expired lock. Withdraw");
uint256 unlockTime = locked_.end;
LockAction action = LockAction.INCREASE_AMOUNT;
_depositFor(msg.sender, _value, 0, locked_, LockAction.INCREASE_LOCK_AMOUNT);
LockedBalance memory newLocked;
if (delegatee == msg.sender) {
// Undelegated lock
action = LockAction.INCREASE_AMOUNT_AND_DELEGATION;
newLocked = _copyLock(locked_);
newLocked.amount += int128(int256(_value));
newLocked.delegated += int128(int256(_value));
locked[msg.sender] = newLocked;
} else {
// Delegated lock, update sender's lock first
locked_.amount += int128(int256(_value));
locked[msg.sender] = locked_;
// Then, update delegatee's lock and voting power (checkpoint)
locked_ = locked[delegatee];
require(locked_.amount > 0, "
Delegatee has no
lock
");
require(locked_.end > block.timestamp, "
Delegatee lock expired");
newLocked = _copyLock(locked_);
newLocked.delegated += int128(int256(_value));
locked[delegatee] = newLocked;
emit Deposit(
delegatee,
_value,
newLocked.end,
LockAction.DELEGATE,
block.timestamp
);
}
// Checkpoint only for delegatee
_checkpoint(delegatee, locked_, newLocked);
// Deposit locked tokens
require(
token.transferFrom(msg.sender, address(this), _value),
"Transfer failed"
);
emit Deposit(msg.sender, _value, unlockTime, action, block.timestamp);
}
}
复制
已复制
复制
已复制
/**
// See IVotingEscrow for documentation
* @dev Increases length of lockup & resets decay
function increase
Unl
ock
Time
(uint256 _unlockTime)
* @param _unlockTime New unlocktime for lockup
*/
function increase
L
ock
Length
(uint256 _unlockTime)
external
external
override
override
nonReentrant
nonReentrant
复制
已复制
复制
已复制
contractNotExpired
checkBlocklist
updateReward(msg.sender)
{
{
复制
已复制
复制
已复制
LockedBalance memory locked_ =
LockedBalance({
LockedBalance memory locked_ =
locked[msg.sender]
;
amount: locked[msg.sender].amount,
end:
locked[msg.sender]
.end
})
;
uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks
uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks
复制
已复制
复制
已复制
// Validate inputs
require(locked_.amount > 0, "
Nothing is
lock
ed");
require(locked_.amount > 0, "
No
lock
");
require(locked_.end > block.timestamp, "Lock expired
");
require(unlock_time > locked_.end, "
O
nly increase lock
end
");
require(unlock_time > locked_.end, "
Can o
nly increase lock
WEEK
");
require(unlock_time <=
block.timestamp + MAXTIME, "Exceeds maxtime");
require(unlock_time <=
END, "Voting lock can be 1 year max (until recol)");
// Update lock
uint256 oldUnlockTime = locked_.end;
_depositFor(msg.sender, 0, unlock_time, locked_, LockAction.INCREASE_LOCK_TIME);
locked_.end = unlock_time;
}
locked[msg.sender] = locked_;
if (locked_.delegatee == msg.sender) {
/**
// Undelegated lock
* @dev Withdraws all the senders stake, providing lockup is over
require(oldUnlockTime > block.timestamp, "Lock expired");
*/
LockedBalance memory oldLocked = _copyLock(locked_);
function withdraw() external override {
oldLocked.end = unlock_time;
_withdraw(msg.sender
);
_checkpoint(msg.sender, oldLocked, locked_);
}
emit Deposit(
msg.sender,
0,
unlock_time,
LockAction.INCREASE_TIME,
block.timestamp
);
}
}
复制
已复制
复制
已复制
/**
// See IVotingEscrow for documentation
* @dev Withdraws a given users stake, providing the lockup has finished
function
withdraw(
) ex
ternal
override
nonReentrant
{
* @param _addr User for which to withdraw
LockedBalance memory
locked_
=
locked[msg.sender];
*/
// Validate inputs
function
_
withdraw(
address _addr) in
ternal
nonReentrant
updateReward(_addr)
{
require(
locked
_
.amount
> 0, "No lock"
);
LockedBalance memory
oldLock
=
LockedBalance({
require(
locked_.end <=
block.timestamp
, "
L
ock
not
expire
d
");
end: locked[_addr].end,
require(
locked_.delegatee == msg.sender
, "
Lock delegated
");
amount:
locked
[_addr]
.amount
// Update lock
}
);
uint256 value =
u
int256(
uint128(locked_
.amount)
);
require(
block.timestamp
>= oldLock.end || expired
, "
The l
ock
didn't
expire
");
LockedBalance memory
newLocked = _copyLock(locked_);
require(
oldLock.amount > 0
, "
Must have something to withdraw
");
newLocked.amount = 0;
newLocked.end = 0;
uint256 value =
SafeCast.toU
int256(
oldLock
.amount)
;
newLocked.delegated -= int128(int256(value));
newLocked.delegatee = address(0);
LockedBalance memory
currentLock = LockedBalance({ end: 0, amount: 0 });
locked[
msg.sender
] =
newLocked;
locked[
_addr
] =
currentLock;
newLocked.delegated = 0;
// oldLocked can have either expired <= timestamp or zero end
// oldLocked can have either expired <= timestamp or zero end
// currentLock has only 0 end
// currentLock has only 0 end
// Both can have >= 0 amount
// Both can have >= 0 amount
复制
已复制
复制
已复制
if (!expired) {
_checkpoint(
msg.sender, locked_, newLocked
);
_checkpoint(
_addr, oldLock, currentLock
);
// Send back deposited tokens
}
require(token.t
ransfer(
msg.sender
, value)
, "Transfer failed");
stakingToken.safeT
ransfer(
_addr
, value)
;
emit Withdraw(
msg.sender
, value,
LockAction.WITHDRAW,
block.timestamp);
emit Withdraw(
_addr
, value,
block.timestamp);
}
/**
* @dev Withdraws and consequently claims rewards for the sender
*/
function exit() external override {
_withdraw(msg.sender);
claimReward();
}
}
复制
已复制
复制
已复制
/**
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
* @dev Ejects a user from the reward allocation, given their lock has freshly expired.
/// DELEGATION ///
* Leave it to the user to withdraw and claim their rewards.
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
* @param _addr Address of the user
*/
function eject(address _addr) external override contractNotExpired lockupIsOver(_addr) {
_withdraw(_addr);
// solium-disable-next-line security/no-tx-origin
emit Ejected(_addr, tx.origin, block.timestamp);
}
复制
已复制
复制
已复制
/**
// See IVotingEscrow for documentation
* @dev Ends the contract, unlocking all stakes.
function
delegate(address _addr
)
* No more staking can happen. Only withdraw and Claim.
*/
function
expireContract(
)
external
external
override
override
复制
已复制
复制
已复制
onlyGovernor
nonReentrant
contractNotExpired
checkBlocklist
updateReward(address(0))
{
{
复制
已复制
复制
已复制
require(block.timestamp > periodFinish, "Period must be over");
LockedBalance memory locked_ = locked[msg.sender];
// Validate inputs
expired = true;
require(!IBlocklist(blocklist).isBlocked(_addr), "Blocked contract");
require(locked_.amount > 0, "No lock");
emit Expired();
require(locked_.delegatee != _addr, "Already delegated");
}
// Update locks
int128 value = locked_.amount;
/***************************************
address delegatee = locked_.delegatee;
GETTERS
LockedBalance memory fromLocked;
****************************************/
LockedBalance memory toLocked;
locked_.delegatee = _addr;
/** @dev Floors a timestamp to the nearest weekly increment */
if (delegatee == msg.sender) {
function _floorToWeek(uint256 _t) internal pure returns (uint256) {
// Delegate
return (_t / WEEK) * WEEK;
fromLocked = locked_;
}
toLocked = locked[_addr];
} else if (_addr == msg.sender) {
/**
// Undelegate
* @dev Uses binarysearch to find the most recent point history preceeding block
fromLocked = locked[delegatee];
* @param _block Find the most recent point history before this block
toLocked = locked_;
* @param _maxEpoch Do not search pointHistories past this index
} else {
*/
// Re-delegate
function _findBlockEpoch(uint256 _block, uint256 _maxEpoch) internal view returns (uint256) {
fromLocked = locked[delegatee];
// Binary search
toLocked = locked
[_addr];
uint256 min = 0;
// Update owner lock if not involved in delegation
uint256 max = _maxEpoch;
locked[msg.sender] = locked_;
// Will be always enough for 128-bit numbers
for (uint256 i = 0; i < 128; i++) {
if (min >= max) break;
uint256 mid = (min + max + 1) / 2;
if (pointHistory[mid].blk <= _block) {
min = mid;
} else {
max = mid - 1;
}
}
return min;
}
/**
* @dev Uses binarysearch to find the most recent user point history preceeding block
* @param _addr User for which to search
* @param _block Find the most recent point history before this block
*/
function _findUserBlockEpoch(address _addr, uint256 _block) internal view returns (uint256) {
uint256 min = 0;
uint256 max = userPointEpoch
[_addr];
for (uint256 i = 0; i < 128; i++) {
if (min >= max) {
break;
}
uint256 mid = (min + max + 1) / 2;
if (userPointHistory[_addr][mid].blk <= _block) {
min = mid;
} else {
max = mid - 1;
}
}
}
复制
已复制
复制
已复制
return min;
require(toLocked.amount > 0, "Delegatee has no lock");
require(toLocked.end > block.timestamp, "Delegatee lock expired");
require(toLocked.end >= fromLocked.end, "Only delegate to longer lock");
_delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE);
_delegate(_addr, toLocked, value, LockAction.DELEGATE);
}
}
复制
已复制
复制
已复制
/
**
/
/ Delegates from/to lock and
voting
power
* @dev Gets curent user
voting
weight (aka effectiveStake)
function _delegate(
* @param _owner User for which to return the balance
address addr,
* @return uint256
Balance
of user
Locked
Balance
memory _locked,
*/
int128 value,
function balanceOf(address _owner) public view override returns (uint256) {
LockAction action
uint256 epoch
=
userPointEpoch[_owner]
;
) internal {
if (
epoch
==
0
) {
LockedBalance memory newLocked
=
_copyLock(_locked)
;
return 0;
if (
action
==
LockAction.DELEGATE
) {
newLocked.delegated += value;
emit Deposit(
addr,
uint256(int256(value)),
newLocked.end,
action,
block.timestamp
);
} else {
newLocked.delegated -= value;
emit Withdraw(
addr,
uint256(int256(value)),
action,
block.timestamp
);
}
}
复制
已复制
复制
已复制
Point memory lastPoint = userPointHistory[_owner][epoch];
locked[addr] = newLocked;
lastPoint.bias =
if (
newLocked.amount >
0) {
lastPoint.bias -
// Only if lock (from lock) hasn't been withdrawn/quitted
(lastPoint.slope * SafeCast.toInt128(int256(block.timestamp - lastPoint.ts)));
_checkpoint(addr, _locked, newLocked);
if (
lastPoint.bias <
0) {
lastPoint.bias = 0;
}
}
复制
已复制
复制
已复制
return SafeCast.toUint256(lastPoint.bias);
}
}
复制
已复制
复制
已复制
/**
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
* @dev Gets a users votingWeight at a given blockNumber
/// QUIT LOCK ///
* @param _owner User for which to return the balance
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
* @param _blockNumber Block at which to calculate balance
* @return uint256 Balance of user
*/
function balanceOfAt(address _owner, uint256 _blockNumber)
public
view
override
returns (uint256)
{
require(_blockNumber <= block.number, "Must pass block number in the past");
// Get most recent user Point to block
uint256 userEpoch = _findUserBlockEpoch(_owner, _blockNumber);
if (userEpoch == 0) {
return 0;
}
Point memory upoint = userPointHistory[_owner][userEpoch];
// Get most recent global Point to block
uint256 maxEpoch = globalEpoch;
uint256 epoch = _findBlockEpoch(_blockNumber, maxEpoch);
Point memory point0 = pointHistory[epoch];
复制
已复制
复制
已复制
// Calculate delta (block & time) between user Point and target block
// See IVotingEscrow for documentation
// Allowing us to calculate the average seconds per block between
function quitLock() external override nonReentrant {
// the two points
LockedBalance memory locked_ = locked[msg.sender];
uint256 dB
lock
= 0
;
// Validate inputs
uint256 dTime = 0
;
require(locked_.amount > 0, "No
lock
")
;
if (epoch < maxEpoch) {
require(locked_.end > block.timestamp, "Lock expired")
;
Point memory point1
=
pointHistory[epoch + 1]
;
require(locked_.delegatee == msg.sender, "Lock delegated");
dBlock
=
point1.blk - point0.blk
;
// Update lock
dTime = po
int1
.ts - point0.ts
;
uint256 value
=
uint256(uint128(locked_.amount))
;
} else {
LockedBalance memory newLocked
=
_copyLock(locked_);
dBlock = b
lock
.number - point0.blk
;
newLocked.amount = 0
;
dTime = block.timestamp - point0.ts
;
newLocked.delegated -=
int1
28(int256(value))
;
}
newLocked.delegatee = address(0);
//
(Deterministically) Estimate
the
time at which block _blockNumber was mined
lock
ed[msg.sender] = newLocked
;
uint256 blockTime = point0.ts;
newLocked.end = 0
;
if (dBlock !
= 0
) {
newLocked.delegated = 0;
blockTime = blockTime + ((dTime * (_blockNumber - point0.blk)) / dBlock);
//
oldLocked can have ei
the
r expired <= timestamp or zero end
}
// currentLock has only 0 end
// Current Bias = most recent bias - (slope * time since update)
// Both can have >
= 0
amount
upoint.bias =
_checkpoint(msg.sender, locked_, newLocked);
upoint.bias -
// apply penalty
(upoint.slope * SafeCast.toInt128(int256(blockTime - upoint.ts)));
uint256 penaltyRate = _calculatePenaltyRate(locked_.end);
if (upoint.bias >= 0) {
uint256 penaltyAmount = (value * penaltyRate) / 10**18; // quitlock_penalty is in 18 decimals precision
return SafeCast.toUint256(upoint.bias);
penaltyAccumulated += penaltyAmount;
} else {
uint256 remainingAmount = value - penaltyAmount;
return 0;
// Send back remaining tokens
}
require(token.transfer(msg.sender, remainingAmount), "Transfer failed");
emit Withdraw(msg.sender, value, LockAction.QUIT, block.timestamp);
}
}
复制
已复制
复制
已复制
/
**
/
/
Calcula
te penalty
* @dev
Calcula
已保存差异
原始文本
打开文件
// SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.6; import { IBasicToken } from "../shared/IBasicToken.sol"; import { IIncentivisedVotingLockup } from "../interfaces/IIncentivisedVotingLockup.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import { RewardsDistributionRecipient } from "../rewards/RewardsDistributionRecipient.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { StableMath } from "../shared/StableMath.sol"; import { Root } from "../shared/Root.sol"; /** * @title IncentivisedVotingLockup * @author Voting Weight tracking & Decay * -> Curve Finance (MIT) - forked & ported to Solidity * -> https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy * osolmaz - Research & Reward distributions * alsco77 - Solidity implementation * @notice Lockup MTA, receive vMTA (voting weight that decays over time), and earn * rewards based on staticWeight * @dev Supports: * 1) Tracking MTA Locked up (LockedBalance) * 2) Pull Based Reward allocations based on Lockup (Static Balance) * 3) Decaying voting weight lookup through CheckpointedERC20 (balanceOf) * 4) Ejecting fully decayed participants from reward allocation (eject) * 5) Migration of points to v2 (used as multiplier in future) ***** (rewardsPaid) * 6) Closure of contract (expire) */ contract IncentivisedVotingLockup is IIncentivisedVotingLockup, ReentrancyGuard, RewardsDistributionRecipient { using StableMath for uint256; using SafeERC20 for IERC20; /** Shared Events */ event Deposit( address indexed provider, uint256 value, uint256 locktime, LockAction indexed action, uint256 ts ); event Withdraw(address indexed provider, uint256 value, uint256 ts); event Ejected(address indexed ejected, address ejector, uint256 ts); event Expired(); event RewardAdded(uint256 reward); event RewardPaid(address indexed user, uint256 reward); /** Shared Globals */ IERC20 public stakingToken; uint256 private constant WEEK = 7 days; uint256 public constant MAXTIME = 365 days; uint256 public END; bool public expired = false; /** Lockup */ uint256 public globalEpoch; Point[] public pointHistory; mapping(address => Point[]) public userPointHistory; mapping(address => uint256) public userPointEpoch; mapping(uint256 => int128) public slopeChanges; mapping(address => LockedBalance) public locked; // Voting token - Checkpointed view only ERC20 string public name; string public symbol; uint256 public decimals = 18; /** Rewards */ // Updated upon admin deposit uint256 public periodFinish = 0; uint256 public rewardRate = 0; // Globals updated per stake/deposit/withdrawal uint256 public totalStaticWeight = 0; uint256 public lastUpdateTime = 0; uint256 public rewardPerTokenStored = 0; // Per user storage updated per stake/deposit/withdrawal mapping(address => uint256) public userRewardPerTokenPaid; mapping(address => uint256) public rewards; mapping(address => uint256) public rewardsPaid; /** Structs */ struct Point { int128 bias; int128 slope; uint256 ts; uint256 blk; } struct LockedBalance { int128 amount; uint256 end; } enum LockAction { CREATE_LOCK, INCREASE_LOCK_AMOUNT, INCREASE_LOCK_TIME } constructor( address _stakingToken, string memory _name, string memory _symbol, address _nexus, address _rewardsDistributor ) RewardsDistributionRecipient(_nexus, _rewardsDistributor) { stakingToken = IERC20(_stakingToken); Point memory init = Point({ bias: int128(0), slope: int128(0), ts: block.timestamp, blk: block.number }); pointHistory.push(init); decimals = IBasicToken(_stakingToken).decimals(); require(decimals <= 18, "Cannot have more than 18 decimals"); name = _name; symbol = _symbol; END = block.timestamp + MAXTIME; } /** @dev Modifier to ensure contract has not yet expired */ modifier contractNotExpired() { require(!expired, "Contract is expired"); _; } /** * @dev Validates that the user has an expired lock && they still have capacity to earn * @param _addr User address to check */ modifier lockupIsOver(address _addr) { LockedBalance memory userLock = locked[_addr]; require(userLock.amount > 0 && block.timestamp >= userLock.end, "Users lock didn't expire"); require(staticBalanceOf(_addr) > 0, "User must have existing bias"); _; } /*************************************** LOCKUP - GETTERS ****************************************/ /** * @dev Gets the last available user point * @param _addr User address * @return bias i.e. y * @return slope i.e. linear gradient * @return ts i.e. time point was logged */ function getLastUserPoint(address _addr) external view override returns ( int128 bias, int128 slope, uint256 ts ) { uint256 uepoch = userPointEpoch[_addr]; if (uepoch == 0) { return (0, 0, 0); } Point memory point = userPointHistory[_addr][uepoch]; return (point.bias, point.slope, point.ts); } /*************************************** LOCKUP ****************************************/ /** * @dev Records a checkpoint of both individual and global slope * @param _addr User address, or address(0) for only global * @param _oldLocked Old amount that user had locked, or null for global * @param _newLocked new amount that user has locked, or null for global */ function _checkpoint( address _addr, LockedBalance memory _oldLocked, LockedBalance memory _newLocked ) internal { Point memory userOldPoint; Point memory userNewPoint; int128 oldSlopeDelta = 0; int128 newSlopeDelta = 0; uint256 epoch = globalEpoch; if (_addr != address(0)) { // Calculate slopes and biases // Kept at zero when they have to if (_oldLocked.end > block.timestamp && _oldLocked.amount > 0) { userOldPoint.slope = _oldLocked.amount / SafeCast.toInt128(int256(MAXTIME)); userOldPoint.bias = userOldPoint.slope * SafeCast.toInt128(int256(_oldLocked.end - block.timestamp)); } if (_newLocked.end > block.timestamp && _newLocked.amount > 0) { userNewPoint.slope = _newLocked.amount / SafeCast.toInt128(int256(MAXTIME)); userNewPoint.bias = userNewPoint.slope * SafeCast.toInt128(int256(_newLocked.end - block.timestamp)); } // Moved from bottom final if statement to resolve stack too deep err // start { // Now handle user history uint256 uEpoch = userPointEpoch[_addr]; if (uEpoch == 0) { userPointHistory[_addr].push(userOldPoint); } // track the total static weight uint256 newStatic = _staticBalance(userNewPoint.slope, block.timestamp, _newLocked.end); uint256 additiveStaticWeight = totalStaticWeight + newStatic; if (uEpoch > 0) { uint256 oldStatic = _staticBalance( userPointHistory[_addr][uEpoch].slope, userPointHistory[_addr][uEpoch].ts, _oldLocked.end ); additiveStaticWeight = additiveStaticWeight - oldStatic; } totalStaticWeight = additiveStaticWeight; userPointEpoch[_addr] = uEpoch + 1; userNewPoint.ts = block.timestamp; userNewPoint.blk = block.number; userPointHistory[_addr].push(userNewPoint); // } end // 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 oldSlopeDelta = slopeChanges[_oldLocked.end]; if (_newLocked.end != 0) { if (_newLocked.end == _oldLocked.end) { newSlopeDelta = oldSlopeDelta; } else { newSlopeDelta = slopeChanges[_newLocked.end]; } } } Point memory lastPoint = Point({ bias: 0, slope: 0, ts: block.timestamp, blk: block.number }); if (epoch > 0) { lastPoint = pointHistory[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 = Point({ bias: 0, slope: 0, ts: lastPoint.ts, blk: lastPoint.blk }); uint256 blockSlope = 0; // dblock/dt if (block.timestamp > lastPoint.ts) { blockSlope = StableMath.scaleInteger(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 iterativeTime = _floorToWeek(lastCheckpoint); for (uint256 i = 0; 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 iterativeTime = iterativeTime + WEEK; int128 dSlope = 0; if (iterativeTime > block.timestamp) { iterativeTime = block.timestamp; } else { dSlope = slopeChanges[iterativeTime]; } int128 biasDelta = lastPoint.slope * SafeCast.toInt128(int256((iterativeTime - lastCheckpoint))); lastPoint.bias = lastPoint.bias - biasDelta; lastPoint.slope = lastPoint.slope + dSlope; // This can happen if (lastPoint.bias < 0) { lastPoint.bias = 0; } // This cannot happen - just in case if (lastPoint.slope < 0) { lastPoint.slope = 0; } lastCheckpoint = iterativeTime; lastPoint.ts = iterativeTime; lastPoint.blk = initialLastPoint.blk + blockSlope.mulTruncate(iterativeTime - initialLastPoint.ts); // when epoch is incremented, we either push here or after slopes updated below epoch = epoch + 1; if (iterativeTime == block.timestamp) { lastPoint.blk = block.number; break; } else { // pointHistory[epoch] = lastPoint; pointHistory.push(lastPoint); } } globalEpoch = epoch; // Now pointHistory 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 = lastPoint.slope + userNewPoint.slope - userOldPoint.slope; lastPoint.bias = lastPoint.bias + userNewPoint.bias - userOldPoint.bias; if (lastPoint.slope < 0) { lastPoint.slope = 0; } if (lastPoint.bias < 0) { lastPoint.bias = 0; } } // Record the changed point into history // pointHistory[epoch] = lastPoint; pointHistory.push(lastPoint); if (_addr != address(0)) { // Schedule the slope changes (slope is going down) // We subtract new_user_slope from [new_locked.end] // and add old_user_slope to [old_locked.end] if (_oldLocked.end > block.timestamp) { // oldSlopeDelta was <something> - userOldPoint.slope, so we cancel that oldSlopeDelta = oldSlopeDelta + userOldPoint.slope; if (_newLocked.end == _oldLocked.end) { oldSlopeDelta = oldSlopeDelta - userNewPoint.slope; // It was a new deposit, not extension } slopeChanges[_oldLocked.end] = oldSlopeDelta; } if (_newLocked.end > block.timestamp) { if (_newLocked.end > _oldLocked.end) { newSlopeDelta = newSlopeDelta - userNewPoint.slope; // old slope disappeared at this point slopeChanges[_newLocked.end] = newSlopeDelta; } // else: we recorded it already in oldSlopeDelta } } } /** * @dev Deposits or creates a stake for a given address * @param _addr User address to assign the stake * @param _value Total units of StakingToken to lockup * @param _unlockTime Time at which the stake should unlock * @param _oldLocked Previous amount staked by this user * @param _action See LockAction enum */ function _depositFor( address _addr, uint256 _value, uint256 _unlockTime, LockedBalance memory _oldLocked, LockAction _action ) internal { LockedBalance memory newLocked = LockedBalance({ amount: _oldLocked.amount, end: _oldLocked.end }); // Adding to existing lock, or if a lock is expired - creating a new one newLocked.amount = newLocked.amount + SafeCast.toInt128(int256(_value)); if (_unlockTime != 0) { newLocked.end = _unlockTime; } locked[_addr] = newLocked; // Possibilities: // Both _oldLocked.end could be current or expired (>/< block.timestamp) // value == 0 (extend lock) or value > 0 (add to lock or extend lock) // newLocked.end > block.timestamp (always) _checkpoint(_addr, _oldLocked, newLocked); if (_value != 0) { stakingToken.safeTransferFrom(_addr, address(this), _value); } emit Deposit(_addr, _value, newLocked.end, _action, block.timestamp); } /** * @dev Public function to trigger global checkpoint */ function checkpoint() external { LockedBalance memory empty; _checkpoint(address(0), empty, empty); } /** * @dev Creates a new lock * @param _value Total units of StakingToken to lockup * @param _unlockTime Time at which the stake should unlock */ function createLock(uint256 _value, uint256 _unlockTime) external override nonReentrant contractNotExpired updateReward(msg.sender) { uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks LockedBalance memory locked_ = LockedBalance({ amount: locked[msg.sender].amount, end: locked[msg.sender].end }); require(_value > 0, "Must stake non zero amount"); require(locked_.amount == 0, "Withdraw old tokens first"); require(unlock_time > block.timestamp, "Can only lock until time in the future"); require(unlock_time <= END, "Voting lock can be 1 year max (until recol)"); _depositFor(msg.sender, _value, unlock_time, locked_, LockAction.CREATE_LOCK); } /** * @dev Increases amount of stake thats locked up & resets decay * @param _value Additional units of StakingToken to add to exiting stake */ function increaseLockAmount(uint256 _value) external override nonReentrant contractNotExpired updateReward(msg.sender) { LockedBalance memory locked_ = LockedBalance({ amount: locked[msg.sender].amount, end: locked[msg.sender].end }); require(_value > 0, "Must stake non zero amount"); require(locked_.amount > 0, "No existing lock found"); require(locked_.end > block.timestamp, "Cannot add to expired lock. Withdraw"); _depositFor(msg.sender, _value, 0, locked_, LockAction.INCREASE_LOCK_AMOUNT); } /** * @dev Increases length of lockup & resets decay * @param _unlockTime New unlocktime for lockup */ function increaseLockLength(uint256 _unlockTime) external override nonReentrant contractNotExpired updateReward(msg.sender) { LockedBalance memory locked_ = LockedBalance({ amount: locked[msg.sender].amount, end: locked[msg.sender].end }); uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks require(locked_.amount > 0, "Nothing is locked"); require(locked_.end > block.timestamp, "Lock expired"); require(unlock_time > locked_.end, "Can only increase lock WEEK"); require(unlock_time <= END, "Voting lock can be 1 year max (until recol)"); _depositFor(msg.sender, 0, unlock_time, locked_, LockAction.INCREASE_LOCK_TIME); } /** * @dev Withdraws all the senders stake, providing lockup is over */ function withdraw() external override { _withdraw(msg.sender); } /** * @dev Withdraws a given users stake, providing the lockup has finished * @param _addr User for which to withdraw */ function _withdraw(address _addr) internal nonReentrant updateReward(_addr) { LockedBalance memory oldLock = LockedBalance({ end: locked[_addr].end, amount: locked[_addr].amount }); require(block.timestamp >= oldLock.end || expired, "The lock didn't expire"); require(oldLock.amount > 0, "Must have something to withdraw"); uint256 value = SafeCast.toUint256(oldLock.amount); LockedBalance memory currentLock = LockedBalance({ end: 0, amount: 0 }); locked[_addr] = currentLock; // oldLocked can have either expired <= timestamp or zero end // currentLock has only 0 end // Both can have >= 0 amount if (!expired) { _checkpoint(_addr, oldLock, currentLock); } stakingToken.safeTransfer(_addr, value); emit Withdraw(_addr, value, block.timestamp); } /** * @dev Withdraws and consequently claims rewards for the sender */ function exit() external override { _withdraw(msg.sender); claimReward(); } /** * @dev Ejects a user from the reward allocation, given their lock has freshly expired. * Leave it to the user to withdraw and claim their rewards. * @param _addr Address of the user */ function eject(address _addr) external override contractNotExpired lockupIsOver(_addr) { _withdraw(_addr); // solium-disable-next-line security/no-tx-origin emit Ejected(_addr, tx.origin, block.timestamp); } /** * @dev Ends the contract, unlocking all stakes. * No more staking can happen. Only withdraw and Claim. */ function expireContract() external override onlyGovernor contractNotExpired updateReward(address(0)) { require(block.timestamp > periodFinish, "Period must be over"); expired = true; emit Expired(); } /*************************************** GETTERS ****************************************/ /** @dev Floors a timestamp to the nearest weekly increment */ function _floorToWeek(uint256 _t) internal pure returns (uint256) { return (_t / WEEK) * WEEK; } /** * @dev Uses binarysearch to find the most recent point history preceeding block * @param _block Find the most recent point history before this block * @param _maxEpoch Do not search pointHistories past this index */ function _findBlockEpoch(uint256 _block, uint256 _maxEpoch) internal view returns (uint256) { // Binary search uint256 min = 0; uint256 max = _maxEpoch; // Will be always enough for 128-bit numbers for (uint256 i = 0; i < 128; i++) { if (min >= max) break; uint256 mid = (min + max + 1) / 2; if (pointHistory[mid].blk <= _block) { min = mid; } else { max = mid - 1; } } return min; } /** * @dev Uses binarysearch to find the most recent user point history preceeding block * @param _addr User for which to search * @param _block Find the most recent point history before this block */ function _findUserBlockEpoch(address _addr, uint256 _block) internal view returns (uint256) { uint256 min = 0; uint256 max = userPointEpoch[_addr]; for (uint256 i = 0; i < 128; i++) { if (min >= max) { break; } uint256 mid = (min + max + 1) / 2; if (userPointHistory[_addr][mid].blk <= _block) { min = mid; } else { max = mid - 1; } } return min; } /** * @dev Gets curent user voting weight (aka effectiveStake) * @param _owner User for which to return the balance * @return uint256 Balance of user */ function balanceOf(address _owner) public view override returns (uint256) { uint256 epoch = userPointEpoch[_owner]; if (epoch == 0) { return 0; } Point memory lastPoint = userPointHistory[_owner][epoch]; lastPoint.bias = lastPoint.bias - (lastPoint.slope * SafeCast.toInt128(int256(block.timestamp - lastPoint.ts))); if (lastPoint.bias < 0) { lastPoint.bias = 0; } return SafeCast.toUint256(lastPoint.bias); } /** * @dev Gets a users votingWeight at a given blockNumber * @param _owner User for which to return the balance * @param _blockNumber Block at which to calculate balance * @return uint256 Balance of user */ function balanceOfAt(address _owner, uint256 _blockNumber) public view override returns (uint256) { require(_blockNumber <= block.number, "Must pass block number in the past"); // Get most recent user Point to block uint256 userEpoch = _findUserBlockEpoch(_owner, _blockNumber); if (userEpoch == 0) { return 0; } Point memory upoint = userPointHistory[_owner][userEpoch]; // Get most recent global Point to block uint256 maxEpoch = globalEpoch; uint256 epoch = _findBlockEpoch(_blockNumber, maxEpoch); Point memory point0 = pointHistory[epoch]; // Calculate delta (block & time) between user Point and target block // Allowing us to calculate the average seconds per block between // the two points uint256 dBlock = 0; uint256 dTime = 0; if (epoch < maxEpoch) { Point memory point1 = pointHistory[epoch + 1]; dBlock = point1.blk - point0.blk; dTime = point1.ts - point0.ts; } else { dBlock = block.number - point0.blk; dTime = block.timestamp - point0.ts; } // (Deterministically) Estimate the time at which block _blockNumber was mined uint256 blockTime = point0.ts; if (dBlock != 0) { blockTime = blockTime + ((dTime * (_blockNumber - point0.blk)) / dBlock); } // Current Bias = most recent bias - (slope * time since update) upoint.bias = upoint.bias - (upoint.slope * SafeCast.toInt128(int256(blockTime - upoint.ts))); if (upoint.bias >= 0) { return SafeCast.toUint256(upoint.bias); } else { return 0; } } /** * @dev Calculates total supply of votingWeight at a given time _t * @param _point Most recent point before time _t * @param _t Time at which to calculate supply * @return totalSupply at given point in time */ function _supplyAt(Point memory _point, uint256 _t) internal view returns (uint256) { Point memory lastPoint = _point; // Floor the timestamp to weekly interval uint256 iterativeTime = _floorToWeek(lastPoint.ts); // Iterate through all weeks between _point & _t to account for slope changes for (uint256 i = 0; i < 255; i++) { iterativeTime = iterativeTime + WEEK; int128 dSlope = 0; // If week end is after timestamp, then truncate & leave dSlope to 0 if (iterativeTime > _t) { iterativeTime = _t; } // else get most recent slope change else { dSlope = slopeChanges[iterativeTime]; } lastPoint.bias = lastPoint.bias - (lastPoint.slope * SafeCast.toInt128(int256(iterativeTime - lastPoint.ts))); if (iterativeTime == _t) { break; } lastPoint.slope = lastPoint.slope + dSlope; lastPoint.ts = iterativeTime; } if (lastPoint.bias < 0) { lastPoint.bias = 0; } return SafeCast.toUint256(lastPoint.bias); } /** * @dev Calculates current total supply of votingWeight * @return totalSupply of voting token weight */ function totalSupply() public view override returns (uint256) { uint256 epoch_ = globalEpoch; Point memory lastPoint = pointHistory[epoch_]; return _supplyAt(lastPoint, block.timestamp); } /** * @dev Calculates total supply of votingWeight at a given blockNumber * @param _blockNumber Block number at which to calculate total supply * @return totalSupply of voting token weight at the given blockNumber */ function totalSupplyAt(uint256 _blockNumber) public view override returns (uint256) { require(_blockNumber <= block.number, "Must pass block number in the past"); uint256 epoch = globalEpoch; uint256 targetEpoch = _findBlockEpoch(_blockNumber, epoch); Point memory point = pointHistory[targetEpoch]; // If point.blk > _blockNumber that means we got the initial epoch & contract did not yet exist if (point.blk > _blockNumber) { return 0; } uint256 dTime = 0; if (targetEpoch < epoch) { Point memory pointNext = pointHistory[targetEpoch + 1]; if (point.blk != pointNext.blk) { dTime = ((_blockNumber - point.blk) * (pointNext.ts - point.ts)) / (pointNext.blk - point.blk); } } else if (point.blk != block.number) { dTime = ((_blockNumber - point.blk) * (block.timestamp - point.ts)) / (block.number - point.blk); } // Now dTime contains info on how far are we beyond point return _supplyAt(point, point.ts + dTime); } /*************************************** REWARDS ****************************************/ /** @dev Updates the reward for a given address, before executing function */ modifier updateReward(address _account) { // Setting of global vars uint256 newRewardPerToken = rewardPerToken(); // If statement protects against loss in initialisation case if (newRewardPerToken > 0) { rewardPerTokenStored = newRewardPerToken; lastUpdateTime = lastTimeRewardApplicable(); // Setting of personal vars based on new globals if (_account != address(0)) { rewards[_account] = earned(_account); userRewardPerTokenPaid[_account] = newRewardPerToken; } } _; } /** * @dev Claims outstanding rewards for the sender. * First updates outstanding reward allocation and then transfers. */ function claimReward() public override updateReward(msg.sender) { uint256 reward = rewards[msg.sender]; if (reward > 0) { rewards[msg.sender] = 0; stakingToken.safeTransfer(msg.sender, reward); rewardsPaid[msg.sender] = rewardsPaid[msg.sender] + reward; emit RewardPaid(msg.sender, reward); } } /*************************************** REWARDS - GETTERS ****************************************/ /** * @dev Gets the most recent Static Balance (bias) for a user * @param _addr User for which to retrieve static balance * @return uint256 balance */ function staticBalanceOf(address _addr) public view returns (uint256) { uint256 uepoch = userPointEpoch[_addr]; if (uepoch == 0 || userPointHistory[_addr][uepoch].bias == 0) { return 0; } return _staticBalance( userPointHistory[_addr][uepoch].slope, userPointHistory[_addr][uepoch].ts, locked[_addr].end ); } function _staticBalance( int128 _slope, uint256 _startTime, uint256 _endTime ) internal pure returns (uint256) { if (_startTime > _endTime) return 0; // get lockup length (end - point.ts) uint256 lockupLength = _endTime - _startTime; // s = amount * sqrt(length) uint256 s = SafeCast.toUint256(_slope * 10000) * Root.sqrt(lockupLength); return s; } /** * @dev Gets the RewardsToken */ function getRewardToken() external view override returns (IERC20) { return stakingToken; } /** * @dev Gets the duration of the rewards period */ function getDuration() external pure returns (uint256) { return WEEK; } /** * @dev Gets the last applicable timestamp for this reward period */ function lastTimeRewardApplicable() public view returns (uint256) { return StableMath.min(block.timestamp, periodFinish); } /** * @dev Calculates the amount of unclaimed rewards per token since last update, * and sums with stored to give the new cumulative reward per token * @return 'Reward' per staked token */ function rewardPerToken() public view returns (uint256) { // If there is no StakingToken liquidity, avoid div(0) uint256 totalStatic = totalStaticWeight; if (totalStatic == 0) { return rewardPerTokenStored; } // new reward units to distribute = rewardRate * timeSinceLastUpdate uint256 rewardUnitsToDistribute = rewardRate * (lastTimeRewardApplicable() - lastUpdateTime); // new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(totalStatic); // return summed rate return rewardPerTokenStored + unitsToDistributePerToken; } /** * @dev Calculates the amount of unclaimed rewards a user has earned * @param _addr User address * @return Total reward amount earned */ function earned(address _addr) public view override returns (uint256) { // current rate per token - rate user previously received uint256 userRewardDelta = rewardPerToken() - userRewardPerTokenPaid[_addr]; // new reward = staked tokens * difference in rate uint256 userNewReward = staticBalanceOf(_addr).mulTruncate(userRewardDelta); // add to previous rewards return rewards[_addr] + userNewReward; } /*************************************** REWARDS - ADMIN ****************************************/ /** * @dev Notifies the contract that new rewards have been added. * Calculates an updated rewardRate based on the rewards in period. * @param _reward Units of RewardToken that have been added to the pool */ function notifyRewardAmount(uint256 _reward) external override onlyRewardsDistributor contractNotExpired updateReward(address(0)) { uint256 currentTime = block.timestamp; // If previous period over, reset rewardRate if (currentTime >= periodFinish) { rewardRate = _reward / WEEK; } // If additional reward to existing period, calc sum else { uint256 remaining = periodFinish - currentTime; uint256 leftover = remaining * rewardRate; rewardRate = (_reward + leftover) / WEEK; } lastUpdateTime = currentTime; periodFinish = currentTime + WEEK; emit RewardAdded(_reward); } }
更改后文本
打开文件
// SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.3; import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import { IERC20 } from "./interfaces/IERC20.sol"; import { IVotingEscrow } from "./interfaces/IVotingEscrow.sol"; import { IBlocklist } from "./interfaces/IBlocklist.sol"; /// @title VotingEscrow /// @author Curve Finance (MIT) - original concept and implementation in Vyper /// (see https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy) /// mStable (AGPL) - forking Curve's Vyper contract and porting to Solidity /// (see https://github.com/mstable/mStable-contracts/blob/master/contracts/governance/IncentivisedVotingLockup.sol) /// FIAT DAO (AGPL) - this version /// @notice Plain Curve VotingEscrow mechanics with following adjustments: /// 1) Delegation of lock and voting power /// 2) Quit an existing lock and pay a penalty /// 3) Whitelisting of SmartWallets outside the VotingEscrow /// 4) Reduced pointHistory array size and, as a result, lifetime of the contract /// 5) Removed public deposit_for and Aragon compatibility (no use case) contract VotingEscrow is IVotingEscrow, ReentrancyGuard { // Shared Events event Deposit( address indexed provider, uint256 value, uint256 locktime, LockAction indexed action, uint256 ts ); event Withdraw( address indexed provider, uint256 value, LockAction indexed action, uint256 ts ); event TransferOwnership(address owner); event UpdateBlocklist(address blocklist); event UpdatePenaltyRecipient(address recipient); event CollectPenalty(uint256 amount, address recipient); event Unlock(); // Shared global state IERC20 public token; uint256 public constant WEEK = 7 days; uint256 public constant MAXTIME = 365 days; uint256 public constant MULTIPLIER = 10**18; address public owner; address public penaltyRecipient; // receives collected penalty payments uint256 public maxPenalty = 10**18; // penalty for quitters with MAXTIME remaining lock uint256 public penaltyAccumulated; // accumulated and unwithdrawn penalty payments address public blocklist; // Lock state uint256 public globalEpoch; Point[1000000000000000000] public pointHistory; // 1e9 * userPointHistory-length, so sufficient for 1e9 users mapping(address => Point[1000000000]) public userPointHistory; mapping(address => uint256) public userPointEpoch; mapping(uint256 => int128) public slopeChanges; mapping(address => LockedBalance) public locked; // Voting token string public name; string public symbol; uint256 public decimals = 18; // Structs struct Point { int128 bias; int128 slope; uint256 ts; uint256 blk; } struct LockedBalance { int128 amount; uint256 end; int128 delegated; address delegatee; } // Miscellaneous enum LockAction { CREATE, INCREASE_AMOUNT, INCREASE_AMOUNT_AND_DELEGATION, INCREASE_TIME, WITHDRAW, QUIT, DELEGATE, UNDELEGATE } /// @notice Initializes state /// @param _owner The owner is able to update `owner`, `penaltyRecipient` and `penaltyRate` /// @param _penaltyRecipient The recipient of penalty paid by lock quitters /// @param _token The token locked in order to obtain voting power /// @param _name The name of the voting token /// @param _symbol The symbol of the voting token constructor( address _owner, address _penaltyRecipient, address _token, string memory _name, string memory _symbol ) { token = IERC20(_token); pointHistory[0] = Point({ bias: int128(0), slope: int128(0), ts: block.timestamp, blk: block.number }); decimals = IERC20(_token).decimals(); require(decimals <= 18, "Exceeds max decimals"); name = _name; symbol = _symbol; owner = _owner; penaltyRecipient = _penaltyRecipient; } modifier checkBlocklist() { require( !IBlocklist(blocklist).isBlocked(msg.sender), "Blocked contract" ); _; } /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// Owner Functions /// /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// @notice Transfers ownership to a new owner /// @param _addr The new owner /// @dev Owner should always be a timelock contract function transferOwnership(address _addr) external { require(msg.sender == owner, "Only owner"); owner = _addr; emit TransferOwnership(_addr); } /// @notice Updates the blocklist contract function updateBlocklist(address _addr) external { require(msg.sender == owner, "Only owner"); blocklist = _addr; emit UpdateBlocklist(_addr); } /// @notice Updates the recipient of the accumulated penalty paid by quitters function updatePenaltyRecipient(address _addr) external { require(msg.sender == owner, "Only owner"); penaltyRecipient = _addr; emit UpdatePenaltyRecipient(_addr); } /// @notice Removes quitlock penalty by setting it to zero /// @dev This is an irreversible action function unlock() external { require(msg.sender == owner, "Only owner"); maxPenalty = 0; emit Unlock(); } /// @notice Forces an undelegation of virtual balance for a blocked locker /// @dev Can only be called by the Blocklist contract (as part of a block) /// @dev This is an irreversible action function forceUndelegate(address _addr) external override { require(msg.sender == blocklist, "Only Blocklist"); LockedBalance memory locked_ = locked[_addr]; address delegatee = locked_.delegatee; int128 value = locked_.amount; if (delegatee != _addr && value > 0) { LockedBalance memory fromLocked; locked_.delegatee = _addr; fromLocked = locked[delegatee]; _delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE); _delegate(_addr, locked_, value, LockAction.DELEGATE); } } /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// LOCK MANAGEMENT /// /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// @notice Returns a user's lock expiration /// @param _addr The address of the user /// @return Expiration of the user's lock function lockEnd(address _addr) external view returns (uint256) { return locked[_addr].end; } /// @notice Returns the last available user point for a user /// @param _addr User address /// @return bias i.e. y /// @return slope i.e. linear gradient /// @return ts i.e. time point was logged function getLastUserPoint(address _addr) external view returns ( int128 bias, int128 slope, uint256 ts ) { uint256 uepoch = userPointEpoch[_addr]; if (uepoch == 0) { return (0, 0, 0); } Point memory point = userPointHistory[_addr][uepoch]; return (point.bias, point.slope, point.ts); } /// @notice Records a checkpoint of both individual and global slope /// @param _addr User address, or address(0) for only global /// @param _oldLocked Old amount that user had locked, or null for global /// @param _newLocked new amount that user has locked, or null for global function _checkpoint( address _addr, LockedBalance memory _oldLocked, LockedBalance memory _newLocked ) internal { Point memory userOldPoint; Point memory userNewPoint; int128 oldSlopeDelta = 0; int128 newSlopeDelta = 0; uint256 epoch = globalEpoch; if (_addr != address(0)) { // Calculate slopes and biases // Kept at zero when they have to if (_oldLocked.end > block.timestamp && _oldLocked.delegated > 0) { userOldPoint.slope = _oldLocked.delegated / int128(int256(MAXTIME)); userOldPoint.bias = userOldPoint.slope * int128(int256(_oldLocked.end - block.timestamp)); } if (_newLocked.end > block.timestamp && _newLocked.delegated > 0) { userNewPoint.slope = _newLocked.delegated / int128(int256(MAXTIME)); userNewPoint.bias = userNewPoint.slope * int128(int256(_newLocked.end - block.timestamp)); } // Moved from bottom final if statement to resolve stack too deep err // start { // Now handle user history uint256 uEpoch = userPointEpoch[_addr]; if (uEpoch == 0) { userPointHistory[_addr][uEpoch + 1] = userOldPoint; } userPointEpoch[_addr] = uEpoch + 1; userNewPoint.ts = block.timestamp; userNewPoint.blk = block.number; userPointHistory[_addr][uEpoch + 1] = userNewPoint; // } end // 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 oldSlopeDelta = slopeChanges[_oldLocked.end]; if (_newLocked.end != 0) { if (_newLocked.end == _oldLocked.end) { newSlopeDelta = oldSlopeDelta; } else { newSlopeDelta = slopeChanges[_newLocked.end]; } } } Point memory lastPoint = Point({ bias: 0, slope: 0, ts: block.timestamp, blk: block.number }); if (epoch > 0) { lastPoint = pointHistory[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 = Point({ bias: 0, slope: 0, ts: lastPoint.ts, blk: lastPoint.blk }); 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 iterativeTime = _floorToWeek(lastCheckpoint); for (uint256 i = 0; 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 iterativeTime = iterativeTime + WEEK; int128 dSlope = 0; if (iterativeTime > block.timestamp) { iterativeTime = block.timestamp; } else { dSlope = slopeChanges[iterativeTime]; } int128 biasDelta = lastPoint.slope * int128(int256((iterativeTime - lastCheckpoint))); lastPoint.bias = lastPoint.bias - biasDelta; lastPoint.slope = lastPoint.slope + dSlope; // This can happen if (lastPoint.bias < 0) { lastPoint.bias = 0; } // This cannot happen - just in case if (lastPoint.slope < 0) { lastPoint.slope = 0; } lastCheckpoint = iterativeTime; lastPoint.ts = iterativeTime; lastPoint.blk = initialLastPoint.blk + (blockSlope * (iterativeTime - initialLastPoint.ts)) / MULTIPLIER; // when epoch is incremented, we either push here or after slopes updated below epoch = epoch + 1; if (iterativeTime == block.timestamp) { lastPoint.blk = block.number; break; } else { pointHistory[epoch] = lastPoint; } } globalEpoch = epoch; // Now pointHistory 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 = lastPoint.slope + userNewPoint.slope - userOldPoint.slope; lastPoint.bias = lastPoint.bias + userNewPoint.bias - userOldPoint.bias; if (lastPoint.slope < 0) { lastPoint.slope = 0; } if (lastPoint.bias < 0) { lastPoint.bias = 0; } } // Record the changed point into history pointHistory[epoch] = lastPoint; if (_addr != address(0)) { // Schedule the slope changes (slope is going down) // We subtract new_user_slope from [new_locked.end] // and add old_user_slope to [old_locked.end] if (_oldLocked.end > block.timestamp) { // oldSlopeDelta was <something> - userOldPoint.slope, so we cancel that oldSlopeDelta = oldSlopeDelta + userOldPoint.slope; if (_newLocked.end == _oldLocked.end) { oldSlopeDelta = oldSlopeDelta - userNewPoint.slope; // It was a new deposit, not extension } slopeChanges[_oldLocked.end] = oldSlopeDelta; } if (_newLocked.end > block.timestamp) { if (_newLocked.end > _oldLocked.end) { newSlopeDelta = newSlopeDelta - userNewPoint.slope; // old slope disappeared at this point slopeChanges[_newLocked.end] = newSlopeDelta; } // else: we recorded it already in oldSlopeDelta } } } /// @notice Public function to trigger global checkpoint function checkpoint() external { LockedBalance memory empty; _checkpoint(address(0), empty, empty); } // See IVotingEscrow for documentation function createLock(uint256 _value, uint256 _unlockTime) external override nonReentrant checkBlocklist { uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(_value > 0, "Only non zero amount"); require(locked_.amount == 0, "Lock exists"); require(unlock_time >= locked_.end, "Only increase lock end"); // from using quitLock, user should increaseAmount instead require(unlock_time > block.timestamp, "Only future lock end"); require(unlock_time <= block.timestamp + MAXTIME, "Exceeds maxtime"); // Update lock and voting power (checkpoint) locked_.amount += int128(int256(_value)); locked_.end = unlock_time; locked_.delegated += int128(int256(_value)); locked_.delegatee = msg.sender; locked[msg.sender] = locked_; _checkpoint(msg.sender, LockedBalance(0, 0, 0, address(0)), locked_); // Deposit locked tokens require( token.transferFrom(msg.sender, address(this), _value), "Transfer failed" ); emit Deposit( msg.sender, _value, unlock_time, LockAction.CREATE, block.timestamp ); } // See IVotingEscrow for documentation // @dev A lock is active until both lock.amount==0 and lock.end<=block.timestamp function increaseAmount(uint256 _value) external override nonReentrant checkBlocklist { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(_value > 0, "Only non zero amount"); require(locked_.amount > 0, "No lock"); require(locked_.end > block.timestamp, "Lock expired"); // Update lock address delegatee = locked_.delegatee; uint256 unlockTime = locked_.end; LockAction action = LockAction.INCREASE_AMOUNT; LockedBalance memory newLocked; if (delegatee == msg.sender) { // Undelegated lock action = LockAction.INCREASE_AMOUNT_AND_DELEGATION; newLocked = _copyLock(locked_); newLocked.amount += int128(int256(_value)); newLocked.delegated += int128(int256(_value)); locked[msg.sender] = newLocked; } else { // Delegated lock, update sender's lock first locked_.amount += int128(int256(_value)); locked[msg.sender] = locked_; // Then, update delegatee's lock and voting power (checkpoint) locked_ = locked[delegatee]; require(locked_.amount > 0, "Delegatee has no lock"); require(locked_.end > block.timestamp, "Delegatee lock expired"); newLocked = _copyLock(locked_); newLocked.delegated += int128(int256(_value)); locked[delegatee] = newLocked; emit Deposit( delegatee, _value, newLocked.end, LockAction.DELEGATE, block.timestamp ); } // Checkpoint only for delegatee _checkpoint(delegatee, locked_, newLocked); // Deposit locked tokens require( token.transferFrom(msg.sender, address(this), _value), "Transfer failed" ); emit Deposit(msg.sender, _value, unlockTime, action, block.timestamp); } // See IVotingEscrow for documentation function increaseUnlockTime(uint256 _unlockTime) external override nonReentrant checkBlocklist { LockedBalance memory locked_ = locked[msg.sender]; uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks // Validate inputs require(locked_.amount > 0, "No lock"); require(unlock_time > locked_.end, "Only increase lock end"); require(unlock_time <= block.timestamp + MAXTIME, "Exceeds maxtime"); // Update lock uint256 oldUnlockTime = locked_.end; locked_.end = unlock_time; locked[msg.sender] = locked_; if (locked_.delegatee == msg.sender) { // Undelegated lock require(oldUnlockTime > block.timestamp, "Lock expired"); LockedBalance memory oldLocked = _copyLock(locked_); oldLocked.end = unlock_time; _checkpoint(msg.sender, oldLocked, locked_); } emit Deposit( msg.sender, 0, unlock_time, LockAction.INCREASE_TIME, block.timestamp ); } // See IVotingEscrow for documentation function withdraw() external override nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(locked_.amount > 0, "No lock"); require(locked_.end <= block.timestamp, "Lock not expired"); require(locked_.delegatee == msg.sender, "Lock delegated"); // Update lock uint256 value = uint256(uint128(locked_.amount)); LockedBalance memory newLocked = _copyLock(locked_); newLocked.amount = 0; newLocked.end = 0; newLocked.delegated -= int128(int256(value)); newLocked.delegatee = address(0); locked[msg.sender] = newLocked; newLocked.delegated = 0; // oldLocked can have either expired <= timestamp or zero end // currentLock has only 0 end // Both can have >= 0 amount _checkpoint(msg.sender, locked_, newLocked); // Send back deposited tokens require(token.transfer(msg.sender, value), "Transfer failed"); emit Withdraw(msg.sender, value, LockAction.WITHDRAW, block.timestamp); } /// ~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// DELEGATION /// /// ~~~~~~~~~~~~~~~~~~~~~~~~~~ /// // See IVotingEscrow for documentation function delegate(address _addr) external override nonReentrant checkBlocklist { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(!IBlocklist(blocklist).isBlocked(_addr), "Blocked contract"); require(locked_.amount > 0, "No lock"); require(locked_.delegatee != _addr, "Already delegated"); // Update locks int128 value = locked_.amount; address delegatee = locked_.delegatee; LockedBalance memory fromLocked; LockedBalance memory toLocked; locked_.delegatee = _addr; if (delegatee == msg.sender) { // Delegate fromLocked = locked_; toLocked = locked[_addr]; } else if (_addr == msg.sender) { // Undelegate fromLocked = locked[delegatee]; toLocked = locked_; } else { // Re-delegate fromLocked = locked[delegatee]; toLocked = locked[_addr]; // Update owner lock if not involved in delegation locked[msg.sender] = locked_; } require(toLocked.amount > 0, "Delegatee has no lock"); require(toLocked.end > block.timestamp, "Delegatee lock expired"); require(toLocked.end >= fromLocked.end, "Only delegate to longer lock"); _delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE); _delegate(_addr, toLocked, value, LockAction.DELEGATE); } // Delegates from/to lock and voting power function _delegate( address addr, LockedBalance memory _locked, int128 value, LockAction action ) internal { LockedBalance memory newLocked = _copyLock(_locked); if (action == LockAction.DELEGATE) { newLocked.delegated += value; emit Deposit( addr, uint256(int256(value)), newLocked.end, action, block.timestamp ); } else { newLocked.delegated -= value; emit Withdraw( addr, uint256(int256(value)), action, block.timestamp ); } locked[addr] = newLocked; if (newLocked.amount > 0) { // Only if lock (from lock) hasn't been withdrawn/quitted _checkpoint(addr, _locked, newLocked); } } /// ~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// QUIT LOCK /// /// ~~~~~~~~~~~~~~~~~~~~~~~~~~ /// // See IVotingEscrow for documentation function quitLock() external override nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(locked_.amount > 0, "No lock"); require(locked_.end > block.timestamp, "Lock expired"); require(locked_.delegatee == msg.sender, "Lock delegated"); // Update lock uint256 value = uint256(uint128(locked_.amount)); LockedBalance memory newLocked = _copyLock(locked_); newLocked.amount = 0; newLocked.delegated -= int128(int256(value)); newLocked.delegatee = address(0); locked[msg.sender] = newLocked; newLocked.end = 0; newLocked.delegated = 0; // oldLocked can have either expired <= timestamp or zero end // currentLock has only 0 end // Both can have >= 0 amount _checkpoint(msg.sender, locked_, newLocked); // apply penalty uint256 penaltyRate = _calculatePenaltyRate(locked_.end); uint256 penaltyAmount = (value * penaltyRate) / 10**18; // quitlock_penalty is in 18 decimals precision penaltyAccumulated += penaltyAmount; uint256 remainingAmount = value - penaltyAmount; // Send back remaining tokens require(token.transfer(msg.sender, remainingAmount), "Transfer failed"); emit Withdraw(msg.sender, value, LockAction.QUIT, block.timestamp); } // Calculate penalty rate (decreasing linearly) function _calculatePenaltyRate(uint256 end) internal view returns (uint256) { // We know that end > block.timestamp because expired locks cannot be quitted return ((end - block.timestamp) * maxPenalty) / MAXTIME; } /// @notice Collect accumulated penalty from quitters /// @dev Everyone can collect but penalty is sent to `penaltyRecipient` function collectPenalty() external { uint256 amount = penaltyAccumulated; penaltyAccumulated = 0; require(token.transfer(penaltyRecipient, amount), "Transfer failed"); emit CollectPenalty(amount, penaltyRecipient); } /// ~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// GETTERS /// /// ~~~~~~~~~~~~~~~~~~~~~~~~~~ /// // Creates a copy of a lock function _copyLock(LockedBalance memory _locked) internal pure returns (LockedBalance memory) { return LockedBalance({ amount: _locked.amount, end: _locked.end, delegatee: _locked.delegatee, delegated: _locked.delegated }); } // @dev Floors a timestamp to the nearest weekly increment // @param _t Timestamp to floor function _floorToWeek(uint256 _t) internal pure returns (uint256) { return (_t / WEEK) * WEEK; } // @dev Uses binarysearch to find the most recent point history preceeding block // @param _block Find the most recent point history before this block // @param _maxEpoch Do not search pointHistories past this index function _findBlockEpoch(uint256 _block, uint256 _maxEpoch) internal view returns (uint256) { // Binary search uint256 min = 0; uint256 max = _maxEpoch; // Will be always enough for 128-bit numbers for (uint256 i = 0; i < 128; i++) { if (min >= max) break; uint256 mid = (min + max + 1) / 2; if (pointHistory[mid].blk <= _block) { min = mid; } else { max = mid - 1; } } return min; } // @dev Uses binarysearch to find the most recent user point history preceeding block // @param _addr User for which to search // @param _block Find the most recent point history before this block function _findUserBlockEpoch(address _addr, uint256 _block) internal view returns (uint256) { uint256 min = 0; uint256 max = userPointEpoch[_addr]; for (uint256 i = 0; i < 128; i++) { if (min >= max) { break; } uint256 mid = (min + max + 1) / 2; if (userPointHistory[_addr][mid].blk <= _block) { min = mid; } else { max = mid - 1; } } return min; } // See IVotingEscrow for documentation function balanceOf(address _owner) public view override returns (uint256) { uint256 epoch = userPointEpoch[_owner]; if (epoch == 0) { return 0; } Point memory lastPoint = userPointHistory[_owner][epoch]; lastPoint.bias = lastPoint.bias - (lastPoint.slope * int128(int256(block.timestamp - lastPoint.ts))); if (lastPoint.bias < 0) { lastPoint.bias = 0; } return uint256(uint128(lastPoint.bias)); } // See IVotingEscrow for documentation function balanceOfAt(address _owner, uint256 _blockNumber) public view override returns (uint256) { require(_blockNumber <= block.number, "Only past block number"); // Get most recent user Point to block uint256 userEpoch = _findUserBlockEpoch(_owner, _blockNumber); if (userEpoch == 0) { return 0; } Point memory upoint = userPointHistory[_owner][userEpoch]; // Get most recent global Point to block uint256 maxEpoch = globalEpoch; uint256 epoch = _findBlockEpoch(_blockNumber, maxEpoch); Point memory point0 = pointHistory[epoch]; // Calculate delta (block & time) between user Point and target block // Allowing us to calculate the average seconds per block between // the two points uint256 dBlock = 0; uint256 dTime = 0; if (epoch < maxEpoch) { Point memory point1 = pointHistory[epoch + 1]; dBlock = point1.blk - point0.blk; dTime = point1.ts - point0.ts; } else { dBlock = block.number - point0.blk; dTime = block.timestamp - point0.ts; } // (Deterministically) Estimate the time at which block _blockNumber was mined uint256 blockTime = point0.ts; if (dBlock != 0) { blockTime = blockTime + ((dTime * (_blockNumber - point0.blk)) / dBlock); } // Current Bias = most recent bias - (slope * time since update) upoint.bias = upoint.bias - (upoint.slope * int128(int256(blockTime - upoint.ts))); if (upoint.bias >= 0) { return uint256(uint128(upoint.bias)); } else { return 0; } } /// @notice Calculate total supply of voting power at a given time _t /// @param _point Most recent point before time _t /// @param _t Time at which to calculate supply /// @return totalSupply at given point in time function _supplyAt(Point memory _point, uint256 _t) internal view returns (uint256) { Point memory lastPoint = _point; // Floor the timestamp to weekly interval uint256 iterativeTime = _floorToWeek(lastPoint.ts); // Iterate through all weeks between _point & _t to account for slope changes for (uint256 i = 0; i < 255; i++) { iterativeTime = iterativeTime + WEEK; int128 dSlope = 0; // If week end is after timestamp, then truncate & leave dSlope to 0 if (iterativeTime > _t) { iterativeTime = _t; } // else get most recent slope change else { dSlope = slopeChanges[iterativeTime]; } lastPoint.bias = lastPoint.bias - (lastPoint.slope * int128(int256(iterativeTime - lastPoint.ts))); if (iterativeTime == _t) { break; } lastPoint.slope = lastPoint.slope + dSlope; lastPoint.ts = iterativeTime; } if (lastPoint.bias < 0) { lastPoint.bias = 0; } return uint256(uint128(lastPoint.bias)); } // See IVotingEscrow for documentation function totalSupply() public view override returns (uint256) { uint256 epoch_ = globalEpoch; Point memory lastPoint = pointHistory[epoch_]; return _supplyAt(lastPoint, block.timestamp); } // See IVotingEscrow for documentation function totalSupplyAt(uint256 _blockNumber) public view override returns (uint256) { require(_blockNumber <= block.number, "Only past block number"); uint256 epoch = globalEpoch; uint256 targetEpoch = _findBlockEpoch(_blockNumber, epoch); Point memory point = pointHistory[targetEpoch]; // If point.blk > _blockNumber that means we got the initial epoch & contract did not yet exist if (point.blk > _blockNumber) { return 0; } uint256 dTime = 0; if (targetEpoch < epoch) { Point memory pointNext = pointHistory[targetEpoch + 1]; if (point.blk != pointNext.blk) { dTime = ((_blockNumber - point.blk) * (pointNext.ts - point.ts)) / (pointNext.blk - point.blk); } } else if (point.blk != block.number) { dTime = ((_blockNumber - point.blk) * (block.timestamp - point.ts)) / (block.number - point.blk); } // Now dTime contains info on how far are we beyond point return _supplyAt(point, point.ts + dTime); } }
查找差异