VotingEscrow

Created Diff never expires
488 removals
Lines
Total
Removed
Words
Total
Removed
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
691 lines
450 additions
Lines
Total
Added
Words
Total
Added
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
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