Diff
checker
Texte
Texte
Images
Documents
Excel
Dossiers
Legal
Enterprise
Application de bureau
Prix
Se connecter
Télécharger Diffchecker Desktop
Comparer le texte
Trouver la différence entre deux fichiers texte
Outils
Historique
Éditeur live
Cacher identiques
Sans retour à la ligne
Vue
Divisé
Unifié
Niveau de précision
Intelligent
Mot
Caractère
Coloration syntaxique
Choisir la syntaxe
Ignorer
Transformer le texte
Aller au premier écart
Modifier l'entrée
Diffchecker Desktop
La façon la plus sécurisée d'utiliser Diffchecker. Obtenez l'application Diffchecker Desktop : vos diffs ne quittent jamais votre ordinateur !
Obtenir Desktop
Magic Dragon Dao Update
Créé
il y a 4 ans
Le diff n'expire jamais
Effacer
Exporter
Partager
Expliquer
43 suppressions
Lignes
Total
Supprimé
Caractères
Total
Supprimé
Pour continuer à utiliser cette fonctionnalité, passez à
Diff
checker
Pro
Voir les prix
666 lignes
Copier tout
30 ajouts
Lignes
Total
Ajouté
Caractères
Total
Ajouté
Pour continuer à utiliser cette fonctionnalité, passez à
Diff
checker
Pro
Voir les prix
652 lignes
Copier tout
// 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;
Copier
Copié
Copier
Copié
/// @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.
*/
*/
Copier
Copié
Copier
Copié
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");
Copier
Copié
Copier
Copié
_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;
Copier
Copié
Copier
Copié
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.
*
*
*/
*/
Copier
Copié
Copier
Copié
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");
Copier
Copié
Copier
Copié
// 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'.
*
*
*/
*/
Copier
Copié
Copier
Copié
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
Copier
Copié
Copier
Copié
int256 accumulatedRewards =
(
(s.amount
* accRewardsPerShare) / ONE).toInt256(
);
int256 accumulatedRewards =
_accumulatedRewards
(s.amount
);
uint256 reward;
uint256 reward;
if (s.rewardDebt < accumulatedRewards) {
if (s.rewardDebt < accumulatedRewards) {
Copier
Copié
Copier
Copié
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;
Copier
Copié
Copier
Copié
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 {
Copier
Copié
Copier
Copié
// 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 {
Copier
Copié
Copier
Copié
// 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
Copier
Copié
Copier
Copié
int256 accumulatedRewards =
(
(s.amount
* accRewardsPerShare) / ONE).toInt256(
);
int256 accumulatedRewards =
_accumulatedRewards
(s.amount
);
if (s.rewardDebt < accumulatedRewards) {
if (s.rewardDebt < accumulatedRewards) {
Copier
Copié
Copier
Copié
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);
}
}
Copier
Copié
Copier
Copié
/**
* @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 {
Copier
Copié
Copier
Copié
// 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 {
Copier
Copié
Copier
Copié
_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.
Copier
Copié
Copier
Copié
* Default 12 hours. Ca
n 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
Différences enregistrées
Texte d'origine
Ouvrir un fichier
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "treasure-staking/contracts/AtlasMine.sol"; import "./interfaces/IAtlasMineStaker.sol"; /** * @title AtlasMineStaker * @author kvk0x * * Dragon of the Magic Dragon DAO - A Tempting Offer. * * Staking pool contract for the Bridgeworld Atlas Mine. * Wraps existing staking with a defined 'lock time' per contract. * * Better than solo staking since a designated 'hoard' can also * deposit Treasures and Legions for staking boosts. Anyone can * enjoy the power of the guild's hoard and maximize their * Atlas Mine yield. * */ contract AtlasMineStakerUpgradeable is IAtlasMineStaker, Initializable, OwnableUpgradeable, ERC1155HolderUpgradeable, ERC721HolderUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeCastUpgradeable for uint256; using SafeCastUpgradeable for int256; using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; // ============================================ STATE ============================================== // ============= Global Immutable State ============== /// @notice MAGIC token /// @dev functionally immutable IERC20Upgradeable public magic; /// @notice The AtlasMine /// @dev functionally immutable AtlasMine public mine; /// @notice The defined lock cycle for the contract /// @dev functionally immutable AtlasMine.Lock public lock; /// @notice The defined lock time for the contract /// @dev functionally immutable uint256 public locktime; // ============= Global Staking State ============== uint256 public constant ONE = 1e30; /// @notice Whether new stakes will get staked on the contract as scheduled. For emergencies bool public schedulePaused; /// @notice Deposited, but unstaked tokens, keyed by the day number since epoch /// @notice DEPRECATED ON UPGRADE mapping(uint256 => uint256) public pendingStakes; /// @notice Last time pending stakes were deposited uint256 public lastStakeTimestamp; /// @notice The minimum amount of time between atlas mine stakes uint256 public minimumStakingWait; /// @notice The total amount of staked token uint256 public totalStaked; /// @notice All stakes currently active Stake[] public stakes; /// @notice Deposit ID of last stake. Also tracked in atlas mine uint256 public lastDepositId; /// @notice Total MAGIC rewards earned by staking. uint256 public override totalRewardsEarned; /// @notice Rewards accumulated per share uint256 public accRewardsPerShare; // ============= User Staking State ============== /// @notice Each user stake, keyed by user address => deposit ID mapping(address => mapping(uint256 => UserStake)) public userStake; /// @notice All deposit IDs for a user, enumerated mapping(address => EnumerableSetUpgradeable.UintSet) private allUserDepositIds; /// @notice The current ID of the user's last deposited stake mapping(address => uint256) public currentId; // ============= NFT Boosting State ============== /// @notice Holder of treasures and legions mapping(address => bool) private hoards; /// @notice Legions staked by hoard users mapping(uint256 => address) public legionsStaked; /// @notice Treasures staked by hoard users mapping(uint256 => mapping(address => uint256)) public treasuresStaked; // ============= Operator State ============== /// @notice Fee to contract operator. Only assessed on rewards. uint256 public fee; /// @notice Amount of fees reserved for withdrawal by the operator. uint256 public feeReserve; /// @notice Max fee the owner can ever take - 30% uint256 public constant MAX_FEE = 30_00; uint256 public constant FEE_DENOMINATOR = 10_000; // =========================================== // ============== Post Upgrade =============== // =========================================== /// @notice deposited but unstaked uint256 public unstakedDeposits; /// @notice Intra-tx buffer for pending payouts uint256 public tokenBuffer; /// @notice Whether the deposit accounting reset has been called (upgrade #2) bool private _resetCalled; /// @notice The next stake index with an active deposit uint256 public nextActiveStake; // ========================================== INITIALIZER =========================================== /** * @dev Prevents malicious initializations of base implementation by * setting contract to initialized on deployment. */ /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} /** * @param _magic The MAGIC token address. * @param _mine The AtlasMine contract. * @param _lock The locking strategy of the staking pool. * Maps to a timelock for AtlasMine deposits. */ function initialize( IERC20Upgradeable _magic, AtlasMine _mine, AtlasMine.Lock _lock ) external initializer { require(address(_magic) != address(0), "Invalid magic token address"); require(address(_mine) != address(0), "Invalid mine contract address"); __ERC1155Holder_init(); __ERC721Holder_init(); __Ownable_init(); __ReentrancyGuard_init(); magic = _magic; mine = _mine; /// @notice each staker cycles its locks for a predefined amount. New /// lock cycle, new contract. lock = _lock; (, uint256 _locktime) = mine.getLockBoost(lock); locktime = _locktime; lastStakeTimestamp = block.timestamp; minimumStakingWait = 12 hours; // Approve the mine magic.approve(address(mine), type(uint256).max); _approveNFTs(); } // ======================================== USER OPERATIONS ======================================== /** * @notice Make a new deposit into the Staker. The Staker will collect * the tokens, to be later staked in atlas mine by the owner, * according to the stake/unlock schedule. * @dev Specified amount of token must be approved by the caller. * * @param _amount The amount of tokens to deposit. */ function deposit(uint256 _amount) public virtual override nonReentrant { require(!schedulePaused, "new staking paused"); require(_amount > 0, "Deposit amount 0"); _updateRewards(); // Add user stake uint256 newDepositId = ++currentId[msg.sender]; allUserDepositIds[msg.sender].add(newDepositId); UserStake storage s = userStake[msg.sender][newDepositId]; s.amount = _amount; s.unlockAt = block.timestamp + locktime + 1 days; s.rewardDebt = ((_amount * accRewardsPerShare) / ONE).toInt256(); // Update global accounting totalStaked += _amount; unstakedDeposits += _amount; // Collect tokens magic.safeTransferFrom(msg.sender, address(this), _amount); // MAGIC tokens sit in contract. Added to pending stakes emit UserDeposit(msg.sender, _amount); } /** * @notice Withdraw a deposit from the Staker contract. Calculates * pro rata share of accumulated MAGIC and distributes any * earned rewards in addition to original deposit. * There must be enough unlocked tokens to withdraw. * * @param depositId The ID of the deposit to withdraw from. * @param _amount The amount to withdraw. * */ function withdraw(uint256 depositId, uint256 _amount) public virtual override nonReentrant { UserStake storage s = userStake[msg.sender][depositId]; require(s.amount > 0, "No deposit"); require(block.timestamp >= s.unlockAt, "Deposit locked"); // Distribute tokens _updateRewards(); magic.safeTransfer(msg.sender, _withdraw(s, depositId, _amount)); } /** * @notice Withdraw all eligible deposits from the staker contract. * Will skip any deposits not yet unlocked. Will also * distribute rewards for all stakes via 'withdraw'. * */ function withdrawAll() public virtual nonReentrant usesBuffer { // Distribute tokens _updateRewards(); uint256[] memory depositIds = allUserDepositIds[msg.sender].values(); for (uint256 i = 0; i < depositIds.length; i++) { UserStake storage s = userStake[msg.sender][depositIds[i]]; if (s.amount > 0 && s.unlockAt > 0 && s.unlockAt <= block.timestamp) { tokenBuffer += _withdraw(s, depositIds[i], type(uint256).max); } } uint256 payout = tokenBuffer; tokenBuffer = 0; magic.safeTransfer(msg.sender, payout); } /** * @dev Logic for withdrawing a deposit. Calculates pro rata share of * accumulated MAGIC and distributes any earned rewards in addition * to original deposit. * * @dev An _amount argument larger than the total deposit amount will * withdraw the entire deposit. * * @param s The UserStake struct to withdraw from. * @param depositId The ID of the deposit to withdraw from (for event). * @param _amount The amount to withdraw. */ function _withdraw( UserStake storage s, uint256 depositId, uint256 _amount ) internal returns (uint256 payout) { require(_amount > 0, "Withdraw amount 0"); if (_amount > s.amount) { _amount = s.amount; } // Update user accounting int256 accumulatedRewards = ((s.amount * accRewardsPerShare) / ONE).toInt256(); uint256 reward; if (s.rewardDebt < accumulatedRewards) { reward = (accumulatedRewards - s.rewardDebt).toUint256(); } payout = _amount + reward; s.amount -= _amount; s.rewardDebt = ((s.amount * accRewardsPerShare) / ONE).toInt256(); // Update global accounting totalStaked -= _amount; // If we need to unstake, unstake until we have enough uint256 totalUsableMagic = _totalUsableMagic(); if (payout > totalUsableMagic) { _unstakeToTarget(payout - totalUsableMagic); } // Decrement unstakedDeposits based on how much we are withdrawing // If we are withdrawing more than is currently unstaked, set it to 0 if (_amount >= unstakedDeposits) { unstakedDeposits = 0; } else { unstakedDeposits -= _amount; } emit UserWithdraw(msg.sender, depositId, _amount, reward); } /** * @notice Claim rewards, unstaking if necessary. Will fail if there * are not enough tokens in the contract to claim rewards. * @dev Reverts if deposit amount is 0, since rewards are auto-harvested * on withdrawal, there should be no unclaimed rewards on fully * withdrawn deposits. * * @param depositId The ID of the deposit to claim rewards from. * */ function claim(uint256 depositId) public virtual override nonReentrant { // Distribute tokens _updateRewards(); UserStake storage s = userStake[msg.sender][depositId]; require(s.amount > 0, "No deposit"); magic.safeTransfer(msg.sender, _claim(s, depositId)); } /** * @notice Claim all possible rewards from the staker contract. * Will apply to both locked and unlocked deposits. * */ function claimAll() public virtual nonReentrant usesBuffer { // Distribute tokens _updateRewards(); uint256[] memory depositIds = allUserDepositIds[msg.sender].values(); for (uint256 i = 0; i < depositIds.length; i++) { UserStake storage s = userStake[msg.sender][depositIds[i]]; if (s.amount > 0) { tokenBuffer += _claim(s, depositIds[i]); } } uint256 reward = tokenBuffer; tokenBuffer = 0; magic.safeTransfer(msg.sender, reward); } /** * @dev Logic for claiming rewards on a deposit. Calculates pro rata share of * accumulated MAGIC and distributed any earned rewards in addition * to original deposit. * * @param s The UserStake struct to claim from. * @param depositId The ID of the deposit to claim from (for event). */ function _claim(UserStake storage s, uint256 depositId) internal returns (uint256 reward) { // Update accounting int256 accumulatedRewards = ((s.amount * accRewardsPerShare) / ONE).toInt256(); if (s.rewardDebt < accumulatedRewards) { reward = (accumulatedRewards - s.rewardDebt).toUint256(); } s.rewardDebt = accumulatedRewards; // Unstake if we need to to ensure we can withdraw uint256 totalUsableMagic = _totalUsableMagic(); if (reward > totalUsableMagic) { _unstakeToTarget(reward - totalUsableMagic); } require(reward <= _totalUsableMagic(), "Not enough rewards to claim"); emit UserClaim(msg.sender, depositId, reward); } /** * @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 * in the Atlas Mine. emergencyUnstakeAllFromMine should be called before this, * since it does not attempt to unstake. * */ function withdrawEmergency() public virtual override nonReentrant { require(schedulePaused, "Not in emergency state"); uint256 totalStake; uint256[] memory depositIds = allUserDepositIds[msg.sender].values(); for (uint256 i = 0; i < depositIds.length; i++) { UserStake storage s = userStake[msg.sender][depositIds[i]]; totalStake += s.amount; s.amount = 0; } require(totalStake <= _totalUsableMagic(), "Not enough unstaked"); totalStaked -= totalStake; magic.safeTransfer(msg.sender, totalStake); emit UserWithdraw(msg.sender, 0, totalStake, 0); } /** * @notice Stake any pending stakes before the current day. Callable * by anybody. Any pending stakes will unlock according * to the time this method is called, and the contract's defined * lock time. */ function stakeScheduled() public virtual override { require(!schedulePaused, "new staking paused"); require(block.timestamp - lastStakeTimestamp >= minimumStakingWait, "not enough time since last stake"); lastStakeTimestamp = block.timestamp; uint256 unlockAt = block.timestamp + locktime; uint256 amountToStake = unstakedDeposits; unstakedDeposits = 0; _stakeInMine(amountToStake); emit MineStake(amountToStake, unlockAt); } // ======================================= HOARD OPERATIONS ======================================== /** * @notice Stake a Treasure owned by the hoard into the Atlas Mine. * Staked treasures will boost all user deposits. * @dev Any treasure must be approved for withdrawal by the caller. * * @param _tokenId The tokenId of the specified treasure. * @param _amount The amount of treasures to stake. */ function stakeTreasure(uint256 _tokenId, uint256 _amount) external override onlyHoard { address treasureAddr = mine.treasure(); require(IERC1155Upgradeable(treasureAddr).balanceOf(msg.sender, _tokenId) >= _amount, "Not enough treasures"); treasuresStaked[_tokenId][msg.sender] += _amount; // First withdraw and approve IERC1155Upgradeable(treasureAddr).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, bytes("")); mine.stakeTreasure(_tokenId, _amount); uint256 boost = mine.boosts(address(this)); emit StakeNFT(msg.sender, treasureAddr, _tokenId, _amount, boost); } /** * @notice Unstake a Treasure from the Atlas Mine and return it to the hoard. * * @param _tokenId The tokenId of the specified treasure. * @param _amount The amount of treasures to stake. */ function unstakeTreasure(uint256 _tokenId, uint256 _amount) external override onlyHoard { require(treasuresStaked[_tokenId][msg.sender] >= _amount, "Not enough treasures"); treasuresStaked[_tokenId][msg.sender] -= _amount; address treasureAddr = mine.treasure(); mine.unstakeTreasure(_tokenId, _amount); uint256 boost = mine.boosts(address(this)); // Distribute to hoard IERC1155Upgradeable(treasureAddr).safeTransferFrom(address(this), msg.sender, _tokenId, _amount, bytes("")); emit UnstakeNFT(msg.sender, treasureAddr, _tokenId, _amount, boost); } /** * @notice Stake a Legion owned by the hoard into the Atlas Mine. * Staked legions will boost all user deposits. * @dev Any legion be approved for withdrawal by the caller. * * @param _tokenId The tokenId of the specified legion. */ function stakeLegion(uint256 _tokenId) external override onlyHoard { address legionAddr = mine.legion(); require(IERC721Upgradeable(legionAddr).ownerOf(_tokenId) == msg.sender, "Not owner of legion"); legionsStaked[_tokenId] = msg.sender; IERC721Upgradeable(legionAddr).safeTransferFrom(msg.sender, address(this), _tokenId); mine.stakeLegion(_tokenId); uint256 boost = mine.boosts(address(this)); emit StakeNFT(msg.sender, legionAddr, _tokenId, 1, boost); } /** * @notice Unstake a Legion from the Atlas Mine and return it to the hoard. * * @param _tokenId The tokenId of the specified legion. */ function unstakeLegion(uint256 _tokenId) external override onlyHoard { require(legionsStaked[_tokenId] == msg.sender, "Not staker of legion"); address legionAddr = mine.legion(); delete legionsStaked[_tokenId]; mine.unstakeLegion(_tokenId); uint256 boost = mine.boosts(address(this)); // Distribute to hoard IERC721Upgradeable(legionAddr).safeTransferFrom(address(this), msg.sender, _tokenId); emit UnstakeNFT(msg.sender, legionAddr, _tokenId, 1, boost); } // ======================================= OWNER OPERATIONS ======================================= /** * @notice Unstake everything eligible for unstaking from Atlas Mine. * 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 * an unexpectedly large amount of withdrawals. * * If unlockAll is set to true in the Atlas Mine, this can withdraw * all stake. */ function unstakeAllFromMine() external override onlyOwner { // Unstake everything eligible _updateRewards(); uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { Stake memory s = stakes[i]; if (s.unlockAt > block.timestamp) { // This stake is not unlocked - stop looking break; } // Withdraw position - auto-harvest mine.withdrawPosition(s.depositId, s.amount); } // Only check for removal after, so we don't mutate while looping _removeZeroStakes(); } /** * @notice Let owner unstake a specified amount as needed to make sure the contract is funded. * Can be used to facilitate expected future withdrawals. * * @param target The amount of tokens to reclaim from the mine. */ function unstakeToTarget(uint256 target) external override onlyOwner { _updateRewards(); _unstakeToTarget(target); } /** * @notice Works similarly to unstakeAllFromMine, but does not harvest * rewards. Used for getting out original stake emergencies. * Requires emergency flag - schedulePaused to be set. Does NOT * take a fee on rewards. * * Requires that everything gets withdrawn to make sure it is only * used in emergency. If not the case, reverts. */ function emergencyUnstakeAllFromMine() external override onlyOwner { require(schedulePaused, "Not in emergency state"); // Unstake everything eligible mine.withdrawAll(); _removeZeroStakes(); } /** * @notice Change the fee taken by the operator. Can never be more than * MAX_FEE. Fees only assessed on rewards. * * @param _fee The fee, expressed in bps. */ function setFee(uint256 _fee) external override onlyOwner { require(_fee <= MAX_FEE, "Invalid fee"); fee = _fee; emit SetFee(fee); } /** * @notice Change the designated hoard, the address where treasures and * legions are held. Staked NFTs can only be * withdrawn to the current hoard address, regardless of which * address the hoard was set to when it was staked. * * @param _hoard The new hoard address. * @param isSet Whether to enable or disable the hoard address. */ function setHoard(address _hoard, bool isSet) external override onlyOwner { require(_hoard != address(0), "Invalid hoard"); hoards[_hoard] = isSet; } /** * @notice Approve treasures and legions for withdrawal from the atlas mine. * Called on startup, and should be called again in case contract * addresses for treasures and legions ever change. * */ function approveNFTs() public override onlyOwner { _approveNFTs(); } /** * @notice Revokes approvals for the Atlas Mine. Should only be used * in case of emergency, blocking further staking, or an Atlas * Mine exploit. * */ function revokeNFTApprovals() public override onlyOwner { address treasureAddr = mine.treasure(); IERC1155Upgradeable(treasureAddr).setApprovalForAll(address(mine), false); address legionAddr = mine.legion(); IERC721Upgradeable(legionAddr).setApprovalForAll(address(mine), false); } /** * @notice Withdraw any accumulated reward fees to the contract owner. */ function withdrawFees() external virtual override onlyOwner { uint256 amount = feeReserve; feeReserve = 0; magic.safeTransfer(msg.sender, amount); } /** * @notice Set the minimum amount of time needed to wait between stakes. * Default 12 hours. Can be adjusted to be longer (if incremental * 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 - toggle pausing new scheduled stakes. * If on, users can deposit, but stakes won't go to Atlas Mine. * Can be used in case of Atlas Mine issues or forced migration * to new contract. */ function toggleSchedulePause(bool paused) external virtual override onlyOwner { schedulePaused = paused; emit StakingPauseToggle(paused); } /** * @notice Must be used when migrating to a new contract that changes the accounting * logic of unstaked deposits. Can be used to reset value to one that would * be in place in the cast that newly-introduced logic was always in place. * * @dev Cannot be used in normal operation, will only be called once as part of * an "upgradeAndCall" contract upgrade. * * @param _unstakedDeposits The new value of unstakedDeposits to set. */ function resetUnstakedAndStake(uint256 _unstakedDeposits) external { require(!_resetCalled, "reset already called"); _resetCalled = true; unstakedDeposits = _unstakedDeposits; stakeScheduled(); } // ======================================== VIEW FUNCTIONS ========================================= /** * @notice Returns all magic either unstaked, staked, or pending rewards in Atlas Mine. * Best proxy for TVL. * * @return total The total amount of MAGIC in the staker. */ function totalMagic() external view override returns (uint256) { return _totalControlledMagic() + mine.pendingRewardsAll(address(this)); } /** * @notice Returns all magic that has been deposited, but not staked, and is eligible * to be staked (deposit time < current day). * * @return total The total amount of MAGIC available to stake. */ function totalPendingStake() external view override returns (uint256) { return unstakedDeposits; } /** * @notice Returns all magic that has been deposited, but not staked, and is eligible * to be staked (deposit time < current day). * * @return total The total amount of MAGIC that can be withdrawn. */ function totalWithdrawableMagic() external view override returns (uint256) { uint256 totalPendingRewards; // AtlasMine attempts to divide by 0 if there are no deposits try mine.pendingRewardsAll(address(this)) returns (uint256 _pending) { totalPendingRewards = _pending; } catch Panic(uint256) { totalPendingRewards = 0; } uint256 vestedPrincipal; uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { vestedPrincipal += mine.calcualteVestedPrincipal(address(this), stakes[i].depositId); } return _totalUsableMagic() + totalPendingRewards + vestedPrincipal; } /** * @notice Returns the details of a user stake. * * @return userStake The details of a user stake. */ function getUserStake(address user, uint256 depositId) external view override returns (UserStake memory) { return userStake[user][depositId]; } /** * @notice Returns the total amount staked by a user. * * @return totalStake The total amount of MAGIC staked by a user. */ function userTotalStake(address user) external view override returns (uint256 totalStake) { uint256[] memory depositIds = allUserDepositIds[user].values(); for (uint256 i = 0; i < depositIds.length; i++) { UserStake storage s = userStake[user][depositIds[i]]; totalStake += s.amount; } } /** * @notice Returns the pending, claimable rewards for a deposit. * @dev This does not update rewards, so out of date if rewards not recently updated. * Needed to maintain 'view' function type. * * @param user The user to check rewards for. * @param depositId The specific deposit to check rewards for. * * @return reward The total amount of MAGIC reward pending. */ function pendingRewards(address user, uint256 depositId) public view override returns (uint256 reward) { UserStake storage s = userStake[user][depositId]; int256 accumulatedRewards = ((s.amount * accRewardsPerShare) / ONE).toInt256(); reward = (accumulatedRewards - s.rewardDebt).toUint256(); } /** * @notice Returns the pending, claimable rewards for all of a user's deposits. * @dev This does not update rewards, so out of date if rewards not recently updated. * Needed to maintain 'view' function type. * * @param user The user to check rewards for. * * @return reward The total amount of MAGIC reward pending. */ function pendingRewardsAll(address user) external view override returns (uint256 reward) { uint256[] memory depositIds = allUserDepositIds[user].values(); for (uint256 i = 0; i < depositIds.length; i++) { reward += pendingRewards(user, depositIds[i]); } } // ============================================ HELPERS ============================================ /** * @dev Approve treasures and legions for withdrawal from the atlas mine. */ function _approveNFTs() internal { address treasureAddr = mine.treasure(); IERC1155Upgradeable(treasureAddr).setApprovalForAll(address(mine), true); address legionAddr = mine.legion(); IERC721Upgradeable(legionAddr).setApprovalForAll(address(mine), true); } /** * @dev Stake tokens held by staker in the Atlas Mine, according to * the predefined lock value. Schedules for staking will be managed by a queue. * * @param _amount Number of tokens to stake */ function _stakeInMine(uint256 _amount) internal { require(_amount <= _totalUsableMagic(), "Not enough funds"); uint256 depositId = ++lastDepositId; uint256 unlockAt = block.timestamp + locktime; stakes.push(Stake({ amount: _amount, unlockAt: unlockAt, depositId: depositId })); mine.deposit(_amount, lock); } /** * @dev Unstakes until we have enough unstaked tokens to meet a specific target. * Used to make sure we can service withdrawals. * * @param target The amount of tokens we want to have unstaked. */ function _unstakeToTarget(uint256 target) internal { uint256 unstaked = 0; uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { Stake memory s = stakes[i]; if (s.unlockAt > block.timestamp && !mine.unlockAll()) { // This stake is not unlocked - stop looking break; } // Withdraw position - auto-harvest uint256 preclaimBalance = _totalUsableMagic(); uint256 targetLeft = target - unstaked; uint256 amount = targetLeft > s.amount ? s.amount : targetLeft; // Do not harvest rewards - if this is running, we've already // harvested in the same fn call mine.withdrawPosition(s.depositId, amount); uint256 postclaimBalance = _totalUsableMagic(); // Increment amount unstaked unstaked += postclaimBalance - preclaimBalance; if (unstaked >= target) { // We unstaked enough break; } } require(unstaked >= target, "Cannot unstake enough"); require(_totalUsableMagic() >= target, "Not enough in contract after unstaking"); // Only check for removal after, so we don't mutate while looping _removeZeroStakes(); } /** * @dev Harvest rewards from the AtlasMine and send them back to * this contract. * * @return earned The amount of rewards earned for depositors, minus the fee. * @return feeEearned The amount of fees earned for the contract operator. */ function _harvestMine() internal returns (uint256, uint256) { uint256 preclaimBalance = magic.balanceOf(address(this)); try mine.harvestAll() { uint256 postclaimBalance = magic.balanceOf(address(this)); uint256 earned = postclaimBalance - preclaimBalance; // Reserve the 'fee' amount of what is earned uint256 feeEarned = (earned * fee) / FEE_DENOMINATOR; feeReserve += feeEarned; emit MineHarvest(earned - feeEarned, feeEarned); return (earned - feeEarned, feeEarned); } catch { // Failed because of reward debt calculation - should be 0 return (0, 0); } } /** * @dev Harvest rewards from the mine so that stakers can claim. * Recalculate how many rewards are distributed to each share. */ function _updateRewards() internal { if (totalStaked == 0) return; (uint256 newRewards, ) = _harvestMine(); totalRewardsEarned += newRewards; accRewardsPerShare += (newRewards * ONE) / totalStaked; } /** * @dev After mutating a stake (by withdrawing fully or partially), * get updated data from the staking contract, and update the stake amounts * * @param stakeIndex The index of the stake in the Stakes storage array. * * @return amount The current, updated amount of the stake. */ function _updateStakeDepositAmount(uint256 stakeIndex) internal returns (uint256) { Stake storage s = stakes[stakeIndex]; (, uint256 depositAmount, , , , , ) = mine.userInfo(address(this), s.depositId); s.amount = depositAmount; return s.amount; } /** * @dev Find stakes with zero deposit amount and remove them from tracking. * Starts from the last fully withdrawn stake, and keeps counting until * we find a non-zero stake. * */ function _removeZeroStakes() internal { uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { _updateStakeDepositAmount(i); Stake memory s = stakes[i]; if (s.amount != 0) { // No more zero stakes - can break nextActiveStake = i; break; } } } /** * @dev Calculate total amount of MAGIC usable by the contract. * 'Usable' means available for either withdrawal or re-staking. * Counts unstaked magic less fee reserve. * * @return amount The amount of usable MAGIC. */ function _totalUsableMagic() internal view returns (uint256) { // Current magic held in contract uint256 unstaked = magic.balanceOf(address(this)); return unstaked - feeReserve - tokenBuffer; } /** * @dev Calculate total amount of MAGIC under control of the contract. * Counts staked and unstaked MAGIC. Does _not_ count accumulated * but unclaimed rewards. * * @return amount The total amount of MAGIC under control of the contract. */ function _totalControlledMagic() internal view returns (uint256) { // Current magic staked in mine uint256 staked = 0; uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { staked += stakes[i].amount; } return staked + _totalUsableMagic(); } /** * @dev For methods only callable by the hoard - Treasure staking/unstaking. */ modifier onlyHoard() { require(hoards[msg.sender], "Not hoard"); _; } /** * @dev For methods that access the token buffer - make sure it is cleared. */ modifier usesBuffer() { _; require(tokenBuffer == 0, "Buffer not clear"); } }
Texte modifié
Ouvrir un fichier
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "treasure-staking/contracts/AtlasMine.sol"; import "./interfaces/IAtlasMineStaker.sol"; /** * @title AtlasMineStaker * @author kvk0x * * Dragon of the Magic Dragon DAO - A Tempting Offer. * * Staking pool contract for the Bridgeworld Atlas Mine. * Wraps existing staking with a defined 'lock time' per contract. * * Better than solo staking since a designated 'hoard' can also * deposit Treasures and Legions for staking boosts. Anyone can * enjoy the power of the guild's hoard and maximize their * Atlas Mine yield. * */ contract AtlasMineStakerUpgradeable is IAtlasMineStaker, Initializable, OwnableUpgradeable, ERC1155HolderUpgradeable, ERC721HolderUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeCastUpgradeable for uint256; using SafeCastUpgradeable for int256; using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; // ============================================ STATE ============================================== // ============= Global Immutable State ============== /// @notice MAGIC token /// @dev functionally immutable IERC20Upgradeable public magic; /// @notice The AtlasMine /// @dev functionally immutable AtlasMine public mine; /// @notice The defined lock cycle for the contract /// @dev functionally immutable AtlasMine.Lock public lock; /// @notice The defined lock time for the contract /// @dev functionally immutable uint256 public locktime; // ============= Global Staking State ============== uint256 public constant ONE = 1e30; /// @notice Whether new stakes will get staked on the contract as scheduled. For emergencies bool public schedulePaused; /// @notice Deposited, but unstaked tokens, keyed by the day number since epoch /// @notice DEPRECATED ON UPGRADE mapping(uint256 => uint256) public pendingStakes; /// @notice Last time pending stakes were deposited uint256 public lastStakeTimestamp; /// @notice The minimum amount of time between atlas mine stakes uint256 public minimumStakingWait; /// @notice The total amount of staked token uint256 public totalStaked; /// @notice All stakes currently active Stake[] public stakes; /// @notice Deposit ID of last stake. Also tracked in atlas mine uint256 public lastDepositId; /// @notice Total MAGIC rewards earned by staking. uint256 public override totalRewardsEarned; /// @notice Rewards accumulated per share uint256 public accRewardsPerShare; // ============= User Staking State ============== /// @notice Each user stake, keyed by user address => deposit ID mapping(address => mapping(uint256 => UserStake)) public userStake; /// @notice All deposit IDs for a user, enumerated mapping(address => EnumerableSetUpgradeable.UintSet) private allUserDepositIds; /// @notice The current ID of the user's last deposited stake mapping(address => uint256) public currentId; // ============= NFT Boosting State ============== /// @notice Holder of treasures and legions mapping(address => bool) private hoards; /// @notice Legions staked by hoard users mapping(uint256 => address) public legionsStaked; /// @notice Treasures staked by hoard users mapping(uint256 => mapping(address => uint256)) public treasuresStaked; // ============= Operator State ============== /// @notice Fee to contract operator. Only assessed on rewards. uint256 public fee; /// @notice Amount of fees reserved for withdrawal by the operator. uint256 public feeReserve; /// @notice Max fee the owner can ever take - 30% uint256 public constant MAX_FEE = 30_00; uint256 public constant FEE_DENOMINATOR = 10_000; // =========================================== // ============== Post Upgrade =============== // =========================================== /// @notice deposited but unstaked uint256 public unstakedDeposits; /// @notice Intra-tx buffer for pending payouts uint256 public tokenBuffer; /// @notice Whether the deposit accounting reset has been called (upgrade #2) bool private _resetCalled; /// @notice The next stake index with an active deposit 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 =========================================== /** * @dev Prevents malicious initializations of base implementation by * setting contract to initialized on deployment. */ /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} /** * @param _magic The MAGIC token address. * @param _mine The AtlasMine contract. * @param _lock The locking strategy of the staking pool. * Maps to a timelock for AtlasMine deposits. */ function initialize( IERC20Upgradeable _magic, AtlasMine _mine, AtlasMine.Lock _lock ) external initializer { require(address(_magic) != address(0), "Invalid magic token address"); require(address(_mine) != address(0), "Invalid mine contract address"); __ERC1155Holder_init(); __ERC721Holder_init(); __Ownable_init(); __ReentrancyGuard_init(); magic = _magic; mine = _mine; /// @notice each staker cycles its locks for a predefined amount. New /// lock cycle, new contract. lock = _lock; (, uint256 _locktime) = mine.getLockBoost(lock); locktime = _locktime; lastStakeTimestamp = block.timestamp; minimumStakingWait = 12 hours; // Approve the mine magic.approve(address(mine), type(uint256).max); _approveNFTs(); } // ======================================== USER OPERATIONS ======================================== /** * @notice Make a new deposit into the Staker. The Staker will collect * the tokens, to be later staked in atlas mine by the owner, * according to the stake/unlock schedule. * @dev Specified amount of token must be approved by the caller. * * @param _amount The amount of tokens to deposit. */ function deposit(uint256 _amount) public virtual override nonReentrant whenNotAccruing { require(!schedulePaused, "new staking paused"); require(_amount > 0, "Deposit amount 0"); // Add user stake uint256 newDepositId = ++currentId[msg.sender]; allUserDepositIds[msg.sender].add(newDepositId); UserStake storage s = userStake[msg.sender][newDepositId]; s.amount = _amount; s.unlockAt = block.timestamp + locktime + 1 days; s.rewardDebt = _accumulatedRewards(s.amount); // Update global accounting totalStaked += _amount; unstakedDeposits += _amount; // Collect tokens magic.safeTransferFrom(msg.sender, address(this), _amount); // MAGIC tokens sit in contract. Added to pending stakes emit UserDeposit(msg.sender, _amount); } /** * @notice Withdraw a deposit from the Staker contract. Calculates * pro rata share of accumulated MAGIC and distributes any * earned rewards in addition to original deposit. * There must be enough unlocked tokens to withdraw. * * @param depositId The ID of the deposit to withdraw from. * @param _amount The amount to withdraw. * */ function withdraw(uint256 depositId, uint256 _amount) public virtual override whenNotAccruing { UserStake storage s = userStake[msg.sender][depositId]; require(s.amount > 0, "No deposit"); require(block.timestamp >= s.unlockAt, "Deposit locked"); magic.safeTransfer(msg.sender, _withdraw(s, depositId, _amount)); } /** * @notice Withdraw all eligible deposits from the staker contract. * Will skip any deposits not yet unlocked. Will also * distribute rewards for all stakes via 'withdraw'. * */ function withdrawAll() public virtual nonReentrant whenNotAccruing usesBuffer { uint256[] memory depositIds = allUserDepositIds[msg.sender].values(); for (uint256 i = 0; i < depositIds.length; i++) { UserStake storage s = userStake[msg.sender][depositIds[i]]; if (s.amount > 0 && s.unlockAt > 0 && s.unlockAt <= block.timestamp) { tokenBuffer += _withdraw(s, depositIds[i], type(uint256).max); } } uint256 payout = tokenBuffer; tokenBuffer = 0; magic.safeTransfer(msg.sender, payout); } /** * @dev Logic for withdrawing a deposit. Calculates pro rata share of * accumulated MAGIC and distributes any earned rewards in addition * to original deposit. * * @dev An _amount argument larger than the total deposit amount will * withdraw the entire deposit. * * @param s The UserStake struct to withdraw from. * @param depositId The ID of the deposit to withdraw from (for event). * @param _amount The amount to withdraw. */ function _withdraw( UserStake storage s, uint256 depositId, uint256 _amount ) internal returns (uint256 payout) { require(_amount > 0, "Withdraw amount 0"); if (_amount > s.amount) { _amount = s.amount; } // Update user accounting int256 accumulatedRewards = _accumulatedRewards(s.amount); uint256 reward; if (s.rewardDebt < accumulatedRewards) { // Reduce by 1 wei to work around off-by-one error in atlas mine reward = (accumulatedRewards - s.rewardDebt - 1).toUint256(); } payout = _amount + reward; s.amount -= _amount; s.rewardDebt = _accumulatedRewards(s.amount); // Update global accounting totalStaked -= _amount; // If we need to unstake, unstake until we have enough uint256 totalUsableMagic = _totalUsableMagic(); if (payout > totalUsableMagic) { _unstakeToTarget(payout - totalUsableMagic); } // Decrement unstakedDeposits based on how much we are withdrawing // If we are withdrawing more than is currently unstaked, set it to 0 if (_amount >= unstakedDeposits) { unstakedDeposits = 0; } else { unstakedDeposits -= _amount; } emit UserWithdraw(msg.sender, depositId, _amount, reward); } /** * @notice Claim rewards, unstaking if necessary. Will fail if there * are not enough tokens in the contract to claim rewards. * @dev Reverts if deposit amount is 0, since rewards are auto-harvested * on withdrawal, there should be no unclaimed rewards on fully * withdrawn deposits. * * @param depositId The ID of the deposit to claim rewards from. * */ function claim(uint256 depositId) public virtual override nonReentrant { UserStake storage s = userStake[msg.sender][depositId]; require(s.amount > 0, "No deposit"); magic.safeTransfer(msg.sender, _claim(s, depositId)); } /** * @notice Claim all possible rewards from the staker contract. * Will apply to both locked and unlocked deposits. * */ function claimAll() public virtual nonReentrant usesBuffer { uint256[] memory depositIds = allUserDepositIds[msg.sender].values(); for (uint256 i = 0; i < depositIds.length; i++) { UserStake storage s = userStake[msg.sender][depositIds[i]]; if (s.amount > 0) { tokenBuffer += _claim(s, depositIds[i]); } } uint256 reward = tokenBuffer; tokenBuffer = 0; magic.safeTransfer(msg.sender, reward); } /** * @dev Logic for claiming rewards on a deposit. Calculates pro rata share of * accumulated MAGIC and distributed any earned rewards in addition * to original deposit. * * @param s The UserStake struct to claim from. * @param depositId The ID of the deposit to claim from (for event). */ function _claim(UserStake storage s, uint256 depositId) internal returns (uint256 reward) { // Update accounting int256 accumulatedRewards = _accumulatedRewards(s.amount); if (s.rewardDebt < accumulatedRewards) { // Reduce by 1 wei to work around off-by-one error in atlas mine reward = (accumulatedRewards - s.rewardDebt - 1).toUint256(); } s.rewardDebt = accumulatedRewards; // Unstake if we need to to ensure we can withdraw uint256 totalUsableMagic = _totalUsableMagic(); if (reward > totalUsableMagic) { _unstakeToTarget(reward - totalUsableMagic); } require(reward <= _totalUsableMagic(), "Not enough rewards to claim"); emit UserClaim(msg.sender, depositId, reward); } /** * @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 * in the Atlas Mine. emergencyUnstakeAllFromMine should be called before this, * since it does not attempt to unstake. * */ function withdrawEmergency() public virtual override nonReentrant { require(schedulePaused, "Not in emergency state"); uint256 totalStake; uint256[] memory depositIds = allUserDepositIds[msg.sender].values(); for (uint256 i = 0; i < depositIds.length; i++) { UserStake storage s = userStake[msg.sender][depositIds[i]]; totalStake += s.amount; s.amount = 0; } require(totalStake <= _totalUsableMagic(), "Not enough unstaked"); totalStaked -= totalStake; magic.safeTransfer(msg.sender, totalStake); emit UserWithdraw(msg.sender, 0, totalStake, 0); } /** * @notice Stake any pending stakes before the current day. Callable * by anybody. Any pending stakes will unlock according * to the time this method is called, and the contract's defined * lock time. */ function stakeScheduled() public virtual override { require(!schedulePaused, "new staking paused"); require(block.timestamp - lastStakeTimestamp >= minimumStakingWait, "not enough time since last stake"); lastStakeTimestamp = block.timestamp; uint256 unlockAt = block.timestamp + locktime; uint256 amountToStake = unstakedDeposits; unstakedDeposits = 0; _stakeInMine(amountToStake); 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 ======================================== /** * @notice Stake a Treasure owned by the hoard into the Atlas Mine. * Staked treasures will boost all user deposits. * @dev Any treasure must be approved for withdrawal by the caller. * * @param _tokenId The tokenId of the specified treasure. * @param _amount The amount of treasures to stake. */ function stakeTreasure(uint256 _tokenId, uint256 _amount) external override onlyHoard { address treasureAddr = mine.treasure(); require(IERC1155Upgradeable(treasureAddr).balanceOf(msg.sender, _tokenId) >= _amount, "Not enough treasures"); treasuresStaked[_tokenId][msg.sender] += _amount; // First withdraw and approve IERC1155Upgradeable(treasureAddr).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, bytes("")); mine.stakeTreasure(_tokenId, _amount); uint256 boost = mine.boosts(address(this)); emit StakeNFT(msg.sender, treasureAddr, _tokenId, _amount, boost); } /** * @notice Unstake a Treasure from the Atlas Mine and return it to the hoard. * * @param _tokenId The tokenId of the specified treasure. * @param _amount The amount of treasures to stake. */ function unstakeTreasure(uint256 _tokenId, uint256 _amount) external override onlyHoard { require(treasuresStaked[_tokenId][msg.sender] >= _amount, "Not enough treasures"); treasuresStaked[_tokenId][msg.sender] -= _amount; address treasureAddr = mine.treasure(); mine.unstakeTreasure(_tokenId, _amount); uint256 boost = mine.boosts(address(this)); // Distribute to hoard IERC1155Upgradeable(treasureAddr).safeTransferFrom(address(this), msg.sender, _tokenId, _amount, bytes("")); emit UnstakeNFT(msg.sender, treasureAddr, _tokenId, _amount, boost); } /** * @notice Stake a Legion owned by the hoard into the Atlas Mine. * Staked legions will boost all user deposits. * @dev Any legion be approved for withdrawal by the caller. * * @param _tokenId The tokenId of the specified legion. */ function stakeLegion(uint256 _tokenId) external override onlyHoard { address legionAddr = mine.legion(); require(IERC721Upgradeable(legionAddr).ownerOf(_tokenId) == msg.sender, "Not owner of legion"); legionsStaked[_tokenId] = msg.sender; IERC721Upgradeable(legionAddr).safeTransferFrom(msg.sender, address(this), _tokenId); mine.stakeLegion(_tokenId); uint256 boost = mine.boosts(address(this)); emit StakeNFT(msg.sender, legionAddr, _tokenId, 1, boost); } /** * @notice Unstake a Legion from the Atlas Mine and return it to the hoard. * * @param _tokenId The tokenId of the specified legion. */ function unstakeLegion(uint256 _tokenId) external override onlyHoard { require(legionsStaked[_tokenId] == msg.sender, "Not staker of legion"); address legionAddr = mine.legion(); delete legionsStaked[_tokenId]; mine.unstakeLegion(_tokenId); uint256 boost = mine.boosts(address(this)); // Distribute to hoard IERC721Upgradeable(legionAddr).safeTransferFrom(address(this), msg.sender, _tokenId); emit UnstakeNFT(msg.sender, legionAddr, _tokenId, 1, boost); } // ======================================= OWNER OPERATIONS ======================================= /** * @notice Unstake everything eligible for unstaking from Atlas Mine. * 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 * an unexpectedly large amount of withdrawals. * * If unlockAll is set to true in the Atlas Mine, this can withdraw * all stake. */ function unstakeAllFromMine() external override onlyOwner { uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { Stake memory s = stakes[i]; if (s.unlockAt > block.timestamp) { // This stake is not unlocked - stop looking break; } // Withdraw position - auto-harvest mine.withdrawPosition(s.depositId, s.amount); } // Only check for removal after, so we don't mutate while looping _removeZeroStakes(); } /** * @notice Let owner unstake a specified amount as needed to make sure the contract is funded. * Can be used to facilitate expected future withdrawals. * * @param target The amount of tokens to reclaim from the mine. */ function unstakeToTarget(uint256 target) external override onlyOwner { _unstakeToTarget(target); } /** * @notice Works similarly to unstakeAllFromMine, but does not harvest * rewards. Used for getting out original stake emergencies. * Requires emergency flag - schedulePaused to be set. Does NOT * take a fee on rewards. * * Requires that everything gets withdrawn to make sure it is only * used in emergency. If not the case, reverts. */ function emergencyUnstakeAllFromMine() external override onlyOwner { require(schedulePaused, "Not in emergency state"); // Unstake everything eligible mine.withdrawAll(); _removeZeroStakes(); } /** * @notice Change the fee taken by the operator. Can never be more than * MAX_FEE. Fees only assessed on rewards. * * @param _fee The fee, expressed in bps. */ function setFee(uint256 _fee) external override onlyOwner { require(_fee <= MAX_FEE, "Invalid fee"); fee = _fee; emit SetFee(fee); } /** * @notice Change the designated hoard, the address where treasures and * legions are held. Staked NFTs can only be * withdrawn to the current hoard address, regardless of which * address the hoard was set to when it was staked. * * @param _hoard The new hoard address. * @param isSet Whether to enable or disable the hoard address. */ function setHoard(address _hoard, bool isSet) external override onlyOwner { require(_hoard != address(0), "Invalid hoard"); hoards[_hoard] = isSet; } /** * @notice Approve treasures and legions for withdrawal from the atlas mine. * Called on startup, and should be called again in case contract * addresses for treasures and legions ever change. * */ function approveNFTs() public override onlyOwner { _approveNFTs(); } /** * @notice Revokes approvals for the Atlas Mine. Should only be used * in case of emergency, blocking further staking, or an Atlas * Mine exploit. * */ function revokeNFTApprovals() public override onlyOwner { address treasureAddr = mine.treasure(); IERC1155Upgradeable(treasureAddr).setApprovalForAll(address(mine), false); address legionAddr = mine.legion(); IERC721Upgradeable(legionAddr).setApprovalForAll(address(mine), false); } /** * @notice Withdraw any accumulated reward fees to the contract owner. */ function withdrawFees() external virtual override onlyOwner { uint256 amount = feeReserve; feeReserve = 0; magic.safeTransfer(msg.sender, amount); } /** * @notice Set the minimum amount of time needed to wait between stakes. * Default 12 hours. Can be adjusted to be longer (if incremental * 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 - toggle pausing new scheduled stakes. * If on, users can deposit, but stakes won't go to Atlas Mine. * Can be used in case of Atlas Mine issues or forced migration * to new contract. */ function toggleSchedulePause(bool paused) external virtual override onlyOwner { schedulePaused = paused; emit StakingPauseToggle(paused); } /** * @notice Used to set windows during which accrual happens, and new deposits/withdrawls * are paused. * * @param windows The list of hourly windows in pairs of tuples, e.g. [0, 1, 12, 13] */ function setAccrualWindows(uint256[] calldata windows) external override onlyOwner { require(windows.length % 2 == 0, "Invalid window length"); for (uint256 i = 0; i < windows.length; i++) { // Must be 0-23, and monotonically increasing if (i < windows.length - 1) { require(windows[i] < 24, "Invalid window value"); } else { // Allow 24 as an ending value require(windows[i] < 25, "Invalid window value"); } if (i > 0) { require(windows[i] > windows[i - 1], "Invalid window ordering"); } } accrualWindows = windows; emit SetAccrualWindows(windows); } // ======================================== VIEW FUNCTIONS ========================================= /** * @notice Returns all magic either unstaked, staked, or pending rewards in Atlas Mine. * Best proxy for TVL. * * @return total The total amount of MAGIC in the staker. */ function totalMagic() external view override returns (uint256) { return _totalControlledMagic() + mine.pendingRewardsAll(address(this)); } /** * @notice Returns all magic that has been deposited, but not staked, and is eligible * to be staked (deposit time < current day). * * @return total The total amount of MAGIC available to stake. */ function totalPendingStake() external view override returns (uint256) { return unstakedDeposits; } /** * @notice Returns all magic that has been deposited, but not staked, and is eligible * to be staked (deposit time < current day). * * @return total The total amount of MAGIC that can be withdrawn. */ function totalWithdrawableMagic() external view override returns (uint256) { uint256 totalPendingRewards; // AtlasMine attempts to divide by 0 if there are no deposits try mine.pendingRewardsAll(address(this)) returns (uint256 _pending) { totalPendingRewards = _pending; } catch Panic(uint256) { totalPendingRewards = 0; } uint256 vestedPrincipal; uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { vestedPrincipal += mine.calcualteVestedPrincipal(address(this), stakes[i].depositId); } return _totalUsableMagic() + totalPendingRewards + vestedPrincipal; } /** * @notice Returns the details of a user stake. * * @return userStake The details of a user stake. */ function getUserStake(address user, uint256 depositId) external view override returns (UserStake memory) { return userStake[user][depositId]; } /** * @notice Returns the total amount staked by a user. * * @return totalStake The total amount of MAGIC staked by a user. */ function userTotalStake(address user) external view override returns (uint256 totalStake) { uint256[] memory depositIds = allUserDepositIds[user].values(); for (uint256 i = 0; i < depositIds.length; i++) { UserStake storage s = userStake[user][depositIds[i]]; totalStake += s.amount; } } /** * @notice Returns the pending, claimable rewards for a deposit. * @dev This does not update rewards, so out of date if rewards not recently updated. * Needed to maintain 'view' function type. * * @param user The user to check rewards for. * @param depositId The specific deposit to check rewards for. * * @return reward The total amount of MAGIC reward pending. */ function pendingRewards(address user, uint256 depositId) public view override returns (uint256 reward) { UserStake storage s = userStake[user][depositId]; int256 accumulatedRewards = _accumulatedRewards(s.amount); // Reduce by 1 wei to work around off-by-one error in atlas mine reward = (accumulatedRewards - s.rewardDebt - 1).toUint256(); } /** * @notice Returns the pending, claimable rewards for all of a user's deposits. * @dev This does not update rewards, so out of date if rewards not recently updated. * Needed to maintain 'view' function type. * * @param user The user to check rewards for. * * @return reward The total amount of MAGIC reward pending. */ function pendingRewardsAll(address user) external view override returns (uint256 reward) { uint256[] memory depositIds = allUserDepositIds[user].values(); for (uint256 i = 0; i < depositIds.length; i++) { reward += pendingRewards(user, depositIds[i]); } } // ============================================ HELPERS ============================================ /** * @dev Approve treasures and legions for withdrawal from the atlas mine. */ function _approveNFTs() internal { address treasureAddr = mine.treasure(); IERC1155Upgradeable(treasureAddr).setApprovalForAll(address(mine), true); address legionAddr = mine.legion(); IERC721Upgradeable(legionAddr).setApprovalForAll(address(mine), true); } /** * @dev Stake tokens held by staker in the Atlas Mine, according to * the predefined lock value. Schedules for staking will be managed by a queue. * * @param _amount Number of tokens to stake */ function _stakeInMine(uint256 _amount) internal { require(_amount <= _totalUsableMagic(), "Not enough funds"); uint256 depositId = ++lastDepositId; uint256 unlockAt = block.timestamp + locktime; stakes.push(Stake({ amount: _amount, unlockAt: unlockAt, depositId: depositId })); mine.deposit(_amount, lock); } /** * @dev Unstakes until we have enough unstaked tokens to meet a specific target. * Used to make sure we can service withdrawals. * * @param target The amount of tokens we want to have unstaked. */ function _unstakeToTarget(uint256 target) internal { uint256 unstaked = 0; uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { Stake memory s = stakes[i]; if (s.unlockAt > block.timestamp && !mine.unlockAll()) { // This stake is not unlocked - stop looking break; } // Withdraw position - auto-harvest uint256 preclaimBalance = _totalUsableMagic(); uint256 targetLeft = target - unstaked; uint256 amount = targetLeft > s.amount ? s.amount : targetLeft; // Do not harvest rewards - if this is running, we've already // harvested in the same fn call mine.withdrawPosition(s.depositId, amount); uint256 postclaimBalance = _totalUsableMagic(); // Increment amount unstaked unstaked += postclaimBalance - preclaimBalance; if (unstaked >= target) { // We unstaked enough break; } } require(unstaked >= target, "Cannot unstake enough"); require(_totalUsableMagic() >= target, "Not enough in contract after unstaking"); // Only check for removal after, so we don't mutate while looping _removeZeroStakes(); } /** * @dev Harvest rewards from the AtlasMine and send them back to * this contract. Cannot harvest entire set of positions due to * gas limits, so positions need to be specified. * * @param depositIds The deposits to harvest rewards for. * * @return earned The amount of rewards earned for depositors, minus the fee. * @return feeEearned The amount of fees earned for the contract operator. */ function _harvestMine(uint256[] memory depositIds) internal returns (uint256, uint256) { uint256 preclaimBalance = magic.balanceOf(address(this)); for (uint256 i = 0; i < depositIds.length; i++) { // Might fail because of reward debt calculation try mine.harvestPosition(depositIds[i]) {} catch { // SafeCast error } } uint256 postclaimBalance = magic.balanceOf(address(this)); uint256 earned = postclaimBalance - preclaimBalance; // Reserve the 'fee' amount of what is earned uint256 feeEarned = (earned * fee) / FEE_DENOMINATOR; feeReserve += feeEarned; emit MineHarvest(earned - feeEarned, feeEarned, depositIds); return (earned - feeEarned, feeEarned); } /** * @dev Harvest rewards from the mine so that stakers can claim. * Recalculate how many rewards are distributed to each share. * * @param depositIds The deposits to harvest rewards for. */ function _updateRewards(uint256[] memory depositIds) internal { if (totalStaked == 0) return; (uint256 newRewards, ) = _harvestMine(depositIds); totalRewardsEarned += newRewards; accRewardsPerShare += (newRewards * ONE) / totalStaked; } /** * @dev After mutating a stake (by withdrawing fully or partially), * get updated data from the staking contract, and update the stake amounts * * @param stakeIndex The index of the stake in the Stakes storage array. * * @return amount The current, updated amount of the stake. */ function _updateStakeDepositAmount(uint256 stakeIndex) internal returns (uint256) { Stake storage s = stakes[stakeIndex]; (, uint256 depositAmount, , , , , ) = mine.userInfo(address(this), s.depositId); s.amount = depositAmount; return s.amount; } /** * @dev Find stakes with zero deposit amount and remove them from tracking. * Starts from the last fully withdrawn stake, and keeps counting until * we find a non-zero stake. * */ function _removeZeroStakes() internal { uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { _updateStakeDepositAmount(i); Stake memory s = stakes[i]; if (s.amount != 0) { // No more zero stakes - can break nextActiveStake = i; break; } } } /** * @dev Calculate total amount of MAGIC usable by the contract. * 'Usable' means available for either withdrawal or re-staking. * Counts unstaked magic less fee reserve. * * @return amount The amount of usable MAGIC. */ function _totalUsableMagic() internal view returns (uint256) { // Current magic held in contract uint256 unstaked = magic.balanceOf(address(this)); return unstaked - feeReserve - tokenBuffer; } /** * @dev Calculate total amount of MAGIC under control of the contract. * Counts staked and unstaked MAGIC. Does _not_ count accumulated * but unclaimed rewards. * * @return amount The total amount of MAGIC under control of the contract. */ function _totalControlledMagic() internal view returns (uint256) { // Current magic staked in mine uint256 staked = 0; uint256 totalStakes = stakes.length; for (uint256 i = nextActiveStake; i < totalStakes; i++) { staked += stakes[i].amount; } return staked + _totalUsableMagic(); } /** * @dev Determine if the current block timestamp falls in an accrual window. * During accrual windows deposits/withdraws/claims are enabled, and reward * updating is enabled. * * @return inWindow Whether the current time falls in an accrual window. */ function _isAccrualWindow() internal view returns (bool inWindow) { require(accrualWindows.length > 0, "Accrual windows not set"); /// time elapsed in day / hours uint256 currentHour = (block.timestamp % 86_400) / 3_600; for (uint256 i = 0; i < accrualWindows.length - 1; i += 2) { if (currentHour >= accrualWindows[i] && currentHour < accrualWindows[i + 1]) { inWindow = true; break; } } } /** * @dev Calculate the current accumulated rewards for a given amount of stake. * Reduces the awards by 1 wei due to an off-by-one issue in the atlas mine. * * @param stakeAmount The amount of stake to accumulate rewards for. */ function _accumulatedRewards(uint256 stakeAmount) internal view returns (int256) { return ((stakeAmount * accRewardsPerShare) / ONE).toInt256(); } /** * @dev For methods only callable by the hoard - Treasure staking/unstaking. */ modifier onlyHoard() { require(hoards[msg.sender], "Not hoard"); _; } /** * @dev For methods that access the token buffer - make sure it is cleared. */ modifier usesBuffer() { _; require(tokenBuffer == 0, "Buffer not clear"); } /** * @dev For methods that affect staking, when an accrual window is not active. */ modifier whenNotAccruing() { require(!_isAccrualWindow(), "In accrual window"); _; } /** * @dev For methods accrue rewards, when staking is not active. */ modifier whenAccruing() { require(_isAccrualWindow(), "Not accruing"); _; } }
Trouver la différence