Magic Dragon Dao Update
42 removals
666 lines
31 additions
652 lines
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
pragma solidity ^0.8.10;
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "treasure-staking/contracts/AtlasMine.sol";
import "treasure-staking/contracts/AtlasMine.sol";
import "./interfaces/IAtlasMineStaker.sol";
import "./interfaces/IAtlasMineStaker.sol";
/**
/**
* @title AtlasMineStaker
* @title AtlasMineStaker
* @author kvk0x
* @author kvk0x
*
*
* Dragon of the Magic Dragon DAO - A Tempting Offer.
* Dragon of the Magic Dragon DAO - A Tempting Offer.
*
*
* Staking pool contract for the Bridgeworld Atlas Mine.
* Staking pool contract for the Bridgeworld Atlas Mine.
* Wraps existing staking with a defined 'lock time' per contract.
* Wraps existing staking with a defined 'lock time' per contract.
*
*
* Better than solo staking since a designated 'hoard' can also
* Better than solo staking since a designated 'hoard' can also
* deposit Treasures and Legions for staking boosts. Anyone can
* deposit Treasures and Legions for staking boosts. Anyone can
* enjoy the power of the guild's hoard and maximize their
* enjoy the power of the guild's hoard and maximize their
* Atlas Mine yield.
* Atlas Mine yield.
*
*
*/
*/
contract AtlasMineStakerUpgradeable is
contract AtlasMineStakerUpgradeable is
IAtlasMineStaker,
IAtlasMineStaker,
Initializable,
Initializable,
OwnableUpgradeable,
OwnableUpgradeable,
ERC1155HolderUpgradeable,
ERC1155HolderUpgradeable,
ERC721HolderUpgradeable,
ERC721HolderUpgradeable,
ReentrancyGuardUpgradeable
ReentrancyGuardUpgradeable
{
{
using SafeERC20Upgradeable for IERC20Upgradeable;
using SafeERC20Upgradeable for IERC20Upgradeable;
using AddressUpgradeable for address;
using AddressUpgradeable for address;
using SafeCastUpgradeable for uint256;
using SafeCastUpgradeable for uint256;
using SafeCastUpgradeable for int256;
using SafeCastUpgradeable for int256;
using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet;
using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet;
// ============================================ STATE ==============================================
// ============================================ STATE ==============================================
// ============= Global Immutable State ==============
// ============= Global Immutable State ==============
/// @notice MAGIC token
/// @notice MAGIC token
/// @dev functionally immutable
/// @dev functionally immutable
IERC20Upgradeable public magic;
IERC20Upgradeable public magic;
/// @notice The AtlasMine
/// @notice The AtlasMine
/// @dev functionally immutable
/// @dev functionally immutable
AtlasMine public mine;
AtlasMine public mine;
/// @notice The defined lock cycle for the contract
/// @notice The defined lock cycle for the contract
/// @dev functionally immutable
/// @dev functionally immutable
AtlasMine.Lock public lock;
AtlasMine.Lock public lock;
/// @notice The defined lock time for the contract
/// @notice The defined lock time for the contract
/// @dev functionally immutable
/// @dev functionally immutable
uint256 public locktime;
uint256 public locktime;
// ============= Global Staking State ==============
// ============= Global Staking State ==============
uint256 public constant ONE = 1e30;
uint256 public constant ONE = 1e30;
/// @notice Whether new stakes will get staked on the contract as scheduled. For emergencies
/// @notice Whether new stakes will get staked on the contract as scheduled. For emergencies
bool public schedulePaused;
bool public schedulePaused;
/// @notice Deposited, but unstaked tokens, keyed by the day number since epoch
/// @notice Deposited, but unstaked tokens, keyed by the day number since epoch
/// @notice DEPRECATED ON UPGRADE
/// @notice DEPRECATED ON UPGRADE
mapping(uint256 => uint256) public pendingStakes;
mapping(uint256 => uint256) public pendingStakes;
/// @notice Last time pending stakes were deposited
/// @notice Last time pending stakes were deposited
uint256 public lastStakeTimestamp;
uint256 public lastStakeTimestamp;
/// @notice The minimum amount of time between atlas mine stakes
/// @notice The minimum amount of time between atlas mine stakes
uint256 public minimumStakingWait;
uint256 public minimumStakingWait;
/// @notice The total amount of staked token
/// @notice The total amount of staked token
uint256 public totalStaked;
uint256 public totalStaked;
/// @notice All stakes currently active
/// @notice All stakes currently active
Stake[] public stakes;
Stake[] public stakes;
/// @notice Deposit ID of last stake. Also tracked in atlas mine
/// @notice Deposit ID of last stake. Also tracked in atlas mine
uint256 public lastDepositId;
uint256 public lastDepositId;
/// @notice Total MAGIC rewards earned by staking.
/// @notice Total MAGIC rewards earned by staking.
uint256 public override totalRewardsEarned;
uint256 public override totalRewardsEarned;
/// @notice Rewards accumulated per share
/// @notice Rewards accumulated per share
uint256 public accRewardsPerShare;
uint256 public accRewardsPerShare;
// ============= User Staking State ==============
// ============= User Staking State ==============
/// @notice Each user stake, keyed by user address => deposit ID
/// @notice Each user stake, keyed by user address => deposit ID
mapping(address => mapping(uint256 => UserStake)) public userStake;
mapping(address => mapping(uint256 => UserStake)) public userStake;
/// @notice All deposit IDs for a user, enumerated
/// @notice All deposit IDs for a user, enumerated
mapping(address => EnumerableSetUpgradeable.UintSet) private allUserDepositIds;
mapping(address => EnumerableSetUpgradeable.UintSet) private allUserDepositIds;
/// @notice The current ID of the user's last deposited stake
/// @notice The current ID of the user's last deposited stake
mapping(address => uint256) public currentId;
mapping(address => uint256) public currentId;
// ============= NFT Boosting State ==============
// ============= NFT Boosting State ==============
/// @notice Holder of treasures and legions
/// @notice Holder of treasures and legions
mapping(address => bool) private hoards;
mapping(address => bool) private hoards;
/// @notice Legions staked by hoard users
/// @notice Legions staked by hoard users
mapping(uint256 => address) public legionsStaked;
mapping(uint256 => address) public legionsStaked;
/// @notice Treasures staked by hoard users
/// @notice Treasures staked by hoard users
mapping(uint256 => mapping(address => uint256)) public treasuresStaked;
mapping(uint256 => mapping(address => uint256)) public treasuresStaked;
// ============= Operator State ==============
// ============= Operator State ==============
/// @notice Fee to contract operator. Only assessed on rewards.
/// @notice Fee to contract operator. Only assessed on rewards.
uint256 public fee;
uint256 public fee;
/// @notice Amount of fees reserved for withdrawal by the operator.
/// @notice Amount of fees reserved for withdrawal by the operator.
uint256 public feeReserve;
uint256 public feeReserve;
/// @notice Max fee the owner can ever take - 30%
/// @notice Max fee the owner can ever take - 30%
uint256 public constant MAX_FEE = 30_00;
uint256 public constant MAX_FEE = 30_00;
uint256 public constant FEE_DENOMINATOR = 10_000;
uint256 public constant FEE_DENOMINATOR = 10_000;
// ===========================================
// ===========================================
// ============== Post Upgrade ===============
// ============== Post Upgrade ===============
// ===========================================
// ===========================================
/// @notice deposited but unstaked
/// @notice deposited but unstaked
uint256 public unstakedDeposits;
uint256 public unstakedDeposits;
/// @notice Intra-tx buffer for pending payouts
/// @notice Intra-tx buffer for pending payouts
uint256 public tokenBuffer;
uint256 public tokenBuffer;
/// @notice Whether the deposit accounting reset has been called (upgrade #2)
/// @notice Whether the deposit accounting reset has been called (upgrade #2)
bool private _resetCalled;
bool private _resetCalled;
/// @notice The next stake index with an active deposit
/// @notice The next stake index with an active deposit
uint256 public nextActiveStake;
uint256 public nextActiveStake;
/// @notice The defined accrual windows in terms of UTC hours.
/// Must be an even-length array of increasing order
uint256[] public accrualWindows;
// ========================================== INITIALIZER ===========================================
// ========================================== INITIALIZER ===========================================
/**
/**
* @dev Prevents malicious initializations of base implementation by
* @dev Prevents malicious initializations of base implementation by
* setting contract to initialized on deployment.
* setting contract to initialized on deployment.
*/
*/
/// @custom:oz-upgrades-unsafe-allow constructor
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}
constructor() initializer {}
/**
/**
* @param _magic The MAGIC token address.
* @param _magic The MAGIC token address.
* @param _mine The AtlasMine contract.
* @param _mine The AtlasMine contract.
* @param _lock The locking strategy of the staking pool.
* @param _lock The locking strategy of the staking pool.
* Maps to a timelock for AtlasMine deposits.
* Maps to a timelock for AtlasMine deposits.
*/
*/
function initialize(
function initialize(
IERC20Upgradeable _magic,
IERC20Upgradeable _magic,
AtlasMine _mine,
AtlasMine _mine,
AtlasMine.Lock _lock
AtlasMine.Lock _lock
) external initializer {
) external initializer {
require(address(_magic) != address(0), "Invalid magic token address");
require(address(_magic) != address(0), "Invalid magic token address");
require(address(_mine) != address(0), "Invalid mine contract address");
require(address(_mine) != address(0), "Invalid mine contract address");
__ERC1155Holder_init();
__ERC1155Holder_init();
__ERC721Holder_init();
__ERC721Holder_init();
__Ownable_init();
__Ownable_init();
__ReentrancyGuard_init();
__ReentrancyGuard_init();
magic = _magic;
magic = _magic;
mine = _mine;
mine = _mine;
/// @notice each staker cycles its locks for a predefined amount. New
/// @notice each staker cycles its locks for a predefined amount. New
/// lock cycle, new contract.
/// lock cycle, new contract.
lock = _lock;
lock = _lock;
(, uint256 _locktime) = mine.getLockBoost(lock);
(, uint256 _locktime) = mine.getLockBoost(lock);
locktime = _locktime;
locktime = _locktime;
lastStakeTimestamp = block.timestamp;
lastStakeTimestamp = block.timestamp;
minimumStakingWait = 12 hours;
minimumStakingWait = 12 hours;
// Approve the mine
// Approve the mine
magic.approve(address(mine), type(uint256).max);
magic.approve(address(mine), type(uint256).max);
_approveNFTs();
_approveNFTs();
}
}
// ======================================== USER OPERATIONS ========================================
// ======================================== USER OPERATIONS ========================================
/**
/**
* @notice Make a new deposit into the Staker. The Staker will collect
* @notice Make a new deposit into the Staker. The Staker will collect
* the tokens, to be later staked in atlas mine by the owner,
* the tokens, to be later staked in atlas mine by the owner,
* according to the stake/unlock schedule.
* according to the stake/unlock schedule.
* @dev Specified amount of token must be approved by the caller.
* @dev Specified amount of token must be approved by the caller.
*
*
* @param _amount The amount of tokens to deposit.
* @param _amount The amount of tokens to deposit.
*/
*/
function deposit(uint256 _amount) public virtual override nonReentrant {
function deposit(uint256 _amount) public virtual override nonReentrant whenNotAccruing {
require(!schedulePaused, "new staking paused");
require(!schedulePaused, "new staking paused");
require(_amount > 0, "Deposit amount 0");
require(_amount > 0, "Deposit amount 0");
_updateRewards();
// Add user stake
// Add user stake
uint256 newDepositId = ++currentId[msg.sender];
uint256 newDepositId = ++currentId[msg.sender];
allUserDepositIds[msg.sender].add(newDepositId);
allUserDepositIds[msg.sender].add(newDepositId);
UserStake storage s = userStake[msg.sender][newDepositId];
UserStake storage s = userStake[msg.sender][newDepositId];
s.amount = _amount;
s.amount = _amount;
s.unlockAt = block.timestamp + locktime + 1 days;
s.unlockAt = block.timestamp + locktime + 1 days;
s.rewardDebt = ((_amount * accRewardsPerShare) / ONE).toInt256();
s.rewardDebt = _accumulatedRewards(s.amount);
// Update global accounting
// Update global accounting
totalStaked += _amount;
totalStaked += _amount;
unstakedDeposits += _amount;
unstakedDeposits += _amount;
// Collect tokens
// Collect tokens
magic.safeTransferFrom(msg.sender, address(this), _amount);
magic.safeTransferFrom(msg.sender, address(this), _amount);
// MAGIC tokens sit in contract. Added to pending stakes
// MAGIC tokens sit in contract. Added to pending stakes
emit UserDeposit(msg.sender, _amount);
emit UserDeposit(msg.sender, _amount);
}
}
/**
/**
* @notice Withdraw a deposit from the Staker contract. Calculates
* @notice Withdraw a deposit from the Staker contract. Calculates
* pro rata share of accumulated MAGIC and distributes any
* pro rata share of accumulated MAGIC and distributes any
* earned rewards in addition to original deposit.
* earned rewards in addition to original deposit.
* There must be enough unlocked tokens to withdraw.
* There must be enough unlocked tokens to withdraw.
*
*
* @param depositId The ID of the deposit to withdraw from.
* @param depositId The ID of the deposit to withdraw from.
* @param _amount The amount to withdraw.
* @param _amount The amount to withdraw.
*
*
*/
*/
function withdraw(uint256 depositId, uint256 _amount) public virtual override nonReentrant {
function withdraw(uint256 depositId, uint256 _amount) public virtual override whenNotAccruing {
UserStake storage s = userStake[msg.sender][depositId];
UserStake storage s = userStake[msg.sender][depositId];
require(s.amount > 0, "No deposit");
require(s.amount > 0, "No deposit");
require(block.timestamp >= s.unlockAt, "Deposit locked");
require(block.timestamp >= s.unlockAt, "Deposit locked");
// Distribute tokens
_updateRewards();
magic.safeTransfer(msg.sender, _withdraw(s, depositId, _amount));
magic.safeTransfer(msg.sender, _withdraw(s, depositId, _amount));
}
}
/**
/**
* @notice Withdraw all eligible deposits from the staker contract.
* @notice Withdraw all eligible deposits from the staker contract.
* Will skip any deposits not yet unlocked. Will also
* Will skip any deposits not yet unlocked. Will also
* distribute rewards for all stakes via 'withdraw'.
* distribute rewards for all stakes via 'withdraw'.
*
*
*/
*/
function withdrawAll() public virtual nonReentrant usesBuffer {
function withdrawAll() public virtual nonReentrant whenNotAccruing usesBuffer {
// Distribute tokens
_updateRewards();
uint256[] memory depositIds = allUserDepositIds[msg.sender].values();
uint256[] memory depositIds = allUserDepositIds[msg.sender].values();
for (uint256 i = 0; i < depositIds.length; i++) {
for (uint256 i = 0; i < depositIds.length; i++) {
UserStake storage s = userStake[msg.sender][depositIds[i]];
UserStake storage s = userStake[msg.sender][depositIds[i]];
if (s.amount > 0 && s.unlockAt > 0 && s.unlockAt <= block.timestamp) {
if (s.amount > 0 && s.unlockAt > 0 && s.unlockAt <= block.timestamp) {
tokenBuffer += _withdraw(s, depositIds[i], type(uint256).max);
tokenBuffer += _withdraw(s, depositIds[i], type(uint256).max);
}
}
}
}
uint256 payout = tokenBuffer;
uint256 payout = tokenBuffer;
tokenBuffer = 0;
tokenBuffer = 0;
magic.safeTransfer(msg.sender, payout);
magic.safeTransfer(msg.sender, payout);
}
}
/**
/**
* @dev Logic for withdrawing a deposit. Calculates pro rata share of
* @dev Logic for withdrawing a deposit. Calculates pro rata share of
* accumulated MAGIC and distributes any earned rewards in addition
* accumulated MAGIC and distributes any earned rewards in addition
* to original deposit.
* to original deposit.
*
*
* @dev An _amount argument larger than the total deposit amount will
* @dev An _amount argument larger than the total deposit amount will
* withdraw the entire deposit.
* withdraw the entire deposit.
*
*
* @param s The UserStake struct to withdraw from.
* @param s The UserStake struct to withdraw from.
* @param depositId The ID of the deposit to withdraw from (for event).
* @param depositId The ID of the deposit to withdraw from (for event).
* @param _amount The amount to withdraw.
* @param _amount The amount to withdraw.
*/
*/
function _withdraw(
function _withdraw(
UserStake storage s,
UserStake storage s,
uint256 depositId,
uint256 depositId,
uint256 _amount
uint256 _amount
) internal returns (uint256 payout) {
) internal returns (uint256 payout) {
require(_amount > 0, "Withdraw amount 0");
require(_amount > 0, "Withdraw amount 0");
if (_amount > s.amount) {
if (_amount > s.amount) {
_amount = s.amount;
_amount = s.amount;
}
}
// Update user accounting
// Update user accounting
int256 accumulatedRewards = ((s.amount * accRewardsPerShare) / ONE).toInt256();
int256 accumulatedRewards = _accumulatedRewards(s.amount);
uint256 reward;
uint256 reward;
if (s.rewardDebt < accumulatedRewards) {
if (s.rewardDebt < accumulatedRewards) {
reward = (accumulatedRewards - s.rewardDebt).toUint256();
// Reduce by 1 wei to work around off-by-one error in atlas mine
reward = (accumulatedRewards - s.rewardDebt - 1).toUint256();
}
}
payout = _amount + reward;
payout = _amount + reward;
s.amount -= _amount;
s.amount -= _amount;
s.rewardDebt = ((s.amount * accRewardsPerShare) / ONE).toInt256();
s.rewardDebt = _accumulatedRewards(s.amount);
// Update global accounting
// Update global accounting
totalStaked -= _amount;
totalStaked -= _amount;
// If we need to unstake, unstake until we have enough
// If we need to unstake, unstake until we have enough
uint256 totalUsableMagic = _totalUsableMagic();
uint256 totalUsableMagic = _totalUsableMagic();
if (payout > totalUsableMagic) {
if (payout > totalUsableMagic) {
_unstakeToTarget(payout - totalUsableMagic);
_unstakeToTarget(payout - totalUsableMagic);
}
}
// Decrement unstakedDeposits based on how much we are withdrawing
// Decrement unstakedDeposits based on how much we are withdrawing
// If we are withdrawing more than is currently unstaked, set it to 0
// If we are withdrawing more than is currently unstaked, set it to 0
if (_amount >= unstakedDeposits) {
if (_amount >= unstakedDeposits) {
unstakedDeposits = 0;
unstakedDeposits = 0;
} else {
} else {
unstakedDeposits -= _amount;
unstakedDeposits -= _amount;
}
}
emit UserWithdraw(msg.sender, depositId, _amount, reward);
emit UserWithdraw(msg.sender, depositId, _amount, reward);
}
}
/**
/**
* @notice Claim rewards, unstaking if necessary. Will fail if there
* @notice Claim rewards, unstaking if necessary. Will fail if there
* are not enough tokens in the contract to claim rewards.
* are not enough tokens in the contract to claim rewards.
* @dev Reverts if deposit amount is 0, since rewards are auto-harvested
* @dev Reverts if deposit amount is 0, since rewards are auto-harvested
* on withdrawal, there should be no unclaimed rewards on fully
* on withdrawal, there should be no unclaimed rewards on fully
* withdrawn deposits.
* withdrawn deposits.
*
*
* @param depositId The ID of the deposit to claim rewards from.
* @param depositId The ID of the deposit to claim rewards from.
*
*
*/
*/
function claim(uint256 depositId) public virtual override nonReentrant {
function claim(uint256 depositId) public virtual override nonReentrant {
// Distribute tokens
_updateRewards();
UserStake storage s = userStake[msg.sender][depositId];
UserStake storage s = userStake[msg.sender][depositId];
require(s.amount > 0, "No deposit");
require(s.amount > 0, "No deposit");
magic.safeTransfer(msg.sender, _claim(s, depositId));
magic.safeTransfer(msg.sender, _claim(s, depositId));
}
}
/**
/**
* @notice Claim all possible rewards from the staker contract.
* @notice Claim all possible rewards from the staker contract.
* Will apply to both locked and unlocked deposits.
* Will apply to both locked and unlocked deposits.
*
*
*/
*/
function claimAll() public virtual nonReentrant usesBuffer {
function claimAll() public virtual nonReentrant usesBuffer {
// Distribute tokens
_updateRewards();
uint256[] memory depositIds = allUserDepositIds[msg.sender].values();
uint256[] memory depositIds = allUserDepositIds[msg.sender].values();
for (uint256 i = 0; i < depositIds.length; i++) {
for (uint256 i = 0; i < depositIds.length; i++) {
UserStake storage s = userStake[msg.sender][depositIds[i]];
UserStake storage s = userStake[msg.sender][depositIds[i]];
if (s.amount > 0) {
if (s.amount > 0) {
tokenBuffer += _claim(s, depositIds[i]);
tokenBuffer += _claim(s, depositIds[i]);
}
}
}
}
uint256 reward = tokenBuffer;
uint256 reward = tokenBuffer;
tokenBuffer = 0;
tokenBuffer = 0;
magic.safeTransfer(msg.sender, reward);
magic.safeTransfer(msg.sender, reward);
}
}
/**
/**
* @dev Logic for claiming rewards on a deposit. Calculates pro rata share of
* @dev Logic for claiming rewards on a deposit. Calculates pro rata share of
* accumulated MAGIC and distributed any earned rewards in addition
* accumulated MAGIC and distributed any earned rewards in addition
* to original deposit.
* to original deposit.
*
*
* @param s The UserStake struct to claim from.
* @param s The UserStake struct to claim from.
* @param depositId The ID of the deposit to claim from (for event).
* @param depositId The ID of the deposit to claim from (for event).
*/
*/
function _claim(UserStake storage s, uint256 depositId) internal returns (uint256 reward) {
function _claim(UserStake storage s, uint256 depositId) internal returns (uint256 reward) {
// Update accounting
// Update accounting
int256 accumulatedRewards = ((s.amount * accRewardsPerShare) / ONE).toInt256();
int256 accumulatedRewards = _accumulatedRewards(s.amount);
if (s.rewardDebt < accumulatedRewards) {
if (s.rewardDebt < accumulatedRewards) {
reward = (accumulatedRewards - s.rewardDebt).toUint256();
// Reduce by 1 wei to work around off-by-one error in atlas mine
reward = (accumulatedRewards - s.rewardDebt - 1).toUint256();
}
}
s.rewardDebt = accumulatedRewards;
s.rewardDebt = accumulatedRewards;
// Unstake if we need to to ensure we can withdraw
// Unstake if we need to to ensure we can withdraw
uint256 totalUsableMagic = _totalUsableMagic();
uint256 totalUsableMagic = _totalUsableMagic();
if (reward > totalUsableMagic) {
if (reward > totalUsableMagic) {
_unstakeToTarget(reward - totalUsableMagic);
_unstakeToTarget(reward - totalUsableMagic);
}
}
require(reward <= _totalUsableMagic(), "Not enough rewards to claim");
require(reward <= _totalUsableMagic(), "Not enough rewards to claim");
emit UserClaim(msg.sender, depositId, reward);
emit UserClaim(msg.sender, depositId, reward);
}
}
/**
/**
* @notice Works similarly to withdraw, but does not attempt to claim rewards.
* @notice Works similarly to withdraw, but does not attempt to claim rewards.
* Used in case there is an issue with rewards calculation either here or
* Used in case there is an issue with rewards calculation either here or
* in the Atlas Mine. emergencyUnstakeAllFromMine should be called before this,
* in the Atlas Mine. emergencyUnstakeAllFromMine should be called before this,
* since it does not attempt to unstake.
* since it does not attempt to unstake.
*
*
*/
*/
function withdrawEmergency() public virtual override nonReentrant {
function withdrawEmergency() public virtual override nonReentrant {
require(schedulePaused, "Not in emergency state");
require(schedulePaused, "Not in emergency state");
uint256 totalStake;
uint256 totalStake;
uint256[] memory depositIds = allUserDepositIds[msg.sender].values();
uint256[] memory depositIds = allUserDepositIds[msg.sender].values();
for (uint256 i = 0; i < depositIds.length; i++) {
for (uint256 i = 0; i < depositIds.length; i++) {
UserStake storage s = userStake[msg.sender][depositIds[i]];
UserStake storage s = userStake[msg.sender][depositIds[i]];
totalStake += s.amount;
totalStake += s.amount;
s.amount = 0;
s.amount = 0;
}
}
require(totalStake <= _totalUsableMagic(), "Not enough unstaked");
require(totalStake <= _totalUsableMagic(), "Not enough unstaked");
totalStaked -= totalStake;
totalStaked -= totalStake;
magic.safeTransfer(msg.sender, totalStake);
magic.safeTransfer(msg.sender, totalStake);
emit UserWithdraw(msg.sender, 0, totalStake, 0);
emit UserWithdraw(msg.sender, 0, totalStake, 0);
}
}
/**
/**
* @notice Stake any pending stakes before the current day. Callable
* @notice Stake any pending stakes before the current day. Callable
* by anybody. Any pending stakes will unlock according
* by anybody. Any pending stakes will unlock according
* to the time this method is called, and the contract's defined
* to the time this method is called, and the contract's defined
* lock time.
* lock time.
*/
*/
function stakeScheduled() public virtual override {
function stakeScheduled() public virtual override {
require(!schedulePaused, "new staking paused");
require(!schedulePaused, "new staking paused");
require(block.timestamp - lastStakeTimestamp >= minimumStakingWait, "not enough time since last stake");
require(block.timestamp - lastStakeTimestamp >= minimumStakingWait, "not enough time since last stake");
lastStakeTimestamp = block.timestamp;
lastStakeTimestamp = block.timestamp;
uint256 unlockAt = block.timestamp + locktime;
uint256 unlockAt = block.timestamp + locktime;
uint256 amountToStake = unstakedDeposits;
uint256 amountToStake = unstakedDeposits;
unstakedDeposits = 0;
unstakedDeposits = 0;
_stakeInMine(amountToStake);
_stakeInMine(amountToStake);
emit MineStake(amountToStake, unlockAt);
emit MineStake(amountToStake, unlockAt);
}
}
/**
* @notice Harvest rewards for a subset of deposit IDs, and accrue harvested
* rewards to users. The contract keeps track of the offset to ensure
* that only chunk size needs to be specified and rewards are not redundantly
* harvested during the same accrual period.
*
* @param depositIds The deposit IDs to harvest rewards from.
*/
function accrue(uint256[] calldata depositIds) public virtual override whenAccruing {
require(depositIds.length != 0, "Must accrue nonzero deposits");
_updateRewards(depositIds);
}
// ======================================= HOARD OPERATIONS ========================================
// ======================================= HOARD OPERATIONS ========================================
/**
/**
* @notice Stake a Treasure owned by the hoard into the Atlas Mine.
* @notice Stake a Treasure owned by the hoard into the Atlas Mine.
* Staked treasures will boost all user deposits.
* Staked treasures will boost all user deposits.
* @dev Any treasure must be approved for withdrawal by the caller.
* @dev Any treasure must be approved for withdrawal by the caller.
*
*
* @param _tokenId The tokenId of the specified treasure.
* @param _tokenId The tokenId of the specified treasure.
* @param _amount The amount of treasures to stake.
* @param _amount The amount of treasures to stake.
*/
*/
function stakeTreasure(uint256 _tokenId, uint256 _amount) external override onlyHoard {
function stakeTreasure(uint256 _tokenId, uint256 _amount) external override onlyHoard {
address treasureAddr = mine.treasure();
address treasureAddr = mine.treasure();
require(IERC1155Upgradeable(treasureAddr).balanceOf(msg.sender, _tokenId) >= _amount, "Not enough treasures");
require(IERC1155Upgradeable(treasureAddr).balanceOf(msg.sender, _tokenId) >= _amount, "Not enough treasures");
treasuresStaked[_tokenId][msg.sender] += _amount;
treasuresStaked[_tokenId][msg.sender] += _amount;
// First withdraw and approve
// First withdraw and approve
IERC1155Upgradeable(treasureAddr).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, bytes(""));
IERC1155Upgradeable(treasureAddr).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, bytes(""));
mine.stakeTreasure(_tokenId, _amount);
mine.stakeTreasure(_tokenId, _amount);
uint256 boost = mine.boosts(address(this));
uint256 boost = mine.boosts(address(this));
emit StakeNFT(msg.sender, treasureAddr, _tokenId, _amount, boost);
emit StakeNFT(msg.sender, treasureAddr, _tokenId, _amount, boost);
}
}
/**
/**
* @notice Unstake a Treasure from the Atlas Mine and return it to the hoard.
* @notice Unstake a Treasure from the Atlas Mine and return it to the hoard.
*
*
* @param _tokenId The tokenId of the specified treasure.
* @param _tokenId The tokenId of the specified treasure.
* @param _amount The amount of treasures to stake.
* @param _amount The amount of treasures to stake.
*/
*/
function unstakeTreasure(uint256 _tokenId, uint256 _amount) external override onlyHoard {
function unstakeTreasure(uint256 _tokenId, uint256 _amount) external override onlyHoard {
require(treasuresStaked[_tokenId][msg.sender] >= _amount, "Not enough treasures");
require(treasuresStaked[_tokenId][msg.sender] >= _amount, "Not enough treasures");
treasuresStaked[_tokenId][msg.sender] -= _amount;
treasuresStaked[_tokenId][msg.sender] -= _amount;
address treasureAddr = mine.treasure();
address treasureAddr = mine.treasure();
mine.unstakeTreasure(_tokenId, _amount);
mine.unstakeTreasure(_tokenId, _amount);
uint256 boost = mine.boosts(address(this));
uint256 boost = mine.boosts(address(this));
// Distribute to hoard
// Distribute to hoard
IERC1155Upgradeable(treasureAddr).safeTransferFrom(address(this), msg.sender, _tokenId, _amount, bytes(""));
IERC1155Upgradeable(treasureAddr).safeTransferFrom(address(this), msg.sender, _tokenId, _amount, bytes(""));
emit UnstakeNFT(msg.sender, treasureAddr, _tokenId, _amount, boost);
emit UnstakeNFT(msg.sender, treasureAddr, _tokenId, _amount, boost);
}
}
/**
/**
* @notice Stake a Legion owned by the hoard into the Atlas Mine.
* @notice Stake a Legion owned by the hoard into the Atlas Mine.
* Staked legions will boost all user deposits.
* Staked legions will boost all user deposits.
* @dev Any legion be approved for withdrawal by the caller.
* @dev Any legion be approved for withdrawal by the caller.
*
*
* @param _tokenId The tokenId of the specified legion.
* @param _tokenId The tokenId of the specified legion.
*/
*/
function stakeLegion(uint256 _tokenId) external override onlyHoard {
function stakeLegion(uint256 _tokenId) external override onlyHoard {
address legionAddr = mine.legion();
address legionAddr = mine.legion();
require(IERC721Upgradeable(legionAddr).ownerOf(_tokenId) == msg.sender, "Not owner of legion");
require(IERC721Upgradeable(legionAddr).ownerOf(_tokenId) == msg.sender, "Not owner of legion");
legionsStaked[_tokenId] = msg.sender;
legionsStaked[_tokenId] = msg.sender;
IERC721Upgradeable(legionAddr).safeTransferFrom(msg.sender, address(this), _tokenId);
IERC721Upgradeable(legionAddr).safeTransferFrom(msg.sender, address(this), _tokenId);
mine.stakeLegion(_tokenId);
mine.stakeLegion(_tokenId);
uint256 boost = mine.boosts(address(this));
uint256 boost = mine.boosts(address(this));
emit StakeNFT(msg.sender, legionAddr, _tokenId, 1, boost);
emit StakeNFT(msg.sender, legionAddr, _tokenId, 1, boost);
}
}
/**
/**
* @notice Unstake a Legion from the Atlas Mine and return it to the hoard.
* @notice Unstake a Legion from the Atlas Mine and return it to the hoard.
*
*
* @param _tokenId The tokenId of the specified legion.
* @param _tokenId The tokenId of the specified legion.
*/
*/
function unstakeLegion(uint256 _tokenId) external override onlyHoard {
function unstakeLegion(uint256 _tokenId) external override onlyHoard {
require(legionsStaked[_tokenId] == msg.sender, "Not staker of legion");
require(legionsStaked[_tokenId] == msg.sender, "Not staker of legion");
address legionAddr = mine.legion();
address legionAddr = mine.legion();
delete legionsStaked[_tokenId];
delete legionsStaked[_tokenId];
mine.unstakeLegion(_tokenId);
mine.unstakeLegion(_tokenId);
uint256 boost = mine.boosts(address(this));
uint256 boost = mine.boosts(address(this));
// Distribute to hoard
// Distribute to hoard
IERC721Upgradeable(legionAddr).safeTransferFrom(address(this), msg.sender, _tokenId);
IERC721Upgradeable(legionAddr).safeTransferFrom(address(this), msg.sender, _tokenId);
emit UnstakeNFT(msg.sender, legionAddr, _tokenId, 1, boost);
emit UnstakeNFT(msg.sender, legionAddr, _tokenId, 1, boost);
}
}
// ======================================= OWNER OPERATIONS =======================================
// ======================================= OWNER OPERATIONS =======================================
/**
/**
* @notice Unstake everything eligible for unstaking from Atlas Mine.
* @notice Unstake everything eligible for unstaking from Atlas Mine.
* Callable by owner. Should only be used in case of emergency
* Callable by owner. Should only be used in case of emergency
* or migration to a new contract, or if there is a need to service
* or migration to a new contract, or if there is a need to service
* an unexpectedly large amount of withdrawals.
* an unexpectedly large amount of withdrawals.
*
*
* If unlockAll is set to true in the Atlas Mine, this can withdraw
* If unlockAll is set to true in the Atlas Mine, this can withdraw
* all stake.
* all stake.
*/
*/
function unstakeAllFromMine() external override onlyOwner {
function unstakeAllFromMine() external override onlyOwner {
// Unstake everything eligible
_updateRewards();
uint256 totalStakes = stakes.length;
uint256 totalStakes = stakes.length;
for (uint256 i = nextActiveStake; i < totalStakes; i++) {
for (uint256 i = nextActiveStake; i < totalStakes; i++) {
Stake memory s = stakes[i];
Stake memory s = stakes[i];
if (s.unlockAt > block.timestamp) {
if (s.unlockAt > block.timestamp) {
// This stake is not unlocked - stop looking
// This stake is not unlocked - stop looking
break;
break;
}
}
// Withdraw position - auto-harvest
// Withdraw position - auto-harvest
mine.withdrawPosition(s.depositId, s.amount);
mine.withdrawPosition(s.depositId, s.amount);
}
}
// Only check for removal after, so we don't mutate while looping
// Only check for removal after, so we don't mutate while looping
_removeZeroStakes();
_removeZeroStakes();
}
}
/**
/**
* @notice Let owner unstake a specified amount as needed to make sure the contract is funded.
* @notice Let owner unstake a specified amount as needed to make sure the contract is funded.
* Can be used to facilitate expected future withdrawals.
* Can be used to facilitate expected future withdrawals.
*
*
* @param target The amount of tokens to reclaim from the mine.
* @param target The amount of tokens to reclaim from the mine.
*/
*/
function unstakeToTarget(uint256 target) external override onlyOwner {
function unstakeToTarget(uint256 target) external override onlyOwner {
_updateRewards();
_unstakeToTarget(target);
_unstakeToTarget(target);
}
}
/**
/**
* @notice Works similarly to unstakeAllFromMine, but does not harvest
* @notice Works similarly to unstakeAllFromMine, but does not harvest
* rewards. Used for getting out original stake emergencies.
* rewards. Used for getting out original stake emergencies.
* Requires emergency flag - schedulePaused to be set. Does NOT
* Requires emergency flag - schedulePaused to be set. Does NOT
* take a fee on rewards.
* take a fee on rewards.
*
*
* Requires that everything gets withdrawn to make sure it is only
* Requires that everything gets withdrawn to make sure it is only
* used in emergency. If not the case, reverts.
* used in emergency. If not the case, reverts.
*/
*/
function emergencyUnstakeAllFromMine() external override onlyOwner {
function emergencyUnstakeAllFromMine() external override onlyOwner {
require(schedulePaused, "Not in emergency state");
require(schedulePaused, "Not in emergency state");
// Unstake everything eligible
// Unstake everything eligible
mine.withdrawAll();
mine.withdrawAll();
_removeZeroStakes();
_removeZeroStakes();
}
}
/**
/**
* @notice Change the fee taken by the operator. Can never be more than
* @notice Change the fee taken by the operator. Can never be more than
* MAX_FEE. Fees only assessed on rewards.
* MAX_FEE. Fees only assessed on rewards.
*
*
* @param _fee The fee, expressed in bps.
* @param _fee The fee, expressed in bps.
*/
*/
function setFee(uint256 _fee) external override onlyOwner {
function setFee(uint256 _fee) external override onlyOwner {
require(_fee <= MAX_FEE, "Invalid fee");
require(_fee <= MAX_FEE, "Invalid fee");
fee = _fee;
fee = _fee;
emit SetFee(fee);
emit SetFee(fee);
}
}
/**
/**
* @notice Change the designated hoard, the address where treasures and
* @notice Change the designated hoard, the address where treasures and
* legions are held. Staked NFTs can only be
* legions are held. Staked NFTs can only be
* withdrawn to the current hoard address, regardless of which
* withdrawn to the current hoard address, regardless of which
* address the hoard was set to when it was staked.
* address the hoard was set to when it was staked.
*
*
* @param _hoard The new hoard address.
* @param _hoard The new hoard address.
* @param isSet Whether to enable or disable the hoard address.
* @param isSet Whether to enable or disable the hoard address.
*/
*/
function setHoard(address _hoard, bool isSet) external override onlyOwner {
function setHoard(address _hoard, bool isSet) external override onlyOwner {
require(_hoard != address(0), "Invalid hoard");
require(_hoard != address(0), "Invalid hoard");
hoards[_hoard] = isSet;
hoards[_hoard] = isSet;
}
}
/**
/**
* @notice Approve treasures and legions for withdrawal from the atlas mine.
* @notice Approve treasures and legions for withdrawal from the atlas mine.
* Called on startup, and should be called again in case contract
* Called on startup, and should be called again in case contract
* addresses for treasures and legions ever change.
* addresses for treasures and legions ever change.
*
*
*/
*/
function approveNFTs() public override onlyOwner {
function approveNFTs() public override onlyOwner {
_approveNFTs();
_approveNFTs();
}
}
/**
/**
* @notice Revokes approvals for the Atlas Mine. Should only be used
* @notice Revokes approvals for the Atlas Mine. Should only be used
* in case of emergency, blocking further staking, or an Atlas
* in case of emergency, blocking further staking, or an Atlas
* Mine exploit.
* Mine exploit.
*
*
*/
*/
function revokeNFTApprovals() public override onlyOwner {
function revokeNFTApprovals() public override onlyOwner {
address treasureAddr = mine.treasure();
address treasureAddr = mine.treasure();
IERC1155Upgradeable(treasureAddr).setApprovalForAll(address(mine), false);
IERC1155Upgradeable(treasureAddr).setApprovalForAll(address(mine), false);
address legionAddr = mine.legion();
address legionAddr = mine.legion();
IERC721Upgradeable(legionAddr).setApprovalForAll(address(mine), false);
IERC721Upgradeable(legionAddr).setApprovalForAll(address(mine), false);
}
}
/**
/**
* @notice Withdraw any accumulated reward fees to the contract owner.
* @notice Withdraw any accumulated reward fees to the contract owner.
*/
*/
function withdrawFees() external virtual override onlyOwner {
function withdrawFees() external virtual override onlyOwner {
uint256 amount = feeReserve;
uint256 amount = feeReserve;
feeReserve = 0;
feeReserve = 0;
magic.safeTransfer(msg.sender, amount);
magic.safeTransfer(msg.sender, amount);
}
}
/**
/**
* @notice Set the minimum amount of time needed to wait between stakes.
* @notice Set the minimum amount of time needed to wait between stakes.
* Default 12 hours. Can be adjusted to be longer (if incremental
* Default 12 hours. Ca
* stakes are too small or we are staking too often) or shorter
* if too much unstaked deposit is building up.
*
* @param wait The minimum amount of time to wait in between stakes.
*/
function setMinimumStakingWait(uint256 wait) external override onlyOwner {
require(wait >= 3 hours, "Minimum interval 3 hours");
minimumStakingWait = wait;
emit SetMinimumStakingWait(wait);
}
/**
* @notice EMERGENCY ONLY - toggl