// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP. Does not include
* the optional functions; to access them see `ERC20Detailed`.
*/
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.
*
* > 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.0;
import "./IERC20.sol";
contract StakingCampaign {
struct StackingInfo {
uint seq;
uint amount;
uint reward;
bool isPayout;
uint unlockTime;
}
event Deposited (
address indexed sender,
uint seq,
uint amount,
uint256 timestamp
);
event Claimed (
address indexed sender,
uint seq,
uint amount,
uint reward,
uint256 timestamp
);
address public owner;
modifier onlyAdmin {
require(msg.sender == owner, 'Caller is not owner');
_;
}
// ERC20 token for staking campaign
IERC20 public token;
// campaign name
string public name;
// total day for staking (in second)
uint public duration;
// annual percentage rate
uint public apr;
// total cap for campaign, stop campaign if cap is reached
uint public maxCap;
// expired time of campaign, no more staking is accepted (in second)
uint public expiredTime;
// min amount for one staking deposit
uint public minTransactionAmount;
// max amount for one staking deposit
uint public maxTransactionAmount;
// total amount already payout for staker (payout = staking amount + reward)
uint public totalPayoutAmount;
// total reward need for campaign
uint public totalCampaignReward;
// total staked amount
uint public totalStakedAmount;
//
bool public isMaxCapReached = false;
mapping(address => StackingInfo[]) internal stakingList;
/**
*
*/
constructor (IERC20 _token, string memory _campaignName, uint _expiredTime,
uint _maxCap, uint _maxTransactionAmount, uint _minTransactionAmount,
uint _duration, uint _apr) {
owner = msg.sender;
token = _token;
name = _campaignName;
expiredTime = block.timestamp + _expiredTime;
maxCap = _maxCap;
maxTransactionAmount = _maxTransactionAmount;
minTransactionAmount = _minTransactionAmount;
duration = _duration;
apr = _apr;
}
/**
* Deposit amount of token to stack
*/
function deposit(uint _amount, address _userAddr) external {
require(totalStakedAmount + _amount <= maxCap, "Total cap is reached");
require(_amount >= minTransactionAmount, "Staking amount is too small");
require(_amount <= maxTransactionAmount, "Staking amount is too big");
require(block.timestamp < expiredTime, "Campaign is over");
token.transferFrom(_userAddr, address(this), _amount);
uint unlockTime = block.timestamp + duration;
uint seq = stakingList[_userAddr].length + 1;
uint reward = _amount*apr*duration/(365*24*60*60*100);
StackingInfo memory staking = StackingInfo(seq, _amount, reward, false, unlockTime);
stakingList[_userAddr].push(staking);
totalStakedAmount += _amount;
totalCampaignReward += reward;
isMaxCapReached = (totalStakedAmount == maxCap || totalStakedAmount + minTransactionAmount > maxCap);
emit Deposited(_userAddr, seq, _amount, block.timestamp);
}
function claim(uint _seq, address _userAddr) public {
StackingInfo[] memory userStakings = stakingList[_userAddr];
require(_seq > 0 && userStakings.length >= _seq, "Invalid index");
uint idx = _seq - 1;
StackingInfo memory staking = userStakings[idx];
require(!staking.isPayout, "Stake is already payout");
require(staking.unlockTime < block.timestamp, "Staking is in lock period");
uint payout = staking.amount + staking.reward;
token.transfer(_userAddr, payout);
totalPayoutAmount += payout;
stakingList[_userAddr][idx].isPayout = true;
emit Claimed(_userAddr, _seq, staking.amount, staking.reward, block.timestamp);
}
function claimRemainingReward(address _userAddr) public onlyAdmin {
require(block.timestamp > expiredTime, "Campaign is not over yet");
uint remainingPayoutAmount = totalStakedAmount + totalCampaignReward - totalPayoutAmount;
uint balance = token.balanceOf(address(this));
token.transfer(_userAddr, balance - remainingPayoutAmount);
}
function getClaimableRemainningReward() public view returns (uint) {
if(block.timestamp < expiredTime) return 0;
else {
uint remainingPayoutAmount = totalStakedAmount + totalCampaignReward - totalPayoutAmount;
uint balance = token.balanceOf(address(this));
return balance - remainingPayoutAmount;
}
}
function getStakings(address _staker) public view returns (uint[] memory _seqs, uint[] memory _amounts, uint[] memory _rewards, bool[] memory _isPayouts, uint[] memory _timestamps) {
StackingInfo[] memory userStakings = stakingList[_staker];
uint length = userStakings.length;
uint256[] memory seqList = new uint256[](length);
uint256[] memory amountList = new uint256[](length);
uint256[] memory rewardList = new uint256[](length);
bool[] memory isPayoutList = new bool[](length);
uint256[] memory timeList = new uint256[](length);
for(uint idx = 0; idx < length; idx++) {
StackingInfo memory stackingInfo = userStakings[idx];
seqList[idx] = stackingInfo.seq;
amountList[idx] = stackingInfo.amount;
rewardList[idx] = stackingInfo.reward;
isPayoutList[idx] = stackingInfo.isPayout;
timeList[idx] = stackingInfo.unlockTime;
}
return (seqList, amountList, rewardList, isPayoutList, timeList);
}
function getCampaignInfo() public view returns (
IERC20 _token, string memory _campaignName, uint _expiredTime,
uint _maxCap, uint _maxTransactionAmount, uint _minTransactionAmount,
uint _duration, uint _apr, uint _stakedAmount,uint _totalPayoutAmount) {
return (token, name, expiredTime, maxCap, maxTransactionAmount, minTransactionAmount, duration, apr, totalStakedAmount, totalPayoutAmount);
}
function transferOwnership(address _newOwner) public onlyAdmin {
owner = _newOwner;
}
}
{
"compilationTarget": {
"StakingCampaign.sol": "StakingCampaign"
},
"evmVersion": "istanbul",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"contract IERC20","name":"_token","type":"address"},{"internalType":"string","name":"_campaignName","type":"string"},{"internalType":"uint256","name":"_expiredTime","type":"uint256"},{"internalType":"uint256","name":"_maxCap","type":"uint256"},{"internalType":"uint256","name":"_maxTransactionAmount","type":"uint256"},{"internalType":"uint256","name":"_minTransactionAmount","type":"uint256"},{"internalType":"uint256","name":"_duration","type":"uint256"},{"internalType":"uint256","name":"_apr","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"seq","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"reward","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Claimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"seq","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Deposited","type":"event"},{"inputs":[],"name":"apr","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_seq","type":"uint256"},{"internalType":"address","name":"_userAddr","type":"address"}],"name":"claim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_userAddr","type":"address"}],"name":"claimRemainingReward","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"address","name":"_userAddr","type":"address"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"duration","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"expiredTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCampaignInfo","outputs":[{"internalType":"contract IERC20","name":"_token","type":"address"},{"internalType":"string","name":"_campaignName","type":"string"},{"internalType":"uint256","name":"_expiredTime","type":"uint256"},{"internalType":"uint256","name":"_maxCap","type":"uint256"},{"internalType":"uint256","name":"_maxTransactionAmount","type":"uint256"},{"internalType":"uint256","name":"_minTransactionAmount","type":"uint256"},{"internalType":"uint256","name":"_duration","type":"uint256"},{"internalType":"uint256","name":"_apr","type":"uint256"},{"internalType":"uint256","name":"_stakedAmount","type":"uint256"},{"internalType":"uint256","name":"_totalPayoutAmount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getClaimableRemainningReward","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_staker","type":"address"}],"name":"getStakings","outputs":[{"internalType":"uint256[]","name":"_seqs","type":"uint256[]"},{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"uint256[]","name":"_rewards","type":"uint256[]"},{"internalType":"bool[]","name":"_isPayouts","type":"bool[]"},{"internalType":"uint256[]","name":"_timestamps","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isMaxCapReached","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxCap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxTransactionAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minTransactionAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalCampaignReward","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalPayoutAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalStakedAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]