// File: contracts/interfaces/IERC20.sol
pragma solidity >=0.4.22 <0.9.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @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
);
/**
* @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 `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, 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 `from` to `to` 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 from,
address to,
uint256 amount
) external returns (bool);
}
// File: contracts/interfaces/IPlanetConventionalPool.sol
pragma solidity >=0.4.22 <0.9.0;
interface IPlanetConventionalPool {
function userTokenStakeInfo(
address _user
)
external
view
returns (
uint256 _amount,
uint256 _time,
uint256 _reward,
uint256 _startTime
);
function userLpStakeInfo(
address _user
)
external
view
returns (
uint256 _lpAmount,
uint256 _amount,
uint256 _time,
uint256 _reward,
uint256 _startTime
);
function getUserInfo(
address _user
)
external
view
returns (
bool _isExists,
uint256 _stakeCount,
uint256 _totalStakedToken,
uint256 _totalStakedLp,
uint256 _totalWithdrawanToken,
uint256 _totalWithdrawanLp
);
}
// File: contracts/interfaces/OwnershipManager.sol
pragma solidity >=0.4.22 <0.9.0;
abstract contract OwnershipManager {
address private _owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor(address initialOwner) {
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(
owner() == msg.sender,
"OwnershipManager: caller is not the owner"
);
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(
newOwner != address(0),
"OwnershipManager: new owner is the zero address"
);
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// File: contracts/interfaces/IPairInterface.sol
pragma solidity >=0.4.22 <0.9.0;
interface IPair {
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
event Transfer(address indexed from, address indexed to, uint256 value);
function name() external pure returns (string memory);
function symbol() external pure returns (string memory);
function decimals() external pure returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address owner) external view returns (uint256);
function allowance(
address owner,
address spender
) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(
address from,
address to,
uint256 value
) external returns (bool);
function DOMAIN_SEPARATOR() external view returns (bytes32);
function PERMIT_TYPEHASH() external pure returns (bytes32);
function nonces(address owner) external view returns (uint256);
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
event Burn(
address indexed sender,
uint256 amount0,
uint256 amount1,
address indexed to
);
event Swap(
address indexed sender,
uint256 amount0In,
uint256 amount1In,
uint256 amount0Out,
uint256 amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);
function MINIMUM_LIQUIDITY() external pure returns (uint256);
function factory() external view returns (address);
function token0() external view returns (address);
function token1() external view returns (address);
function getReserves()
external
view
returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function price0CumulativeLast() external view returns (uint256);
function price1CumulativeLast() external view returns (uint256);
function kLast() external view returns (uint256);
function mint(address to) external returns (uint256 liquidity);
function burn(
address to
) external returns (uint256 amount0, uint256 amount1);
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) external;
function skim(address to) external;
function sync() external;
function initialize(address, address) external;
}
// File: contracts/PlanetStakingPool.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
/**
* @notice Token staking pool enables users to stake their tokens,
* to earn % APY for providing their tokens to the staking pool.
*/
contract PlanetStakingPool is OwnershipManager {
struct AccountDetails {
uint256 stakedTokenAmount; // Stake Fn
uint256 firstStakedAt; // Stake Fn
uint256 rewards; // Stake & Unstake Fn
uint256 stakeEntries; // Stake Fn
}
// Tracking of global account level staking details.
mapping(address => AccountDetails) public accountDetails;
// Staking Pool token dependency.
IERC20 public immutable PLANET_TOKEN;
// Tracking of staking pool details.
uint256 public totalStakedTokens; // Stake Fn
uint256 public totalUnstakedTokens; // Unstake Fn
uint256 public totalUniqueStakers; // Stake Fn
uint256 public totalRewards; // Stake Fn
uint256 public totalHarvestedRewards; // Unstake Fn
uint256 public APY = 50;
// Staking pool requirements.
uint256 public startDate = 1689602400;
uint256 public maximumPoolAmount = 1e29;
uint256 public remainingPoolAmount = 1e29;
uint256 public tokensLockinPeriod = 90 days;
uint256 public allowedStakingPeriod = 7 days;
// Tracking of banned accounts
mapping(address => bool) public isBanned;
// Staking pool reward provisioning distributor endpoint.
address public rewardVault;
// Emergency state
bool public isPaused;
// Staking pool events to log core functionality.
event Staked(address indexed staker, uint256 amount);
event Unstaked(address indexed staker, uint256 amount);
event Harvested(address indexed staker, uint256 rewards);
event Banned(address indexed staker, bool isBanned);
event Paused(bool isPaused);
modifier onlyIfNotPaused() {
require(!isPaused, "PlanetStakingPool: all actions are paused");
_;
}
modifier onlyIfNotBanned() {
require(!isBanned[msg.sender], "PlanetStakingPool: account banned");
_;
}
/**
* @dev Initialize contract by deploying a staking pool and setting
* up external dependencies.
*
* @param initialOwner --> The manager of access restricted functionality.
* @param rewardToken --> The token that will be rewarded for staking in the pool.
* @param distributor --> The reward distribution endpoint that will do reward provisioning.
*/
constructor(
address payable initialOwner,
address rewardToken,
address payable distributor
) OwnershipManager(initialOwner) {
PLANET_TOKEN = IERC20(rewardToken);
rewardVault = distributor;
}
/**
* @notice Update APY
*
* @param newAPY --> new APY.
*/
function UpdatePoolAPY(uint256 newAPY) external onlyOwner {
require(
newAPY > 0,
"PlanetStakingPool: new APY must be greater than zero"
);
APY = newAPY;
}
/**
* @notice SeUpdatet the max token staking requirement for all users,
* the pool must comply with the max to stake.
*
* @param newMaximumPoolAmount --> The maximum token amount for all users.
*/
function UpdateMaximumPoolAmount(
uint256 newMaximumPoolAmount
) external onlyOwner {
require(
newMaximumPoolAmount > 0 &&
newMaximumPoolAmount >= totalStakedTokens,
"PlanetStakingPool: new maximum pool amount must be greater than zero"
);
maximumPoolAmount = newMaximumPoolAmount;
remainingPoolAmount = maximumPoolAmount - totalStakedTokens;
}
/**
* @notice Update the pool start date.
*
* @param newStartDate --> The start date in seconds.
*/
function UpdateStartDate(uint256 newStartDate) external onlyOwner {
require(
newStartDate > block.timestamp,
"PlanetStakingPool: start date must be in the future"
);
startDate = newStartDate;
}
/**
* @notice Update the allowed staking period.
*
* @param newAllowedStakingPeriod --> The allowed staing period period in days.
*/
function UpdateAllowedStakingPeriod(
uint256 newAllowedStakingPeriod
) external onlyOwner {
require(
newAllowedStakingPeriod > 0 &&
newAllowedStakingPeriod < tokensLockinPeriod,
"PlanetStakingPool: new allowed staking period must be greater than zero and less than lockin period"
);
allowedStakingPeriod = newAllowedStakingPeriod * 1 days;
}
/**
* @notice Update the token staking lockin period.
*
* @param newLockinPeriod --> The lockin period in days.
*/
function UpdateLockinPeriod(uint256 newLockinPeriod) external onlyOwner {
require(
newLockinPeriod > allowedStakingPeriod,
"PlanetStakingPool: new lockin period must be greater than allowed staking period (14 days)"
);
tokensLockinPeriod = newLockinPeriod * 1 days;
}
/**
* @notice Update the reward provisioning endpoint,
* this should be the distributor that handle rewards.
*
* @param newRewardVault --> The new distributor for reward provisioning.
*/
function UpdateRewardVault(address newRewardVault) external onlyOwner {
require(
newRewardVault != address(0),
"PlanetStakingPool: new reward vault cannot be zero address"
);
rewardVault = newRewardVault;
}
/**
* @notice Update restrictions on an account.
*
* @param account --> The account to restrict.
* @param state --> The state of the restriction.
*/
function UpdateBanState(address account, bool state) external onlyOwner {
require(
isBanned[account] != state,
"PlanetStakingPool: account already in state"
);
isBanned[account] = state;
emit Banned(account, state);
}
/**
* @notice Update the staking pool in a pause.
*
* @param state --> The state of the staking pool.
*/
function UpdatePoolPauseState(bool state) external onlyOwner {
require(isPaused != state, "PlanetStakingPool: pool already in state");
isPaused = state;
emit Paused(state);
}
/**
* @notice Stake tokens to accumulate token rewards,
* token reward accumulation is based on the % APY.
*
* @param amount --> The amount of tokens that the account wish,
* to stake in the staking pool.
*/
function stake(uint256 amount) external onlyIfNotPaused onlyIfNotBanned {
address account = msg.sender;
// Check that the staked amount is greater than zero
require(
amount > 0,
"PlanetStakingPool: staked amount must be greater than zero"
);
// Check that staking has started
require(
startDate <= block.timestamp,
"PlanetStakingPool: staking not started"
);
// Check that staking has not locked
require(
startDate + allowedStakingPeriod >= block.timestamp,
"PlanetStakingPool: staking period ended"
);
// Check that the staked amount does not overflow the pool
require(
totalStakedTokens + amount <= maximumPoolAmount,
"PlanetStakingPool: staking pool overflow"
);
// Check if the account is unique (First Stake), if yes then add it to global tracking and capture first staking time.
if (accountDetails[account].stakedTokenAmount == 0) {
totalUniqueStakers++;
accountDetails[account].firstStakedAt = block.timestamp;
}
// Transfer the staked amount of tokens to the staking pool.
require(
PLANET_TOKEN.transferFrom(account, address(this), amount),
"PlanetStakingPool: transfer failed"
);
// Update global account staking details.
accountDetails[account].stakeEntries++;
// accountDetails[account].lastStakedAt = block.timestamp;
accountDetails[account].stakedTokenAmount += amount;
// Calculate rewards
totalRewards -= accountDetails[account].rewards;
uint256 rewards = calculateTokenReward(account);
totalRewards += rewards;
accountDetails[account].rewards = rewards;
// Update global staking pool details.
totalStakedTokens += amount;
remainingPoolAmount -= amount;
// Log successful activity.
emit Staked(account, amount);
}
/**
* @notice Unstake tokens to withdraw your position from the staking pools,
* available rewards are transferred to the staking account.
*/
function unstake() external onlyIfNotPaused onlyIfNotBanned {
require(
getAllowingUnstakeDate(msg.sender) <= block.timestamp,
"PlanetStakingPool: can not unstake yet"
);
require(
accountDetails[msg.sender].rewards > 0,
"PlanetStakingPool: no rewards to harvest"
);
uint256 rewards = accountDetails[msg.sender].rewards;
uint256 stakedAmount = accountDetails[msg.sender].stakedTokenAmount;
// Delete the account details before making external calls.
delete accountDetails[msg.sender];
// Transfer the staked tokens and rewards to the user account.
require(
PLANET_TOKEN.transferFrom(rewardVault, msg.sender, rewards),
"PlanetStakingPool: rewards transfer failed"
);
require(
PLANET_TOKEN.transfer(msg.sender, stakedAmount),
"PlanetStakingPool: staked tokens transfer failed"
);
totalHarvestedRewards += rewards;
totalUnstakedTokens += stakedAmount;
// Log successful activity.
emit Unstaked(msg.sender, stakedAmount);
emit Harvested(msg.sender, rewards);
}
/**
* @notice Calculate the unsettled rewards for an account from staking
* in the pool, rewards that has not been compounded yet.
*
* @param account --> The account to use for reward calculation.
*/
function calculateTokenReward(
address account
) public view returns (uint256 reward) {
// Calculate the reward rate of an account.
reward =
(accountDetails[account].stakedTokenAmount *
tokensLockinPeriod *
APY) /
(365 days * 100.0);
}
/**
* @notice Calculate the current rewards.
*
*/
function calculateTokenCurrentReward(
address account
) public view returns (uint256 reward) {
require(
accountDetails[account].stakedTokenAmount > 0,
"PlanetStakingPool: no staked tokens"
);
reward =
(accountDetails[account].stakedTokenAmount *
(block.timestamp - accountDetails[account].firstStakedAt) *
APY) /
(365 days * 100.0);
}
/**
* @notice Calculate the Unstake Date.
*
*/
function getAllowingUnstakeDate(
address account
) public view returns (uint256 unstakeDate) {
unstakeDate =
accountDetails[account].firstStakedAt +
tokensLockinPeriod;
}
}
{
"compilationTarget": {
"PlanetStakingPool.sol": "PlanetStakingPool"
},
"evmVersion": "shanghai",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": false,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address payable","name":"initialOwner","type":"address"},{"internalType":"address","name":"rewardToken","type":"address"},{"internalType":"address payable","name":"distributor","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"bool","name":"isBanned","type":"bool"}],"name":"Banned","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"uint256","name":"rewards","type":"uint256"}],"name":"Harvested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bool","name":"isPaused","type":"bool"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Unstaked","type":"event"},{"inputs":[],"name":"APY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PLANET_TOKEN","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"newAllowedStakingPeriod","type":"uint256"}],"name":"UpdateAllowedStakingPeriod","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bool","name":"state","type":"bool"}],"name":"UpdateBanState","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newLockinPeriod","type":"uint256"}],"name":"UpdateLockinPeriod","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newMaximumPoolAmount","type":"uint256"}],"name":"UpdateMaximumPoolAmount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newAPY","type":"uint256"}],"name":"UpdatePoolAPY","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"state","type":"bool"}],"name":"UpdatePoolPauseState","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newRewardVault","type":"address"}],"name":"UpdateRewardVault","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newStartDate","type":"uint256"}],"name":"UpdateStartDate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"accountDetails","outputs":[{"internalType":"uint256","name":"stakedTokenAmount","type":"uint256"},{"internalType":"uint256","name":"firstStakedAt","type":"uint256"},{"internalType":"uint256","name":"rewards","type":"uint256"},{"internalType":"uint256","name":"stakeEntries","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"allowedStakingPeriod","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"calculateTokenCurrentReward","outputs":[{"internalType":"uint256","name":"reward","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"calculateTokenReward","outputs":[{"internalType":"uint256","name":"reward","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"getAllowingUnstakeDate","outputs":[{"internalType":"uint256","name":"unstakeDate","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isBanned","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maximumPoolAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"remainingPoolAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"rewardVault","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"startDate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"tokensLockinPeriod","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalHarvestedRewards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalRewards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalStakedTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalUniqueStakers","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalUnstakedTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unstake","outputs":[],"stateMutability":"nonpayable","type":"function"}]