// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
contract Owned {
address public owner;
address public proposedOwner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor() {
owner = msg.sender;
emit OwnershipTransferred(address(0), msg.sender);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() virtual {
require(msg.sender == owner);
_;
}
/**
* @dev propeses a new owner
* Can only be called by the current owner.
*/
function proposeOwner(address payable _newOwner) external onlyOwner {
proposedOwner = _newOwner;
}
/**
* @dev claims ownership of the contract
* Can only be called by the new proposed owner.
*/
function claimOwnership() external {
require(msg.sender == proposedOwner);
emit OwnershipTransferred(owner, proposedOwner);
owner = proposedOwner;
}
}
interface IPika {
function minSupply() external returns (uint256);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount)
external
returns (bool);
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
function burn(uint256 value) external;
}
contract PikaStaking is Owned {
address public communityWallet;
uint256 public totalAmountStaked = 0;
mapping(address => uint256) public balances;
mapping(address => uint256) public claimPeriods;
IPika public pika;
uint256 public periodNonce = 0;
uint256 public periodFinish;
uint256 public minPeriodDuration = 14 days;
uint256 public rewardPerToken = 0;
uint256 public maxInitializationReward;
event Staked(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event RewardClaimed(address indexed user, uint256 amount);
event StakingPeriodStarted(uint256 totalRewardPool, uint256 periodFinish);
event MinPeriodDurationUpdated(uint256 oldDuration, uint256 newDuration);
event MaxInitializationRewardUpdated(uint256 oldValue, uint256 newValue);
constructor(address _token, address _communityWallet) {
pika = IPika(_token);
communityWallet = _communityWallet;
maxInitializationReward = 1000000000 ether;
periodFinish = block.timestamp + 3 days;
}
/**
* @notice allows a user to stake tokens
* @dev requires to claim pending rewards before being able to stake more tokens
* @param _amount of tokens to stake
*/
function stake(uint256 _amount) public {
uint256 balance = balances[msg.sender];
if (balance > 0) {
require(
claimPeriods[msg.sender] == periodNonce,
"Claim your reward before staking more tokens"
);
}
pika.transferFrom(msg.sender, address(this), _amount);
uint256 burnedAmount = (_amount * 12) / 100;
if (pika.totalSupply() - burnedAmount >= pika.minSupply()) {
pika.burn(burnedAmount);
} else {
burnedAmount = 0;
}
uint256 communityWalletAmount = (_amount * 3) / 100;
pika.transfer(communityWallet, communityWalletAmount);
uint256 userBalance = _amount - burnedAmount - communityWalletAmount;
balances[msg.sender] += userBalance;
claimPeriods[msg.sender] = periodNonce;
totalAmountStaked += userBalance;
emit Staked(msg.sender, userBalance);
}
/**
* @notice allows a user to withdraw staked tokens
* @dev unclaimed tokens cannot be claimed after withdrawal
* @dev unstakes all tokens
*/
function withdraw() public {
uint256 balance = balances[msg.sender];
balances[msg.sender] = 0;
totalAmountStaked -= balance;
pika.transfer(msg.sender, balance);
emit Withdraw(msg.sender, balance);
}
/**
* @notice claims a reward for the staked tokens
* @dev can only claim once per staking period
*/
function claimReward() public {
uint256 balance = balances[msg.sender];
require(balance > 0, "No tokens staked");
require(
claimPeriods[msg.sender] < periodNonce,
"Wait for this period to finish before claiming your reward"
);
claimPeriods[msg.sender] = periodNonce;
uint256 reward = (balance * rewardPerToken) / 1 ether;
pika.transfer(msg.sender, reward);
emit RewardClaimed(msg.sender, reward);
}
/**
* @notice returns claimable reward for a user
* @param _user to check
*/
function claimableReward(address _user) public view returns (uint256) {
if (claimPeriods[_user] == periodNonce) {
return 0;
}
return (balances[_user] * rewardPerToken) / 1 ether;
}
/**
* @notice initializes new staking claim period
* @dev requires previous staking period to be over
* @dev only callable by anyone, msg.sender receives a portion of the staking pool as a reward
*/
function initNewRewardPeriod() external {
require(
block.timestamp >= periodFinish,
"Wait for claim period to finish"
);
require(totalAmountStaked > 0, "No tokens staked in contract");
uint256 rewardPool = pika.balanceOf(address(this)) - totalAmountStaked;
uint256 initializationReward = rewardPool / 1000;
if (initializationReward > maxInitializationReward) {
initializationReward = maxInitializationReward;
}
rewardPool -= initializationReward;
pika.transfer(msg.sender, initializationReward);
rewardPerToken = (rewardPool * 1 ether) / totalAmountStaked;
periodNonce++;
periodFinish = block.timestamp + minPeriodDuration;
emit StakingPeriodStarted(rewardPool, periodFinish);
}
/**
* @notice sets a new minimum duration for each staking claim period
* @dev only callable by owner
* @param _days amount of days the new staking claim period should at least last
*/
function setMinDuration(uint256 _days) external onlyOwner {
emit MinPeriodDurationUpdated(minPeriodDuration / 1 days, _days);
minPeriodDuration = _days * 1 days;
}
/**
* @notice sets maximum initialization reward
* @dev only callable by owner
* @param _newMaxReward new maximum reward paid out by initNewRewardPeriod function
*/
function setMaxInitializationReward(uint256 _newMaxReward)
external
onlyOwner
{
emit MaxInitializationRewardUpdated(
maxInitializationReward,
_newMaxReward
);
maxInitializationReward = _newMaxReward;
}
}
{
"compilationTarget": {
"PikaStaking.sol": "PikaStaking"
},
"evmVersion": "istanbul",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 10000
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_communityWallet","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"oldValue","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"MaxInitializationRewardUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"oldDuration","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newDuration","type":"uint256"}],"name":"MinPeriodDurationUpdated","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":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"RewardClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"totalRewardPool","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"periodFinish","type":"uint256"}],"name":"StakingPeriodStarted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balances","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claimOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"claimPeriods","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claimReward","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_user","type":"address"}],"name":"claimableReward","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"communityWallet","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"initNewRewardPeriod","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"maxInitializationReward","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minPeriodDuration","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":"periodFinish","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"periodNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pika","outputs":[{"internalType":"contract IPika","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address payable","name":"_newOwner","type":"address"}],"name":"proposeOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"proposedOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardPerToken","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newMaxReward","type":"uint256"}],"name":"setMaxInitializationReward","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_days","type":"uint256"}],"name":"setMinDuration","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"totalAmountStaked","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]