harb/src/Stake.sol

155 lines
5.4 KiB
Solidity
Raw Normal View History

2024-02-21 22:20:04 +01:00
// SPDX-License-Identifier: GPL-3.0-or-later
2024-02-23 22:01:23 +01:00
2024-02-21 22:20:04 +01:00
pragma solidity ^0.8.13;
import "./interfaces/IStake.sol";
import "./interfaces/IHarb.sol";
contract Stake is IStake {
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;
2024-02-23 22:01:23 +01:00
uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time.
2024-02-21 22:20:04 +01:00
/**
* @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);
struct StakingPosition {
2024-02-23 22:01:23 +01:00
uint256 share;
2024-02-21 22:20:04 +01:00
address owner;
2024-02-23 22:01:23 +01:00
uint32 creationTime;
uint32 lastTaxTime;
uint32 taxRate; // e.g. value of 60 = 60% tax per year
2024-02-21 22:20:04 +01:00
}
2024-02-23 22:01:23 +01:00
uint256 public immutable totalSupply;
address private immutable tokenContract;
address private immutable taxPool;
2024-02-21 22:20:04 +01:00
uint256 public outstandingStake;
uint256 private lastTokenId;
uint256 public minStake;
mapping (uint256 positionID => StakingPosition) public positions;
constructor(
address _tokenContract
2024-02-23 22:01:23 +01:00
) {
2024-02-21 22:20:04 +01:00
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++];
2024-02-23 22:01:23 +01:00
sp.share = shares;
2024-02-21 22:20:04 +01:00
sp.owner = receiver;
2024-02-23 22:01:23 +01:00
sp.lastTaxTime = now;
sp.creationTime = now;
2024-02-21 22:20:04 +01:00
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
2024-02-23 22:01:23 +01:00
if(now - pos.creationTime < 60 * 60 * 24 * 3) {
ExitTooEarly(pos.owner, positionID, pos.creationTime);
2024-02-21 22:20:04 +01:00
}
2024-02-23 22:01:23 +01:00
_payTax(pos, TAX_FLOOR_DURATION);
2024-02-21 22:20:04 +01:00
_exitPosition(pos);
}
function payTax(uint256 positionID) public {
StakingPosition pos = positions[positionID];
2024-02-23 22:01:23 +01:00
_payTax(pos, 0);
2024-02-21 22:20:04 +01:00
}
2024-02-23 22:01:23 +01:00
function _payTax(StakingPosition storage pos, uint256 taxFloorDuration) private {
// ihet = Implied Holding Expiry Timestamp
uint256 ihet = (now - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : now;
uint256 elapsedTime = ihet - pos.lastTaxTime;
uint256 assetsBefore = sharesToAssets(pos.share);
2024-02-21 22:20:04 +01:00
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
2024-02-23 22:01:23 +01:00
sp.shares = assetsToShares(assetsBefore - taxDue);
sp.lastTaxTime = now;
2024-02-21 22:20:04 +01:00
} else {
// if nothing left over, liquidate position
2024-02-23 22:01:23 +01:00
outstandingStake -= sp.share;
2024-02-21 22:20:04 +01:00
delete sp;
}
}
function _exitPosition(StakingPosition storage pos) private {
2024-02-23 22:01:23 +01:00
outstandingStake -= pos.share;
2024-02-21 22:20:04 +01:00
address owner = pos.owner;
2024-02-23 22:01:23 +01:00
uint256 assets = sharesToAssets(pos.share);
2024-02-21 22:20:04 +01:00
delete pos;
SafeERC20.safeTransfer(tokenContract, owner, assets);
}
}