diff --git a/src/Stake.sol b/src/Stake.sol new file mode 100644 index 0000000..a715d30 --- /dev/null +++ b/src/Stake.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.13; + +import "./interfaces/IStake.sol"; +import "./interfaces/IHarb.sol"; + +contract Stake is IStake { + + // when ustaking, at least authorizedSupply/minUnstake stake should be claimed + uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply + uint256 internal constant MAX_TAX = 1000; // max 1000% tax + uint256 internal constant TAX_RATE_BASE = 100; + uint256 public immutable totalSupply; + address private immutable tokenContract; + address private immutable taxPool; + + /** + * @dev Attempted to deposit more assets than the max amount for `receiver`. + */ + error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake); + error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId); + error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake); + error NoPermission(address requester, address owner); + error ExitTooEarly(address owner, uint256 positionID, uint32 creationTimestamp); + + + struct StakingPosition { + uint256 stakeShare; + address owner; + uint32 creationTimestamp; + uint32 lastTaxPaymentTimestamp; + uint32 taxRate; // value of 60 = 60% + } + + uint256 public outstandingStake; + uint256 private lastTokenId; + uint256 public minStake; + mapping (uint256 positionID => StakingPosition) public positions; + + + constructor( + string memory name, + string memory symbol, + address _tokenContract + ) ERC20(name, symbol) { + tokenContract = _tokenContract; + IHarb harb = IHarb(_tokenContract); + totalSupply = 100 * 10 ** 5 * harb.decimals(); + taxPool = harb.taxPool(); + } + + function dormantSupply() public view override returns(uint256) { + return totalSupply * (100 - MAX_STAKE) / 100; + } + + function assetsToShares(uint256 assets) private view returns (uint256) { + return assets * totalSupply / IERC20(_tokenContract).totalSupply(); + } + + function sharesToAssets(uint256 shares) private view returns (uint256) { + return shares * IERC20(_tokenContract).totalSupply() / totalSupply; + } + + function snatch(uint256 assets, address receiver, uint64 taxRate, uint256[] positions) public returns(uint256) { + + // check lower boundary + uint256 sharesWanted = assetsToShares(assets); + if (sharesWanted < minStake) { + revert SharesTooLow(receiver, assets, sharesWanted, minStake); + } + + // run through all suggested positions + for (uint i = 0; i < positions.length; i++) { + StakingPosition pos = positions[i]; + // check that tax lower + if (taxRate <= pos.perSecondTaxRate) { + revert TaxTooLow(receiver, taxRate, pos.perSecondTaxRate, i); + } + // dissolve position + _payTax(pos); + _exitPosition(pos); + } + + // now try to make a new position in the free space and hope it is big enough + uint256 availableStake = authorizedStake - outstandingStake; + if (sharesWanted > availableStake) { + revert ExceededAvailableStake(receiver, sharesWanted, availableStake); + } + + // transfer + SafeERC20.safeTransferFrom(tokenContract, _msgSender(), address(this), assets); + + // mint + StakingPosition storage sp = c.funders[lastTokenId++]; + sp.stakeShare = shares; + sp.owner = receiver; + sp.lastTaxPaymentTimestamp = now; + sp.creationTimestamp = now; + sp.perSecondTaxRate = taxRate; + + outstandingStake += sharesWanted; + + return lastTokenId; + } + + + function exitPosition(uint256 positionID) public { + StakingPosition pos = positions[positionID]; + if(pos.owner != _msgSender()) { + NoPermission(_msgSender(), pos.owner); + } + // to prevent snatch-and-exit grieving attack + if(now - pos.creationTimestamp < 60 * 60 * 24 * 3) { + ExitTooEarly(pos.owner, positionID, pos.creationTimestamp); + } + _payTax(pos); + _exitPosition(pos); + } + + function payTax(uint256 positionID) public { + + StakingPosition pos = positions[positionID]; + _payTax(pos); + } + + + function _payTax(StakingPosition storage pos) private { + uint256 elapsedTime = now - pos.lastTaxPaymentTimestamp; + uint256 assetsBefore = sharesToAssets(pos.stakeShare); + uint256 taxDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE; + if (taxDue >= assetsBefore) { + // can not pay more tax than value of position + taxDue = assetsBefore; + } + SafeERC20.safeTransfer(tokenContract, taxPool, taxDue); + if (assetsBefore - taxDue > 0) { + // if something left over, update storage + sp.stakeShares = assetsToShares(assetsBefore - taxDue); + sp.lastTaxPaymentTimestamp = now; + } else { + // if nothing left over, liquidate position + outstandingStake -= sp.stakeShare; + delete sp; + } + } + + function _exitPosition(StakingPosition storage pos) private { + outstandingStake -= pos.stakeShare; + address owner = pos.owner; + uint256 assets = sharesToAssets(pos.stakeShare); + delete pos; + SafeERC20.safeTransfer(tokenContract, owner, assets); + } + +} diff --git a/src/StakeX.sol b/src/StakeX.sol deleted file mode 100644 index bc1cd88..0000000 --- a/src/StakeX.sol +++ /dev/null @@ -1,173 +0,0 @@ -// 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); - } - -} diff --git a/src/interfaces/IERC1363.sol b/src/interfaces/IERC1363.sol deleted file mode 100644 index 57ae276..0000000 --- a/src/interfaces/IERC1363.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363.sol) - -pragma solidity ^0.8.0; - -import "@openzeppelin/contracts/interfaces//IERC20.sol"; -import "@openzeppelin/contracts/interfaces//IERC165.sol"; - -interface IERC1363 is IERC20, IERC165 { - /* - * Note: the ERC-165 identifier for this interface is 0xb0202a11. - * 0xb0202a11 === - * bytes4(keccak256('transferAndCall(address,uint256)')) ^ - * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ - * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ - * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^ - * bytes4(keccak256('approveAndCall(address,uint256)')) ^ - * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) - */ - - /** - * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver - * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred - * @return true unless throwing - */ - function transferAndCall(address to, uint256 value) external returns (bool); - - /** - * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver - * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred - * @param data bytes Additional data with no specified format, sent in call to `to` - * @return true unless throwing - */ - function transferAndCall(address to, uint256 value, bytes memory data) external returns (bool); - - /** - * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver - * @param from address The address which you want to send tokens from - * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred - * @return true unless throwing - */ - function transferFromAndCall(address from, address to, uint256 value) external returns (bool); - - /** - * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver - * @param from address The address which you want to send tokens from - * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred - * @param data bytes Additional data with no specified format, sent in call to `to` - * @return true unless throwing - */ - function transferFromAndCall(address from, address to, uint256 value, bytes memory data) external returns (bool); - - /** - * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender - * and then call `onApprovalReceived` on spender. - * @param spender address The address which will spend the funds - * @param value uint256 The amount of tokens to be spent - */ - function approveAndCall(address spender, uint256 value) external returns (bool); - - /** - * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender - * and then call `onApprovalReceived` on spender. - * @param spender address The address which will spend the funds - * @param value uint256 The amount of tokens to be spent - * @param data bytes Additional data with no specified format, sent in call to `spender` - */ - function approveAndCall(address spender, uint256 value, bytes memory data) external returns (bool); -} \ No newline at end of file diff --git a/src/interfaces/IERC1363Receiver.sol b/src/interfaces/IERC1363Receiver.sol deleted file mode 100644 index 2f5f641..0000000 --- a/src/interfaces/IERC1363Receiver.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363Receiver.sol) - -pragma solidity ^0.8.0; - -interface IERC1363Receiver { - - event Received(address operator, address from, uint256 value, bytes data); - - /* - * Note: the ERC-165 identifier for this interface is 0x88a7ca5c. - * 0x88a7ca5c === bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)")) - */ - - /** - * @notice Handle the receipt of ERC1363 tokens - * @dev Any ERC1363 smart contract calls this function on the recipient - * after a `transfer` or a `transferFrom`. This function MAY throw to revert and reject the - * transfer. Return of other than the magic value MUST result in the - * transaction being reverted. - * Note: the token contract address is always the message sender. - * @param operator address The address which called `transferAndCall` or `transferFromAndCall` function - * @param from address The address which are token transferred from - * @param value uint256 The amount of tokens transferred - * @param data bytes Additional data with no specified format - * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` - * unless throwing - */ - function onTransferReceived( - address operator, - address from, - uint256 value, - bytes memory data - ) external returns (bytes4); -} \ No newline at end of file diff --git a/src/interfaces/IERC1363Spender.sol b/src/interfaces/IERC1363Spender.sol deleted file mode 100644 index 9df76c5..0000000 --- a/src/interfaces/IERC1363Spender.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363Spender.sol) - -pragma solidity ^0.8.0; - -interface IERC1363Spender { - /* - * Note: the ERC-165 identifier for this interface is 0x7b04a2d0. - * 0x7b04a2d0 === bytes4(keccak256("onApprovalReceived(address,uint256,bytes)")) - */ - - /** - * @notice Handle the approval of ERC1363 tokens - * @dev Any ERC1363 smart contract calls this function on the recipient - * after an `approve`. This function MAY throw to revert and reject the - * approval. Return of other than the magic value MUST result in the - * transaction being reverted. - * Note: the token contract address is always the message sender. - * @param owner address The address which called `approveAndCall` function - * @param value uint256 The amount of tokens to be spent - * @param data bytes Additional data with no specified format - * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` - * unless throwing - */ - function onApprovalReceived(address owner, uint256 value, bytes memory data) external returns (bytes4); -} \ No newline at end of file diff --git a/src/interfaces/IStake.sol b/src/interfaces/IStake.sol new file mode 100644 index 0000000..2975fe9 --- /dev/null +++ b/src/interfaces/IStake.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +interface IStake { + + function dormantSupply() external view returns(uint256); + +} diff --git a/src/interfaces/IStakeX.sol b/src/interfaces/IStakeX.sol deleted file mode 100644 index 5ef2aaa..0000000 --- a/src/interfaces/IStakeX.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -interface IStakeX { - - function dormantSupply() external view returns(uint256); - - function outstandingSupply() external view returns(uint256); - - function authorizedSupply() external view returns(uint256); - -}