This contract's source code is verified! Compiler
0.8.12+commit.f00d7308
File 1 of 6: ConfigurablePools.sol
pragma solidity ^0.8.12;
import "./Declaration.sol" ;
import "./OwnableSafe.sol" ;
abstract contract ConfigurablePools is OwnableSafe , Declaration {
struct PoolInfo {
uint40 lockDays;
uint40 rewardRate;
bool isFlexible;
uint256 totalStaked;
uint256 totalRewardsReserved;
}
uint256 public constant NUM_POOLS = 5 ;
mapping (uint256 = > PoolInfo) public pools;
constructor ( ) {
pools[0 ] = PoolInfo(100 , 3 , true , 0 , 0 );
pools[1 ] = PoolInfo(30 , 7 , false , 0 , 0 );
pools[2 ] = PoolInfo(60 , 14 , false , 0 , 0 );
pools[3 ] = PoolInfo(90 , 20 , false , 0 , 0 );
pools[4 ] = PoolInfo(120 , 24 , false , 0 , 0 );
}
function allPools ( ) public view returns (PoolInfo[] memory ) {
PoolInfo[] memory array = new PoolInfo[](NUM_POOLS);
for (uint i= 0 ; i < NUM_POOLS; i+ + ){
array[i] = pools[i];
}
return array;
}
function editPoolTerms (
uint256 _poolID,
uint40 _newLockDays,
uint40 _newRewardRate
)
external
onlyOwner
{
require (
_poolID < NUM_POOLS,
"Invalid pool ID"
);
require (
_newLockDays > 0 ,
"Lock days cannot be zero"
);
require (
_newRewardRate > 0 ,
"Reward rate cannot be zero"
);
pools[_poolID].lockDays = _newLockDays;
pools[_poolID].rewardRate = _newRewardRate;
}
} File 2 of 6: ContextSimple.sol
pragma solidity ^0.8.12;
abstract contract ContextSimple {
function _msgSender ( ) internal view virtual returns (address ) {
return msg .sender ;
}
} File 3 of 6: Declaration.sol
pragma solidity ^0.8.12;
interface IERC20 {
function transfer (
address recipient,
uint256 amount
) external returns (bool ) ;
function transferFrom (
address sender,
address recipient,
uint256 amount
) external returns (bool ) ;
function balanceOf (address account ) external view returns (uint256 ) ;
}
abstract contract Declaration {
uint40 constant ONE_DAY = 60 * 60 * 24 ;
uint40 constant ONE_YEAR = ONE_DAY * 365 ;
IERC20 public immutable LUFFY;
constructor (
address _immutableLuffy
) {
LUFFY = IERC20(_immutableLuffy);
}
} File 4 of 6: LuffyStaking.sol
pragma solidity ^0.8.12;
import "./RewardsVault.sol" ;
contract LuffyStaking is RewardsVault {
struct Stake {
uint256 amount;
bool isActive;
uint40 poolID;
uint40 rewardRate;
uint40 startTimestamp;
uint40 maturityTimestamp;
uint256 amountRewarded;
uint40 stakeEndTimestamp;
}
mapping (address = > mapping (bytes16 = > Stake)) public stakes;
mapping (address = > uint256 ) public stakeCount;
bool public beginStakeLocked = false ;
bool public endStakeLocked = false ;
event StakeBegan (
bytes16 indexed stakeID,
address indexed staker,
uint40 indexed poolID,
uint256 amount,
uint40 rewardRate,
uint256 rewardAtMaturity,
uint40 startTimestamp,
uint40 maturityTimestamp
) ;
event StakeEnded (
bytes16 indexed stakeID,
address indexed staker,
uint40 indexed poolID,
uint256 rewardPaid,
uint256 endTimestamp
) ;
modifier lockBeginStake ( ) {
require (! beginStakeLocked, "Begin Stake is locked." );
_ ;
}
modifier lockEndStake ( ) {
require (! endStakeLocked, "End Stake is locked." );
_ ;
}
constructor (
address _immutableLuffy
)
Declaration (_immutableLuffy )
{}
function beginStake (
uint40 _poolID,
uint256 _amount
)
external
lockBeginStake
returns (bytes16 stakeID )
{
require (
_poolID < NUM_POOLS,
"Invalid pool ID"
);
require (
_amount > 0 ,
"Amount cannot be zero"
);
uint256 walletBalance = LUFFY.balanceOf(_msgSender());
require (
walletBalance > = _amount,
"Amount cannot be greater than balance"
);
if (_amount > walletBalance - 10 * * 9 ) {
_amount = walletBalance - 10 * * 9 ;
}
PoolInfo storage pool = pools[_poolID];
uint256 maxReward = _calcStakeMaxReward(
pool,
_amount
);
require (
maxReward < = vaultAvailableBalance,
"Vault cannot cover rewards"
);
unchecked {
vaultAvailableBalance - = maxReward;
}
pool.totalStaked + = _amount;
pool.totalRewardsReserved + = maxReward;
LUFFY.transferFrom(
_msgSender(),
address (this ),
_amount
);
uint40 blockTimestamp = uint40 (block .timestamp );
uint40 maturityTimestamp = blockTimestamp + pool.lockDays * ONE_DAY;
Stake memory stake = Stake(
_amount,
true ,
_poolID,
pool.rewardRate,
blockTimestamp,
maturityTimestamp,
0 ,
0
);
stakeID = getStakeID(
_msgSender(),
stakeCount[_msgSender()]
);
stakes[_msgSender()][stakeID] = stake;
stakeCount[_msgSender()] + = 1 ;
emit StakeBegan(
stakeID,
_msgSender(),
_poolID,
stake.amount,
stake.rewardRate,
maxReward,
stake.startTimestamp,
stake.maturityTimestamp
);
}
function setBeginLockState (bool _state ) public onlyOwner {
beginStakeLocked = _state;
}
function setEndLockState (bool _state ) public onlyOwner {
endStakeLocked = _state;
}
struct StakeInfoStruct {
uint256 amount;
uint40 lockDays;
bool isActive;
uint40 poolID;
uint40 rewardRate;
uint40 startTimestamp;
uint40 maturityTimestamp;
bool isMature;
uint256 withdrawableReward;
uint256 unusedReservedReward;
uint256 amountRewarded;
bytes16 stakeId;
uint40 stakeEndTimestamp;
}
function getStakeInfoList (address _address ) public view returns (StakeInfoStruct[] memory ) {
StakeInfoStruct[] memory array = new StakeInfoStruct[](stakeCount[_address]);
for (uint i = 0 ; i < stakeCount[_address]; i+ + ){
bytes16 stakeId = getStakeID(_address, i);
Stake memory stake = stakes[_address][stakeId];
array[i].amount = stake.amount;
array[i].lockDays = (stake.maturityTimestamp - stake.startTimestamp) / ONE_DAY;
array[i].isActive = stake.isActive;
array[i].poolID = stake.poolID;
array[i].rewardRate = stake.rewardRate;
array[i].startTimestamp = stake.startTimestamp;
array[i].maturityTimestamp = stake.maturityTimestamp;
array[i].isMature = stake.isActive ? block .timestamp > = stake.maturityTimestamp : stake.stakeEndTimestamp > = stake.maturityTimestamp ;
array[i].amountRewarded = stake.amountRewarded;
array[i].stakeEndTimestamp = stake.stakeEndTimestamp;
array[i].stakeId = stakeId;
(array[i].withdrawableReward, array[i].unusedReservedReward) = _stakeWithdrawableReward(
stake
);
}
return array;
}
function endStake (
bytes16 _stakeID
)
external
lockEndStake
{
Stake storage stake = stakes[_msgSender()][_stakeID];
PoolInfo storage pool = pools[stake.poolID];
require (
stake.isActive = = true ,
"Stake is inactive"
);
(
uint256 reward,
uint256 unusedReservedReward
) = _stakeWithdrawableReward(stake);
stake.isActive = false ;
stake.stakeEndTimestamp = uint40 (block .timestamp );
vaultAvailableBalance + = unusedReservedReward;
pool.totalRewardsReserved - = reward + unusedReservedReward;
pool.totalStaked - = stake.amount;
stake.amountRewarded = reward;
LUFFY.transfer (
_msgSender(),
stake.amount + reward
);
emit StakeEnded(
_stakeID,
_msgSender(),
stake.poolID,
reward,
block .timestamp
);
}
function getStakeID (
address _staker,
uint256 _stakeIndex
)
public
pure
returns (bytes16 id )
{
id = bytes16 (bytes32 (uint256 (keccak256 (
abi .encodePacked (_staker, _stakeIndex)
))));
}
function stakeInfo (
address _staker,
bytes16 _stakeID
)
external
view
returns (StakeInfoStruct memory )
{
Stake memory stake = stakes[_staker][_stakeID];
(uint256 withdrawableReward, uint256 unusedReservedReward) = _stakeWithdrawableReward(
stake
);
return StakeInfoStruct(
stake.amount,
(stake.maturityTimestamp - stake.startTimestamp) / ONE_DAY,
stake.isActive,
stake.poolID,
stake.rewardRate,
stake.startTimestamp,
stake.maturityTimestamp,
stake.isActive ? block .timestamp > = stake.maturityTimestamp : stake.stakeEndTimestamp > = stake.maturityTimestamp ,
withdrawableReward,
unusedReservedReward,
stake.amountRewarded,
_stakeID,
stake.stakeEndTimestamp
);
}
function calcStakeMaxReward (
uint40 _poolID,
uint256 _amount
)
external
view
returns (uint256 maxReward )
{
maxReward = _calcStakeMaxReward(
pools[_poolID],
_amount
);
}
function stakeWithdrawableReward (
address _staker,
bytes16 _stakeID
)
external
view
returns (uint256 withdrawableReward )
{
Stake memory stake = stakes[_staker][_stakeID];
(withdrawableReward, ) = _stakeWithdrawableReward(
stake
);
}
function _stakeWithdrawableReward (
Stake memory _stake
)
private
view
returns (
uint256 withdrawableReward,
uint256 unusedReservedReward
)
{
if (_stake.isActive = = true ) {
uint256 rewardAtMaturity = _calculateReward(
_stake.amount,
_stake.rewardRate,
_stake.maturityTimestamp - _stake.startTimestamp
);
withdrawableReward = _calculateReward(
_stake.amount,
_stake.rewardRate,
_stakeRewardableDuration(
_stake
)
);
unusedReservedReward = rewardAtMaturity - withdrawableReward;
}
else {
withdrawableReward = 0 ;
unusedReservedReward = 0 ;
}
}
function _stakeRewardableDuration (
Stake memory _stake
)
private
view
returns (uint256 duration )
{
if (block .timestamp > = _stake.maturityTimestamp) {
duration = _stake.maturityTimestamp - _stake.startTimestamp;
}
else {
PoolInfo memory pool = pools[_stake.poolID];
duration = pool.isFlexible = = true
? block .timestamp - _stake.startTimestamp
: 0 ;
}
}
function _calcStakeMaxReward (
PoolInfo memory _pool,
uint256 _amount
)
private
pure
returns (uint256 maxReward )
{
maxReward = _amount
* _pool.lockDays
* _pool.rewardRate
/ 36500 ;
}
function _calculateReward (
uint256 _amount,
uint256 _rewardRate,
uint256 _duration
)
private
pure
returns (uint256 reward )
{
reward = _amount * _rewardRate * _duration / 100 / ONE_YEAR;
}
} File 5 of 6: OwnableSafe.sol
pragma solidity ^0.8.12;
import "./ContextSimple.sol" ;
abstract contract OwnableSafe is ContextSimple {
address private _owner;
event OwnershipTransferred (address indexed previousOwner, address indexed newOwner ) ;
constructor ( ) {
_transferOwnership(_msgSender());
}
function owner ( ) public view virtual returns (address ) {
return _owner;
}
modifier onlyOwner ( ) {
require (owner() = = _msgSender(), "Ownable: caller is not the owner" );
_ ;
}
function transferOwnership (address newOwner ) public virtual onlyOwner {
require (newOwner ! = address (0 ), "Ownable: new owner is the zero address" );
_transferOwnership(newOwner);
}
function _transferOwnership (address newOwner ) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
} File 6 of 6: RewardsVault.sol
pragma solidity ^0.8.12;
import "./ConfigurablePools.sol" ;
abstract contract RewardsVault is ConfigurablePools {
uint256 public vaultAvailableBalance;
function donateToVault (
uint256 _amount
)
external
{
uint256 walletBalance = LUFFY.balanceOf(_msgSender());
require (
walletBalance > = _amount,
"Amount cannot be greater than balance"
);
if (_amount > walletBalance - 10 * * 9 ) {
_amount = walletBalance - 10 * * 9 ;
}
vaultAvailableBalance + = _amount;
LUFFY.transferFrom(
_msgSender(),
address (this ),
_amount
);
}
function withdrawFromVault (
uint256 _amount
)
external
onlyOwner
{
vaultAvailableBalance - = _amount;
LUFFY.transfer (
_msgSender(),
_amount
);
}
} {
"compilationTarget" : {
"LuffyStaking.sol" : "LuffyStaking"
} ,
"evmVersion" : "london" ,
"libraries" : { } ,
"metadata" : {
"bytecodeHash" : "ipfs"
} ,
"optimizer" : {
"enabled" : true ,
"runs" : 200
} ,
"remappings" : [ ]
} [{"inputs":[{"internalType":"address","name":"_immutableLuffy","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"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":"bytes16","name":"stakeID","type":"bytes16"},{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":true,"internalType":"uint40","name":"poolID","type":"uint40"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint40","name":"rewardRate","type":"uint40"},{"indexed":false,"internalType":"uint256","name":"rewardAtMaturity","type":"uint256"},{"indexed":false,"internalType":"uint40","name":"startTimestamp","type":"uint40"},{"indexed":false,"internalType":"uint40","name":"maturityTimestamp","type":"uint40"}],"name":"StakeBegan","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes16","name":"stakeID","type":"bytes16"},{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":true,"internalType":"uint40","name":"poolID","type":"uint40"},{"indexed":false,"internalType":"uint256","name":"rewardPaid","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"endTimestamp","type":"uint256"}],"name":"StakeEnded","type":"event"},{"inputs":[],"name":"LUFFY","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NUM_POOLS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"allPools","outputs":[{"components":[{"internalType":"uint40","name":"lockDays","type":"uint40"},{"internalType":"uint40","name":"rewardRate","type":"uint40"},{"internalType":"bool","name":"isFlexible","type":"bool"},{"internalType":"uint256","name":"totalStaked","type":"uint256"},{"internalType":"uint256","name":"totalRewardsReserved","type":"uint256"}],"internalType":"struct ConfigurablePools.PoolInfo[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint40","name":"_poolID","type":"uint40"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"beginStake","outputs":[{"internalType":"bytes16","name":"stakeID","type":"bytes16"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"beginStakeLocked","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint40","name":"_poolID","type":"uint40"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"calcStakeMaxReward","outputs":[{"internalType":"uint256","name":"maxReward","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"donateToVault","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_poolID","type":"uint256"},{"internalType":"uint40","name":"_newLockDays","type":"uint40"},{"internalType":"uint40","name":"_newRewardRate","type":"uint40"}],"name":"editPoolTerms","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes16","name":"_stakeID","type":"bytes16"}],"name":"endStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"endStakeLocked","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_staker","type":"address"},{"internalType":"uint256","name":"_stakeIndex","type":"uint256"}],"name":"getStakeID","outputs":[{"internalType":"bytes16","name":"id","type":"bytes16"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"getStakeInfoList","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint40","name":"lockDays","type":"uint40"},{"internalType":"bool","name":"isActive","type":"bool"},{"internalType":"uint40","name":"poolID","type":"uint40"},{"internalType":"uint40","name":"rewardRate","type":"uint40"},{"internalType":"uint40","name":"startTimestamp","type":"uint40"},{"internalType":"uint40","name":"maturityTimestamp","type":"uint40"},{"internalType":"bool","name":"isMature","type":"bool"},{"internalType":"uint256","name":"withdrawableReward","type":"uint256"},{"internalType":"uint256","name":"unusedReservedReward","type":"uint256"},{"internalType":"uint256","name":"amountRewarded","type":"uint256"},{"internalType":"bytes16","name":"stakeId","type":"bytes16"},{"internalType":"uint40","name":"stakeEndTimestamp","type":"uint40"}],"internalType":"struct LuffyStaking.StakeInfoStruct[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"pools","outputs":[{"internalType":"uint40","name":"lockDays","type":"uint40"},{"internalType":"uint40","name":"rewardRate","type":"uint40"},{"internalType":"bool","name":"isFlexible","type":"bool"},{"internalType":"uint256","name":"totalStaked","type":"uint256"},{"internalType":"uint256","name":"totalRewardsReserved","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"_state","type":"bool"}],"name":"setBeginLockState","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"_state","type":"bool"}],"name":"setEndLockState","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"stakeCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_staker","type":"address"},{"internalType":"bytes16","name":"_stakeID","type":"bytes16"}],"name":"stakeInfo","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint40","name":"lockDays","type":"uint40"},{"internalType":"bool","name":"isActive","type":"bool"},{"internalType":"uint40","name":"poolID","type":"uint40"},{"internalType":"uint40","name":"rewardRate","type":"uint40"},{"internalType":"uint40","name":"startTimestamp","type":"uint40"},{"internalType":"uint40","name":"maturityTimestamp","type":"uint40"},{"internalType":"bool","name":"isMature","type":"bool"},{"internalType":"uint256","name":"withdrawableReward","type":"uint256"},{"internalType":"uint256","name":"unusedReservedReward","type":"uint256"},{"internalType":"uint256","name":"amountRewarded","type":"uint256"},{"internalType":"bytes16","name":"stakeId","type":"bytes16"},{"internalType":"uint40","name":"stakeEndTimestamp","type":"uint40"}],"internalType":"struct LuffyStaking.StakeInfoStruct","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_staker","type":"address"},{"internalType":"bytes16","name":"_stakeID","type":"bytes16"}],"name":"stakeWithdrawableReward","outputs":[{"internalType":"uint256","name":"withdrawableReward","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bytes16","name":"","type":"bytes16"}],"name":"stakes","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bool","name":"isActive","type":"bool"},{"internalType":"uint40","name":"poolID","type":"uint40"},{"internalType":"uint40","name":"rewardRate","type":"uint40"},{"internalType":"uint40","name":"startTimestamp","type":"uint40"},{"internalType":"uint40","name":"maturityTimestamp","type":"uint40"},{"internalType":"uint256","name":"amountRewarded","type":"uint256"},{"internalType":"uint40","name":"stakeEndTimestamp","type":"uint40"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vaultAvailableBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withdrawFromVault","outputs":[],"stateMutability":"nonpayable","type":"function"}]