// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @notice Wording below needs to properly represent the legal terms so has to be reviewed
/// @notice Comments have been written by the development team and may not represent the actual terms of the contract correctly
/// @title Oiler Staking
/// @author oiler.network
/// @dev that staking contract is fully dependent on the provided reward token and the underlying LP token.
/**
* @notice Staking contract assumes there is a Staking Program going on until a specified Staking Program End Date.
* And there is an amount of Oiler tokens that is gonna be given away to incentivise participation in the Staking Program (called StakingFund).
*
* During this Program - users commit to lock tokens for some period of time, earning RewardPoints (if they don't unlock prematurely).
* RewardPoints multiplier grows linearly with the locking period length (see the formula in calculateStakingRewardPoints() function)
*
* After the end of the Staking Program - the amount of RewardPoints earned by each user is relatively compared to the total RewardPoints
* earned by all staking participants - and the OIL tokens from StakingFund are divided among them accordingly, by their RewardPoints proportions.
*/
contract Staking {
/**
* @dev Saving gas by using lower-bit variables to fit the Stake struct into 256bits
*
* LP Tokens are calculated by this formula:
* LP Tokens = sqrt(tokenAmount * e18 * usdcAmount * e6) =
* = sqrt(100 000 000 * usdcAmount * e24) = // here 100 000 000 is totalSupply of OIL
* = sqrt(usdcAmount * e32) =
* = sqrt(usdcAmount) * e16
*
* Thus the maximum amount of USDC we can use to not overflow the maximum amount of uint72 (4722 e18) will be:
* sqrt(usdcAmount) * e16 < 4722 * e18
* sqrt(usdcAmount) < 472 200
* usdcAmount < 222 972 840 000
* Which is over two hundred trillion dollars - the amount highly improbable at our Uniswap pool as for today
*
* tokenAmount is limited by LP tokens amount (4722e18 LPs for hundreds of trillions of dollars) (Range: [0 - 4722 e18] - uint72 (max 4722 e18))
* lockingPeriodInBlocks is limited by Staking Program duration (around 700000 blocks) (Range: [1 - 700000] - uint24 (max 16 777 215))
* startBlock is in a typical range of Mainnet, Testnets, local networks blocks range (Range: [0 - 100 000 000] - uint32 (max 4 294 967 295))
*
* expectedStakingRewardPoints is limited by:
* LP tokens amount * lockingPeriodInBlocks * lockingPeriodInBlocks
* which is:
* uint72 * uint24 * uint24 = which gives us max uint120, but we use uint128 anyway (Range: [0 - 1.33 e36] - uint128 (max 340 e36))
*/
struct Stake {
uint72 tokenAmount; // Amount of tokens locked in a stake
uint24 lockingPeriodInBlocks; // Arbitrary lock period that will give you a reward
uint32 startBlock; // Start of the locking
uint128 expectedStakingRewardPoints; // The amount of RewardPoints the stake will earn if not unlocked prematurely
}
/// @notice Active stakes for each user
mapping (address => Stake) public stakes;
/// @notice "Reward points" each user earned (would be relative to totalRewardPoints to get the percentage)
mapping (address => uint256) public rewardPointsEarned;
/// @notice Total "reward points" all users earned
uint256 public totalRewardPoints;
/// @notice Block when Staking Program ends
uint256 immutable public stakingProgramEndsBlock;
/// @notice Amount of Staking Bonus Fund (500 000 OIL), Oiler funds must be here, approved and ready to be transferredFrom
uint256 immutable public stakingFundAmount;
/// @notice Uniswap pool that we accept LP tokens from
IERC20 public poolToken;
/// @notice Oiler token that will be given as a reward
IERC20 immutable public oilerToken;
/// @notice The amount of OIL tokens earned, granted to be released during vesting period
mapping (address => uint256) public grantedTokens;
/// @notice The amount of OIL tokens that were already released during vesting period
mapping (address => uint256) public releasedTokens;
/// @dev In blocks - should be around 100 days
uint256 immutable public vestingDuration;
/// @dev Check if poolToken was initialized
modifier poolTokenSet() {
require(address(poolToken) != address(0x0), "poolToken not set");
_;
}
/// @dev Owner is used only in setPoolToken()
address immutable public owner;
/// @dev Used only in setPoolToken()
modifier onlyOwner() {
require(msg.sender == owner, "Can only be called by owner");
_;
}
/**
* @dev before deploying the stakingFundAddress must have set allowances on behalf of that contract. The address can be predicted basing on the CREATE or CREATE2 opcode.
* @param oilerToken_ - address of the token in which rewards will be payed off.
* @param stakingDurationInBlocks_ - Number of blocks after which staking will end.
* @param stakingFundAmount_ - Amount of tokens to be payed of as rewards.
* @param vestingDuration_ - Number of blocks after which OIL tokens earned by staking will be released (duration of Vesting period).
* @param owner_ - Owner of the contract (is used to initialize poolToken after it's available).
*/
constructor(address oilerToken_, uint256 stakingDurationInBlocks_, uint256 stakingFundAmount_, uint256 vestingDuration_, address owner_) {
require(owner_ != address(0x0), "Owner address cannot be zero");
owner = owner_;
require(oilerToken_ != address(0x0), "oilerToken address cannot be zero");
oilerToken = IERC20(oilerToken_);
stakingProgramEndsBlock = block.number + stakingDurationInBlocks_;
vestingDuration = vestingDuration_;
stakingFundAmount = stakingFundAmount_;
}
/// @notice Initialize poolToken when OIL<>USDC Uniswap pool is available
function setPoolToken(address poolToken_, address stakingFundAddress_) public onlyOwner {
require(address(poolToken) == address(0x0), "poolToken was already set");
require(poolToken_ != address(0x0), "poolToken address cannot be zero");
poolToken = IERC20(poolToken_);
// Transfer the Staking Bonus Funds from stakingFundAddress here
require(IERC20(oilerToken).balanceOf(stakingFundAddress_) >= stakingFundAmount, "StakingFund doesn't have enough OIL balance");
require(IERC20(oilerToken).allowance(stakingFundAddress_, address(this)) >= stakingFundAmount, "StakingFund doesn't have enough allowance");
require(IERC20(oilerToken).transferFrom(stakingFundAddress_, address(this), stakingFundAmount), "TransferFrom of OIL from StakingFund failed");
}
/**
* @notice Calculates the RewardPoints user will earn for a given tokenAmount locked for a given period
* @dev If any parameter is zero - it will fail, thus we save gas on "requires" by not checking in other places
* @param tokenAmount_ - Amount of tokens to be stake.
* @param lockingPeriodInBlocks_ - Lock duration defined in blocks.
*/
function calculateStakingRewardPoints(uint72 tokenAmount_, uint24 lockingPeriodInBlocks_) public pure returns (uint128) {
//
// / \
// stakingRewardPoints = ( tokenAmount * lockingPeriodInBlocks ) * lockingPeriodInBlocks
// \ /
//
uint256 stakingRewardPoints = uint256(tokenAmount_) * uint256(lockingPeriodInBlocks_) * uint256(lockingPeriodInBlocks_);
require(stakingRewardPoints > 0, "Neither tokenAmount nor lockingPeriod couldn't be 0");
return uint128(stakingRewardPoints);
}
/**
* @notice Lock the LP tokens for a specified period of Blocks.
* @notice Can only be called before Staking Program ends.
* @notice And the locking period can't last longer than the end of Staking Program block.
* @param tokenAmount_ - Amount of LP tokens to be locked.
* @param lockingPeriodInBlocks_ - locking period duration defined in blocks.
*/
function lockTokens(uint72 tokenAmount_, uint24 lockingPeriodInBlocks_) public poolTokenSet {
// Here we don't check lockingPeriodInBlocks_ for being non-zero, cause its happening in calculateStakingRewardPoints() calculation
require(block.number <= stakingProgramEndsBlock - lockingPeriodInBlocks_, "Your lock period exceeds Staking Program duration");
require(stakes[msg.sender].tokenAmount == 0, "Already staking");
// This is a locking reward - will be earned only after the full lock period is over - otherwise not applicable
uint128 expectedStakingRewardPoints = calculateStakingRewardPoints(tokenAmount_, lockingPeriodInBlocks_);
Stake memory stake = Stake(tokenAmount_, lockingPeriodInBlocks_, uint32(block.number), expectedStakingRewardPoints);
stakes[msg.sender] = stake;
// We add the rewards initially during locking of tokens, and subtract them later if unlocking is made prematurely
// That prevents us from waiting for all users to unlock to distribute the rewards after Staking Program Ends
totalRewardPoints += expectedStakingRewardPoints;
rewardPointsEarned[msg.sender] += expectedStakingRewardPoints;
// We transfer LP tokens from user to this contract, "locking" them
// We don't check for allowances or balance cause it's done within the transferFrom() and would only raise gas costs
require(poolToken.transferFrom(msg.sender, address(this), tokenAmount_), "TransferFrom of poolTokens failed");
emit StakeLocked(msg.sender, tokenAmount_, lockingPeriodInBlocks_, expectedStakingRewardPoints);
}
/**
* @notice Unlock the tokens and get the reward
* @notice This can be called at any time, even after Staking Program end block
*/
function unlockTokens() public poolTokenSet {
Stake memory stake = stakes[msg.sender];
uint256 stakeAmount = stake.tokenAmount;
require(stakeAmount != 0, "You don't have a stake to unlock");
require(block.number > stake.startBlock, "You can't withdraw the stake in the same block it was locked");
// Check if the unlock is called prematurely - and subtract the reward if it is the case
_punishEarlyWithdrawal(stake);
// Zero the Stake - to protect from double-unlocking and to be able to stake again
delete stakes[msg.sender];
require(poolToken.transfer(msg.sender, stakeAmount), "Pool token transfer failed");
}
/**
* @notice If the unlock is called prematurely - we subtract the bonus
*/
function _punishEarlyWithdrawal(Stake memory stake_) internal {
// As any of the locking periods can't be longer than Staking Program end block - this will automatically mean that if called after Staking Program end - all stakes locking periods are over
// So no rewards can be manipulated after Staking Program ends
if (block.number < (stake_.startBlock + stake_.lockingPeriodInBlocks)) { // lt - cause you can only withdraw at or after startBlock + lockPeriod
rewardPointsEarned[msg.sender] -= stake_.expectedStakingRewardPoints;
totalRewardPoints -= stake_.expectedStakingRewardPoints;
emit StakeUnlockedPrematurely(msg.sender, stake_.tokenAmount, stake_.lockingPeriodInBlocks, block.number - stake_.startBlock);
} else {
emit StakeUnlocked(msg.sender, stake_.tokenAmount, stake_.lockingPeriodInBlocks, stake_.expectedStakingRewardPoints);
}
}
/**
* @notice This can only be called after the Staking Program ended
* @dev Which means that all stakes lock periods are already over, and totalRewardPoints value isn't changing anymore - so we can now calculate the percentages of rewards
*/
function getRewards() public {
require(block.number > stakingProgramEndsBlock, "You can only get Rewards after Staking Program ends");
require(stakes[msg.sender].tokenAmount == 0, "You still have a stake locked - please unlock first, don't leave free money here");
require(rewardPointsEarned[msg.sender] > 0, "You don't have any rewardPoints");
// The amount earned is calculated as:
//
// user RewardPoints earned during Staking Program
// amountEarned = stakingFund * -------------------------------------------------------
// total RewardPoints earned by everyone participated
//
// Division always rounds towards zero in solidity.
// And because of this rounding somebody always misses the fractional part of their earnings and gets only integer amount.
// Thus the worst thing that can happen is amountEarned becomes 0, and we check for that in _grantTokens()
uint256 amountEarned = stakingFundAmount * rewardPointsEarned[msg.sender] / totalRewardPoints;
rewardPointsEarned[msg.sender] = 0; // Zero rewardPoints of a user - so this function can be called only once per user
_grantTokens(msg.sender, amountEarned); // Grant OIL reward earned by user for future vesting during the Vesting period
}
//////////////////////////////////////////////////////
//
// VESTING PART
//
//////////////////////////////////////////////////////
/**
* @param recipient_ - Recipient of granted tokens
* @param amountEarned_ - Amount of tokens earned to be granted
*/
function _grantTokens(address recipient_, uint256 amountEarned_) internal {
require(amountEarned_ > 0, "You didn't earn any integer amount of wei");
require(recipient_ != address(0), "TokenVesting: beneficiary is the zero address");
grantedTokens[recipient_] = amountEarned_;
emit RewardGranted(recipient_, amountEarned_);
}
/// @notice Releases granted tokens
function release() public {
uint256 releasable = _releasableAmount(msg.sender);
require(releasable > 0, "Vesting release: no tokens are due");
releasedTokens[msg.sender] += releasable;
require(oilerToken.transfer(msg.sender, releasable), "Reward oilers transfer failed");
emit grantedTokensReleased(msg.sender, releasable);
}
/// @notice Releasable amount is what is available at a given time minus what was already withdrawn
function _releasableAmount(address recipient_) internal view returns (uint256) {
return _vestedAmount(recipient_) - releasedTokens[recipient_];
}
/**
* @notice The output of this function gradually changes from [0.. to ..grantedAmount] while the vesting is going
* @param recipient_ - vested tokens recipient
* @return vested amount
*/
function _vestedAmount(address recipient_) internal view returns (uint256) {
if (block.number >= stakingProgramEndsBlock + vestingDuration) {
// Return the full granted amount if Vesting Period is over
return grantedTokens[recipient_];
} else {
// Return the proportional amount if Vesting Period is still going
return grantedTokens[recipient_] * (block.number - stakingProgramEndsBlock) / vestingDuration;
}
}
event StakeLocked(address recipient, uint256 tokenAmount, uint256 lockingPeriodInBlocks, uint256 expectedStakingRewardPoints);
event StakeUnlockedPrematurely(address recipient, uint256 tokenAmount, uint256 lockingPeriodInBlocks, uint256 actualLockingPeriodInBlocks);
event StakeUnlocked(address recipient, uint256 tokenAmount, uint256 lockingPeriodInBlocks, uint256 rewardPoints);
event RewardGranted(address recipient, uint256 amountEarned);
event grantedTokensReleased(address recipient, uint256 amount);
}
{
"compilationTarget": {
"contracts/Staking.sol": "Staking"
},
"evmVersion": "istanbul",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 999999
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"oilerToken_","type":"address"},{"internalType":"uint256","name":"stakingDurationInBlocks_","type":"uint256"},{"internalType":"uint256","name":"stakingFundAmount_","type":"uint256"},{"internalType":"uint256","name":"vestingDuration_","type":"uint256"},{"internalType":"address","name":"owner_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountEarned","type":"uint256"}],"name":"RewardGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lockingPeriodInBlocks","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"expectedStakingRewardPoints","type":"uint256"}],"name":"StakeLocked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lockingPeriodInBlocks","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"rewardPoints","type":"uint256"}],"name":"StakeUnlocked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lockingPeriodInBlocks","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"actualLockingPeriodInBlocks","type":"uint256"}],"name":"StakeUnlockedPrematurely","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"grantedTokensReleased","type":"event"},{"inputs":[{"internalType":"uint72","name":"tokenAmount_","type":"uint72"},{"internalType":"uint24","name":"lockingPeriodInBlocks_","type":"uint24"}],"name":"calculateStakingRewardPoints","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getRewards","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"grantedTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint72","name":"tokenAmount_","type":"uint72"},{"internalType":"uint24","name":"lockingPeriodInBlocks_","type":"uint24"}],"name":"lockTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"oilerToken","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"poolToken","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"release","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"releasedTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"rewardPointsEarned","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"poolToken_","type":"address"},{"internalType":"address","name":"stakingFundAddress_","type":"address"}],"name":"setPoolToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"stakes","outputs":[{"internalType":"uint72","name":"tokenAmount","type":"uint72"},{"internalType":"uint24","name":"lockingPeriodInBlocks","type":"uint24"},{"internalType":"uint32","name":"startBlock","type":"uint32"},{"internalType":"uint128","name":"expectedStakingRewardPoints","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stakingFundAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stakingProgramEndsBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalRewardPoints","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unlockTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vestingDuration","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]