// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.13; // didn't use solmate here because totalSupply needs override import "./interfaces/IStakeX.sol"; import "./ERC1363.sol"; contract StakeX is ERC1363, IERC1363Receiver, IStakeX { // when ustaking, at least authorizedSupply/minUnstake stake should be claimed uint256 internal constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; uint256 internal constant MIN_STAKE = 100000; uint256 internal constant MIN_UNSTAKE_STEP = 5; uint256 internal constant USTAKE_TIME = 60 * 60 * 72; mapping(address => uint256) private unstakeSlot; address private tokenContract; uint256 public maxStake; constructor( string memory name, string memory symbol, address _tokenContract ) ERC20(name, symbol) { tokenContract = _tokenContract; _mint(_tokenContract, MIN_STAKE * 1 ether); } function getUnstakeSlot(address account) view public returns (uint256 total, uint256 left, uint256 start) { uint256 raw = unstakeSlot[account]; start = uint64(raw); left = uint96(raw >> (64)); total = uint96(raw >> (96 + 64)); } function dormantSupply() public view override returns(uint256) { return balanceOf(tokenContract); } function outstandingSupply() public view override returns(uint256) { return super.totalSupply() - balanceOf(tokenContract); } function authorizedSupply() public view override returns(uint256) { return super.totalSupply(); } function totalSupply() public view override(ERC20, IERC20) returns(uint256) { return outstandingSupply(); } function onTransferReceived( address operator, address from, uint256 value, bytes memory data ) external override returns (bytes4) { if (data.length == 1) { if (data[0] == 0x00) return bytes4(0); if (data[0] == 0x01) revert("onTransferReceived revert"); if (data[0] == 0x02) revert(); if (data[0] == 0x03) assert(false); } if (operator == tokenContract) { // a user has initiated staking require(data.length <= 32, "The byte array is too long"); _stake(from, value, uint256(bytes32(data))); } else { emit Received(operator, from, value, data); } return this.onTransferReceived.selector; } /** * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding * this function. * * Emits a {Transfer} event. */ function _update(address from, address to, uint256 value) internal override { super._update(from, to, value); // don't emit transfer event when updating staking pool. if (from != tokenContract && to != tokenContract) { emit Transfer(from, to, value); } } function unstake(address from, uint256 amount) external { address spender = _msgSender(); if (from == address(0)) { revert ERC20InvalidSender(address(0)); } if (from != spender) { _spendAllowance(from, spender, amount); } // prevent unstaking tiny amounts require(amount >= authorizedSupply() / MIN_STAKE); _update(from, tokenContract, amount); (, uint256 left, ) = getUnstakeSlot(from); uint256 total = amount + left; _setUstakeSlot(from, total, total, block.timestamp); emit Transfer(from, address(0), amount); } // totalSupply is activeSupply + stakingPool supply of BloodX function _stake(address account, uint256 amount, uint256 _totalSupply) internal { uint256 authorizedStake = authorizedSupply(); require(authorizedStake > 0, "no stake issued yet"); require(amount > 0, "can not stake 0 amount"); require(_totalSupply > 0, "no stake issued yet"); // to avoid arithmetic overflow amount should be < MAX_INT / authorizedStake; require(amount < MAX_INT / authorizedStake, "arithmetic overflow"); uint256 newStake = amount * authorizedStake / _totalSupply; // check stake limits if (maxStake > 0) { require(outstandingSupply() + amount <= maxStake, "not enough stake outstanding"); } _update(tokenContract, account, newStake); emit Transfer(address(0), account, newStake); require(balanceOf(account) >= authorizedStake / MIN_STAKE, "stake too small"); } function _setUstakeSlot(address account, uint256 total, uint256 left, uint256 start) internal { unstakeSlot[account] = uint64(start) + (left << 64) + (total << (96 + 64)); } function _vestedStake(uint256 total, uint256 left, uint256 start, uint256 _now) internal pure returns (uint256) { if (_now <= start) { return 0; } // calculate amountVested // amountVested is amount that can be withdrawn according to time passed uint256 timePassed = _now - start; if (timePassed > USTAKE_TIME) { timePassed = USTAKE_TIME; } uint256 amountVested = total * timePassed / USTAKE_TIME; uint256 amountFrozen = total - amountVested; if (left <= amountFrozen) { return 0; } return left - amountFrozen; } // executes a powerdown request function unstakeTick(address account) public { (uint256 total,uint256 left,uint256 start) = getUnstakeSlot(account); uint256 amount = _vestedStake(total, left, start, block.timestamp); // prevent power down in tiny steps uint256 minStep = total / MIN_UNSTAKE_STEP; require(left <= minStep || minStep <= amount); left = left - amount; // handle ustake completed if (left == 0) { start = 0; total = 0; } _setUstakeSlot(account, total, left, start); bytes memory data = new bytes(32); uint256 overall = authorizedSupply(); assembly { mstore(add(data, 32), overall) } _checkOnTransferReceived(address(this), account, tokenContract, amount, data); } }