2023-11-23 22:14:47 +01:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
2023-11-21 20:27:49 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|