VotingEscrow
488 removals
Words removed | 1585 |
Total words | 2656 |
Words removed (%) | 59.68 |
691 lines
450 additions
Words added | 1356 |
Total words | 2427 |
Words added (%) | 55.87 |
661 lines
// 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 Globals */
// Shared global 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;
/** Lockup */
// 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, "Contract is expired");
!IBlocklist(blocklist).isBlocked(msg.sender),
"Blocked contract"
);
_;
_;
}
}
/**
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ///
* @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.toInt128(int256(MAXTIME));
userOldPoint.slope =
_oldLocked.delegated /
int128(int256(MAXTIME));
userOldPoint.bias =
userOldPoint.bias =
userOldPoint.slope *
userOldPoint.slope *
SafeCast.toInt128(int256(_oldLocked.end - block.timestamp));
int128(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.toInt128(int256(MAXTIME));
userNewPoint.slope =
_newLocked.delegated /
int128(int256(MAXTIME));
userNewPoint.bias =
userNewPoint.bias =
userNewPoint.slope *
userNewPoint.slope *
SafeCast.toInt128(int256(_newLocked.end - block.timestamp));
int128(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.toInt128(int256((iterativeTime - lastCheckpoint)));
lastPoint.slope *
int128(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, "Only 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 only 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 increaseAmount(uint256 _value)
*/
function increaseLockAmount(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 increaseUnlockTime(uint256 _unlockTime)
* @param _unlockTime New unlocktime for lockup
*/
function increaseLockLength(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 locked");
require(locked_.amount > 0, "No lock");
require(locked_.end > block.timestamp, "Lock expired");
require(unlock_time > locked_.end, "Only increase lock end");
require(unlock_time > locked_.end, "Can only 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() external override nonReentrant {
* @param _addr User for which to withdraw
LockedBalance memory locked_ = locked[msg.sender];
*/
// Validate inputs
function _withdraw(address _addr) internal nonReentrant updateReward(_addr) {
require(locked_.amount > 0, "No lock");
LockedBalance memory oldLock = LockedBalance({
require(locked_.end <= block.timestamp, "Lock not expired");
end: locked[_addr].end,
require(locked_.delegatee == msg.sender, "Lock delegated");
amount: locked[_addr].amount
// Update lock
});
uint256 value = uint256(uint128(locked_.amount));
require(block.timestamp >= oldLock.end || expired, "The lock 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.toUint256(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.transfer(msg.sender, value), "Transfer failed");
stakingToken.safeTransfer(_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
LockedBalance 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 dBlock = 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 = point1.ts - point0.ts;
uint256 value = uint256(uint128(locked_.amount));
} else {
LockedBalance memory newLocked = _copyLock(locked_);
dBlock = block.number - point0.blk;
newLocked.amount = 0;
dTime = block.timestamp - point0.ts;
newLocked.delegated -= int128(int256(value));
}
newLocked.delegatee = address(0);
// (Deterministically) Estimate the time at which block _blockNumber was mined
locked[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 either 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);
}
}
/**
// Calculate penalty
* @dev Calcula