harb/src/StakeX.sol
2023-11-23 22:14:47 +01:00

173 lines
6.3 KiB
Solidity

// 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);
}
}