Diff
checker
텍스트
텍스트
이미지
문서
Excel
폴더
Legal
Enterprise
데스크톱
요금제
로그인
데스크톱 앱 다운로드
텍스트 비교
두 텍스트 파일의 차이점을 찾아보세요
도구
기록
실시간 편집
변경 없는 행 숨기기
줄바꿈 비활성화
레이아웃
나란히 보기
합쳐 보기
비교 단위
스마트
단어
글자
구문 강조
언어 선택
제외
텍스트 변환
첫 변경으로
수정
Diffchecker Desktop
가장 안전하게 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); } }
비교하기