HARB wip1

This commit is contained in:
JulesCrown 2024-02-21 22:20:04 +01:00
parent f1d9104d24
commit dea9e0704a
7 changed files with 163 additions and 319 deletions

155
src/Stake.sol Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface IStake {
function dormantSupply() external view returns(uint256);
}

View file

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