Comparing sensitive data, confidential files or internal emails?

Most legal and privacy policies prohibit uploading sensitive data online. Diffchecker Desktop ensures your confidential information never leaves your computer. Work offline and compare documents securely.

Magic Dragon Dao Update

Created Diff never expires
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