// SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.17; import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; /** * @dev Deposit token for a period of time to recieve rewards. */ contract OneSidedStaking is Ownable2Step, Pausable, ReentrancyGuard { using SafeERC20 for IERC20; using SafeCast for uint256; using SafeCast for int256; IERC20 public immutable STAKING_TOKEN; IERC20 public immutable REWARD_TOKEN; // variable has been made constant to cut down on gas costs uint256 constant public ACC_PRECISION = 1e23; // 32 days uint256 public defaultLockup = 2764800; uint256 public rewardPerSecond = 0.128 ether; // 11111 daily uint256 public totalStaked; uint256 public lastRewardTs; uint256 public accReward; /// @notice Debts are done without collateral, so it's assumed that parties are trusted /// (e.g. DAO multisig or authorized parties by the DAO). mapping(address => uint256) public trustedCreditLines; mapping(address => uint256) public trustedDebts; /// @notice Info of each reward debt and virtual amount. /// One object is instantiated per address per pool, info of each address participating in rewards program. mapping(address => DepositInfo) public depositInfo; mapping(address => UnstakingRequest[]) public unstakingRequests; uint256 public pendingUnstaking; struct DepositInfo { int256 rewardDebt; /// assumption here is that we will never go over 2^256 -1 uint256 virtualAmount; } struct UnstakingRequest { uint256 amount; uint256 lockedUntil; } event Staked( address indexed _stacker, uint256 _newAmount, uint256 _totalAmount ); event UnstakeRequest( address indexed _stacker, uint256 _ticket, uint256 _amount, uint256 _lockup ); event Unstaked( address indexed _stacker, uint256 _amount ); event UnstakedWithdrawn( address indexed _stacker, uint256 _ticket, uint256 _amount ); event Borrowed( address indexed _loaner, uint256 _amount ); event Repaid( address indexed _loaner, address indexed _payer, uint256 _amount ); event AuthorizeCreditLine( address indexed _authorizer, address indexed _loaner, uint256 _maxAmount ); event StopCreditLine( address indexed _authorizer, address indexed _loaner ); event RewardsUpdated( address _triggerAddress, uint256 _accumulatedRewards, uint256 _totalStaked ); event RewardRate(uint256 _rate); event Lockup(uint256 _lockupSeconds); error Unauthorized(); error NotEnoughTokens(); error InvalidAmount(); error LockedTokens(); error NoReward(); error InvalidRewardRate(); error InvalidLockup(); /** * @dev Configure staking options. * * @param _stakingToken ERC20 token which is used for staking. * @param _rewardToken ERC20 token which used for rewarding users. */ constructor(IERC20 _stakingToken, IERC20 _rewardToken) { STAKING_TOKEN = _stakingToken; REWARD_TOKEN = _rewardToken; lastRewardTs = block.timestamp; emit RewardRate(rewardPerSecond); emit Lockup(defaultLockup); _pause(); } /** * @dev Check how many reward tokens are pending to be paid to staker's wallet. * * @param _wallet address which stakes token. */ function pendingRewards(address _wallet) public view returns (uint256) { uint256 _totalStaked = totalStaked; if (_totalStaked == 0) return 0; uint256 _currentAccReward = accReward; if (lastRewardTs < block.timestamp) { /// assumption here is that we will never go over 2^256 -1 uint256 _accReward = (block.timestamp - lastRewardTs) * rewardPerSecond * ACC_PRECISION / _totalStaked; _currentAccReward += _accReward; } uint256 _shares = depositInfo[_wallet].virtualAmount; int256 _debt = depositInfo[_wallet].rewardDebt; /// assumption here is that we will never go over 2^256 -1 int256 _pending = (_currentAccReward * _shares / ACC_PRECISION).toInt256(); if (_pending <= _debt) return 0; return (_pending - _debt).toUint256(); } /** * @dev Claim all pending reward tokens. Can be paused by admin. */ function claim() external nonReentrant whenNotPaused { updateRewards(); uint256 _shares = depositInfo[msg.sender].virtualAmount; int256 _debt = depositInfo[msg.sender].rewardDebt; /// assumption here is that we will never go over 2^256 -1 int256 _pending = (accReward * _shares / ACC_PRECISION).toInt256(); int256 _reward = _pending - _debt; if (_reward <= 0) revert NoReward(); depositInfo[msg.sender].rewardDebt = _debt + _reward; REWARD_TOKEN.safeTransfer(msg.sender, _reward.toUint256()); } /** * @dev Normally all pending tokens should be claimed for which the staking period has passed. */ function claimAllUnstaked() external nonReentrant { uint256 _totalAmount; for (uint256 _i = 0; _i < unstakingRequests[msg.sender].length; _i++) { if (block.timestamp < unstakingRequests[msg.sender][_i].lockedUntil) continue; uint256 _amount = unstakingRequests[msg.sender][_i].amount; if (_amount == 0) continue; delete unstakingRequests[msg.sender][_i]; _totalAmount += _amount; emit UnstakedWithdrawn(msg.sender, _i, _amount); } if (_totalAmount == 0) revert InvalidAmount(); pendingUnstaking -= _totalAmount; STAKING_TOKEN.safeTransfer(msg.sender, _totalAmount); } /** * @dev Claim specific claim by it's ticket number. * * @param _ticket exact ticket to claim. */ function claimUnstaked(uint256 _ticket) external nonReentrant { if (block.timestamp < unstakingRequests[msg.sender][_ticket].lockedUntil) revert LockedTokens(); uint256 _amount = unstakingRequests[msg.sender][_ticket].amount; if (_amount == 0) revert InvalidAmount(); delete unstakingRequests[msg.sender][_ticket]; pendingUnstaking -= _amount; emit UnstakedWithdrawn(msg.sender, _ticket, _amount); STAKING_TOKEN.safeTransfer(msg.sender, _amount); } /** * @dev Transfer amount of staking tokens to staking contract to earn rewards in reward tokens. * Keep in mind that withdrawals are not immediatly possible if defaultLockup is set to be * more than 0. Can be paused by admin. * * @param _amount amount to unstake. */ function stake(uint256 _amount) external nonReentrant whenNotPaused { if (_amount == 0) revert InvalidAmount(); // we have to update the pool before we allow a user to deposit so that we can correctly calculate their reward debt // if we didn't do this, it would allow users to steal from us by calling deposits and gaining rewards they aren't entitled to updateRewards(); uint256 _balance = STAKING_TOKEN.balanceOf(msg.sender); if (_balance < _amount) revert NotEnoughTokens(); STAKING_TOKEN.safeTransferFrom(msg.sender, address(this), _amount); DepositInfo memory _depositInfo = depositInfo[msg.sender]; uint256 _currentCredit = pendingRewards(msg.sender); uint256 _newAmount = _depositInfo.virtualAmount + _amount; uint256 _newTotal = totalStaked + _amount; int256 _newDebt = (_newAmount * accReward / ACC_PRECISION).toInt256() - _currentCredit.toInt256(); depositInfo[msg.sender] = DepositInfo( _newDebt, _newAmount ); totalStaked = _newTotal; emit Staked(msg.sender, _amount, _newAmount); } /** * @dev Send unstaking requests, which needs to be claimed after the unlocking period. * If defaultLockup is set as 0, can be claimed immediatly. * * @param _amount amount to unstake. */ function unstake(uint256 _amount) external nonReentrant { if (_amount == 0) revert InvalidAmount(); updateRewards(); uint256 _balance = STAKING_TOKEN.balanceOf(address(this)); if (_balance < _amount) revert NotEnoughTokens(); DepositInfo memory _depositInfo = depositInfo[msg.sender]; uint256 _loan = _depositInfo.virtualAmount; if (_loan < _amount) revert NotEnoughTokens(); uint256 _currentCredit = pendingRewards(msg.sender); uint256 _newAmount = _loan - _amount; uint256 _newTotal = totalStaked - _amount; int256 _newDebt = (_newAmount * accReward / ACC_PRECISION).toInt256() - _currentCredit.toInt256(); depositInfo[msg.sender] = DepositInfo(_newDebt, _newAmount); totalStaked = _newTotal; pendingUnstaking += _amount; uint256 _lockedUntil = block.timestamp + defaultLockup; UnstakingRequest memory _newRequest = UnstakingRequest(_amount, _lockedUntil); unstakingRequests[msg.sender].push(_newRequest); emit UnstakeRequest(msg.sender, unstakingRequests[msg.sender].length - 1, _amount, _lockedUntil); } /** * @dev Keep track of accumulated reward amount for all stakers. */ function updateRewards() public { if (lastRewardTs < block.timestamp) { uint256 _accReward = accReward; uint256 _totalStaked = totalStaked; if (_totalStaked > 0) { _accReward += (block.timestamp - lastRewardTs) * rewardPerSecond * ACC_PRECISION / _totalStaked; accReward = _accReward; } lastRewardTs = block.timestamp; emit RewardsUpdated(msg.sender, _accReward, _totalStaked); } } ///////////////////////////////////////////////////////////////////////////// // ADMIN ZONE // ///////////////////////////////////////////////////////////////////////////// /** * @dev DAO address or authorized wallets of the DAO can loan staking tokens from this contract. * DAO address can loan no more than pending unstaking requests also authorized parties can have an individual allowance. * No collateral is provided, so all loans provided should be guaranteed by the DAO. * * @param _amount amount to loan. */ function adminBorrow(uint256 _amount) external nonReentrant whenNotPaused { if (_amount == 0) revert InvalidAmount(); uint256 _balance = STAKING_TOKEN.balanceOf(address(this)); if (_balance < pendingUnstaking + _amount) revert NotEnoughTokens(); uint256 _currentDebt = trustedDebts[msg.sender]; if (msg.sender != owner() && trustedCreditLines[msg.sender] < _amount + _currentDebt) revert Unauthorized(); trustedDebts[msg.sender] = _amount + _currentDebt; STAKING_TOKEN.safeTransfer(msg.sender, _amount); emit Borrowed(msg.sender, _amount); } /** * @dev Any wallet can replay debt on behalf of another wallet. * e.g. DAO can replay debt for market maker. * * @param _debtor wallet whose debt is repaid. * @param _amount amount to repay. */ function returnDebt(address _debtor, uint256 _amount) external nonReentrant { uint256 _balance = STAKING_TOKEN.balanceOf(msg.sender); if (_balance < _amount) revert NotEnoughTokens(); uint256 _debt = trustedDebts[_debtor]; if (_amount == 0 || _amount > _debt) revert InvalidAmount(); STAKING_TOKEN.safeTransferFrom(msg.sender, address(this), _amount); trustedDebts[_debtor] = _debt - _amount; emit Repaid(_debtor, msg.sender, _amount); } /** * @dev Admin can authorize and address to take uncollateralized loan from smart contract for give amount. * * @param _authorizedDebtor address which can take uncollateralized loan from staking contract. * @param _amount amount which can be loaned, set to 0 to remove authorization. */ function adminAuthorizeCreditLine(address _authorizedDebtor, uint256 _amount) external onlyOwner { trustedCreditLines[_authorizedDebtor] = _amount; address _owner = owner(); if (_amount > 0) { emit AuthorizeCreditLine(_owner, _authorizedDebtor, _amount); } else { emit StopCreditLine(_owner, _authorizedDebtor); } } /** * @dev Admin can pause staking and claims of rewards, but not unstaking. */ function adminPause() external onlyOwner { _pause(); } /** * @dev Admin can resume staking and claims of reward. */ function adminUnpause() external onlyOwner { _unpause(); } /** * @dev Admin can change reward per block, expected to distribute 50m in scope of 2 years * unless canceled earlier. * * @param _newRewardPerSecond expect to be 0.8 or less, which would be ~50m in 2 years. */ function adminChangeReward(uint256 _newRewardPerSecond) external onlyOwner { if (_newRewardPerSecond > 0.8 ether) revert InvalidRewardRate(); updateRewards(); rewardPerSecond = _newRewardPerSecond; emit RewardRate(_newRewardPerSecond); } /** * @dev Admin can set lockup from 0 (no lockup) to 1 year. * Ongoing unstaking requests won't be affected, applies only to a new unstaking. * * @param _newLockup Seconds of lockup after unstaking request, 31536000 (one year is allowed max). */ function adminChangeLockup(uint256 _newLockup) external onlyOwner { if (_newLockup > 31536000) revert InvalidLockup(); defaultLockup = _newLockup; emit Lockup(_newLockup); } }