Contract Name:
AegisStaking
Contract Source Code:
File 1 of 1 : AegisStaking
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
/**
* Multiple authorisation system.
*/
abstract contract Auth {
address internal owner;
mapping (address => bool) internal authorizations;
constructor(address _owner) {
owner = _owner;
authorizations[_owner] = true;
}
modifier onlyOwner() {
require(isOwner(msg.sender), "!OWNER"); _;
}
modifier authorized() {
require(isAuthorized(msg.sender), "!AUTHORIZED"); _;
}
function authorize(address adr) public onlyOwner {
authorizations[adr] = true;
}
function unauthorize(address adr) public onlyOwner {
authorizations[adr] = false;
}
function isOwner(address account) public view returns (bool) {
return account == owner;
}
function isAuthorized(address adr) public view returns (bool) {
return authorizations[adr];
}
function transferOwnership(address payable adr) public onlyOwner {
owner = adr;
authorizations[adr] = true;
emit OwnershipTransferred(adr);
}
event OwnershipTransferred(address owner);
}
/**
* @dev Representation of a fraction
*/
struct Fraction {
uint16 numerator;
uint16 denominator;
}
/**
* @dev Describes each possible pool status: Inactive it did not begin yet, active people can stake and unstake,
* finished means the staking is over but the prize has not yet been entirely given, cleared means this match is entirely over and stakes done.
*/
enum Status {
INACTIVE,
ACTIVE,
FINISHED,
CLEARED
}
/**
* @dev There are two pools to bet on when staking, pool A and pool B.
* pool A is the ID 0 and pool B is the id 1.
*/
struct Match {
bool stakingEnabled;
int8 winningPool;
mapping (uint256 => uint256) poolStakedTokens;
mapping (uint256 => mapping (address => uint256)) poolStakes;
Status matchStatus;
uint256 totalPrize;
uint256 winningPoolTokens;
uint256 iteration;
}
contract AegisStaking is Auth {
address public stakingToken;
Fraction internal depositFee = Fraction(1, 4);
address internal feeReceiver;
Match internal stakingMatch;
uint256 internal reentrancyStatus;
event Stake(address indexed staker, uint256 indexed poolId, uint256 amount);
event Unstake(address indexed staker, uint256 indexed poolId, uint256 amount);
event WinningPool(uint256 indexed poolId);
event NewStakingPools();
event StakingOpen();
event StakingClosed();
constructor(address t, address fr) Auth(msg.sender) {
stakingToken = t;
feeReceiver = fr;
stakingMatch.winningPool = -1;
stakingMatch.matchStatus = Status.ACTIVE;
stakingMatch.stakingEnabled = true;
stakingMatch.iteration = 1;
}
modifier validPool(uint256 poolId) {
require(poolId == 0 || poolId == 1, "Only valid pool IDs are 0 and 1.");
_;
}
modifier validTokenAmount(uint256 amount) {
require(amount > 0, "Amount needs to be bigger than 0");
_;
}
modifier stakingIsEnabled {
require(stakingMatch.stakingEnabled, "Staking is not currently enabled.");
_;
}
modifier nonReentrant() {
require(reentrancyStatus == 0, "Reentrant call");
reentrancyStatus = 1;
_;
reentrancyStatus = 0;
}
/**
* @dev Add your betting stake to a pool.
*/
function stake(uint256 externalPoolId, uint256 amount) external validPool(externalPoolId) validTokenAmount(amount) stakingIsEnabled nonReentrant {
// Transfer tokens from the owner to the staking contract.
IERC20(stakingToken).transferFrom(msg.sender, address(this), amount);
// If appliable, get the deposit fee and send to the set receiver.
uint256 toStake = amount;
Fraction memory df = depositFee;
if (df.numerator > 0 && df.denominator > 0) {
uint256 fee = amount * df.numerator / df.denominator;
toStake = amount - fee;
IERC20(stakingToken).transfer(feeReceiver, fee);
}
uint256 realPoolId = getRealPoolID(externalPoolId);
// Add stake to corresponding pool.
stakeFor(msg.sender, realPoolId, toStake);
emit Stake(msg.sender, externalPoolId, toStake);
}
function stakeFor(address staker, uint256 realPoolId, uint256 amount) internal {
stakingMatch.poolStakes[realPoolId][staker] += amount;
stakingMatch.poolStakedTokens[realPoolId] += amount;
}
function getRealPoolID(uint256 externalPoolId) internal view returns (uint256) {
uint256 realPoolId;
if (externalPoolId == 0) {
realPoolId = getPoolAID();
} else {
realPoolId = getPoolBID();
}
return realPoolId;
}
/**
* @dev Unstake to remove part or the entirety of a stake.
*/
function unstake(uint256 externalPoolId, uint256 amount) external validPool(externalPoolId) validTokenAmount(amount) stakingIsEnabled nonReentrant {
uint256 realPoolId = getRealPoolID(externalPoolId);
uint256 toUnstake = unstakeFor(msg.sender, realPoolId, amount);
if (toUnstake > 0) {
IERC20(stakingToken).transfer(msg.sender, toUnstake);
emit Unstake(msg.sender, externalPoolId, toUnstake);
}
}
function unstakeFor(address staker, uint256 realPoolId, uint256 amount) internal returns (uint256) {
// Check staked tokens status and update the amount.
uint256 toUnstake = amount;
if (stakingMatch.poolStakes[realPoolId][staker] == 0) {
return 0;
}
// If attempting to unstake more than staked simply unstake all at once.
if (amount > stakingMatch.poolStakes[realPoolId][staker]) {
toUnstake = stakingMatch.poolStakes[realPoolId][staker];
}
stakingMatch.poolStakes[realPoolId][staker] -= toUnstake;
stakingMatch.poolStakedTokens[realPoolId] -= toUnstake;
return toUnstake;
}
function setWinningPool(uint256 externalPoolId) external validPool(externalPoolId) authorized {
require(stakingMatch.matchStatus == Status.INACTIVE, "The staking must have been closed before picking a winning pool.");
stakingMatch.matchStatus = Status.FINISHED;
stakingMatch.winningPool = int8(uint8(externalPoolId));
uint256 realAID = getPoolAID();
uint256 realBID = getPoolBID();
uint256 poolAStake = stakingMatch.poolStakedTokens[realAID];
uint256 poolBStake = stakingMatch.poolStakedTokens[realBID];
stakingMatch.totalPrize = externalPoolId == 0 ? poolBStake : poolAStake;
stakingMatch.winningPoolTokens = externalPoolId == 0 ? poolAStake : poolBStake;
emit WinningPool(externalPoolId);
}
/**
* @dev Sets which address receives the deposit fee.
*/
function setFeeReceiver(address newFeeReceier) external authorized {
feeReceiver = newFeeReceier;
}
function setDepositFee(uint16 numerator, uint16 denominator) external authorized {
depositFee.numerator = numerator;
depositFee.denominator = denominator;
}
function getTotalStakedTokens() public view returns (uint256) {
uint256 realAID = getPoolAID();
uint256 realBID = getPoolBID();
uint256 poolAStake = stakingMatch.poolStakedTokens[realAID];
uint256 poolBStake = stakingMatch.poolStakedTokens[realBID];
return poolAStake + poolBStake;
}
function getPoolAStakedTokens() external view returns (uint256) {
uint256 poolAID = getPoolAID();
return stakingMatch.poolStakedTokens[poolAID];
}
function getPoolBStakedTokens() external view returns (uint256) {
uint256 poolBID = getPoolBID();
return stakingMatch.poolStakedTokens[poolBID];
}
function getUserStakedTokens(address user, uint256 externalPoolId) public view returns (uint256) {
uint256 realPoolId = getRealPoolID(externalPoolId);
return stakingMatch.poolStakes[realPoolId][user];
}
function isStakingEnabled() external view returns (bool) {
return stakingMatch.stakingEnabled;
}
function getCurrentStakingStatus() public view returns (Status) {
return stakingMatch.matchStatus;
}
function getTotalPrize() external view returns (uint256) {
return stakingMatch.totalPrize;
}
function getWinningPoolTokens() external view returns (uint256) {
return stakingMatch.winningPoolTokens;
}
function getWinningPool() external view returns (int8) {
return stakingMatch.winningPool;
}
function setStakingEnabled(bool enabled) external authorized {
if (enabled) {
// Enable staking from either uninitialised state or restarting the stake before a winner is picked.
require(stakingMatch.matchStatus == Status.INACTIVE, "Staking must be inactive.");
stakingMatch.stakingEnabled = true;
stakingMatch.matchStatus = Status.ACTIVE;
emit StakingOpen();
} else {
// Turn staking off in order to decide a winning pool or to temporarily pause the stake process.
require(stakingMatch.matchStatus == Status.ACTIVE, "Staking must be active.");
stakingMatch.stakingEnabled = false;
stakingMatch.matchStatus = Status.INACTIVE;
emit StakingClosed();
}
}
function forcePayout(address staker) external authorized {
processPayout(staker);
}
function claimPrize() external {
processPayout(msg.sender);
}
/**
* @dev Claim the stake and prize from a winning pool stake.
*/
function processPayout(address staker) internal nonReentrant {
require(stakingMatch.matchStatus == Status.FINISHED, "Winning bets can only be claimed when a winning pool has been decided.");
uint256 win = uint256(uint8(stakingMatch.winningPool));
require(win == 0 || win == 1, "Winning pool is not correctly set!");
uint256 stakedTokens = getUserStakedTokens(staker, win);
require(stakedTokens > 0, "No stake on winning pool.");
uint256 prize = getUserPrize(staker);
require(prize > 0, "No prize to claim.");
uint256 toGive = prize + stakedTokens;
// Reset staking for user.
uint256 realPoolId = getRealPoolID(win);
stakingMatch.poolStakes[realPoolId][staker] = 0;
stakingMatch.poolStakedTokens[realPoolId] -= stakedTokens;
if (stakingMatch.poolStakedTokens[realPoolId] == 0) {
stakingMatch.matchStatus = Status.CLEARED;
}
IERC20(stakingToken).transfer(staker, toGive);
}
function resetStaking() external authorized {
require(
stakingMatch.matchStatus == Status.CLEARED || getTotalStakedTokens() == 0,
"New staking can only be started when previous one is cleared."
);
stakingMatch.winningPool = -1;
stakingMatch.matchStatus = Status.INACTIVE;
stakingMatch.iteration += 1;
stakingMatch.stakingEnabled = false;
stakingMatch.totalPrize = 0;
stakingMatch.winningPoolTokens = 0;
emit NewStakingPools();
}
/**
* @dev Gets the price for a specific staker.
*/
function getUserPrize(address staker) public view returns (uint256) {
uint256 stakedTokens;
uint256 realPoolID;
int8 win = stakingMatch.winningPool;
// Wrongly set winning pool.
if (win < 0 || win > 1) {
return 0;
}
// Winner is pool A.
if (win == 0) {
realPoolID = getPoolAID();
} else {
// Winner is pool B.
realPoolID = getPoolBID();
}
stakedTokens = stakingMatch.poolStakes[realPoolID][staker];
if (stakedTokens > 0) {
return calculatePrize(stakedTokens, stakingMatch.winningPoolTokens, stakingMatch.totalPrize);
}
return 0;
}
/**
* @dev Calculates a price from the total prize and stake in a pool.
*/
function calculatePrize(uint256 stakedTokens, uint256 totalPoolStake, uint256 prize) public pure returns (uint256) {
if (stakedTokens == 0 || totalPoolStake == 0 || prize == 0) {
return 0;
}
// Factor used to avoid losing digits to rounding.
uint256 factor = 10000;
uint256 part = stakedTokens * factor / totalPoolStake;
return part * prize / factor;
}
function recoverDust() external authorized {
require(stakingMatch.matchStatus == Status.CLEARED, "Requires all prizes to have been given out.");
IERC20 st = IERC20(stakingToken);
st.transfer(feeReceiver, st.balanceOf(address(this)));
}
/**
* @dev Internal pool IDs. Public IDs are always 0 for A and 1 for B.
*/
function getPoolAID() public view returns (uint256) {
return stakingMatch.iteration * 2 - 1;
}
function getPoolBID() public view returns (uint256) {
return stakingMatch.iteration * 2;
}
function getCurrentIteration() external view returns (uint256) {
return stakingMatch.iteration;
}
}