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;
|
|
|
|
|
|
2024-03-12 11:38:16 +01:00
|
|
|
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
|
|
|
|
|
import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
2024-02-21 22:20:04 +01:00
|
|
|
import "./interfaces/IStake.sol";
|
|
|
|
|
import "./interfaces/IHarb.sol";
|
|
|
|
|
|
|
|
|
|
contract Stake is IStake {
|
|
|
|
|
|
|
|
|
|
uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply
|
2024-02-27 17:41:22 +01:00
|
|
|
uint256 internal constant MAX_TAX = 1000; // max 1000% tax per year
|
2024-02-21 22:20:04 +01:00
|
|
|
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);
|
2024-03-12 11:38:16 +01:00
|
|
|
error PositionNotFound();
|
2024-02-21 22:20:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2024-03-12 12:27:47 +01:00
|
|
|
IERC20 private immutable tokenContract;
|
2024-02-23 22:01:23 +01:00
|
|
|
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-03-12 12:27:47 +01:00
|
|
|
tokenContract = IERC20(_tokenContract);
|
2024-02-21 22:20:04 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-12 11:38:16 +01:00
|
|
|
function authorizedStake() private pure returns(uint256) {
|
|
|
|
|
return totalSupply * MAX_STAKE / 100;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-21 22:20:04 +01:00
|
|
|
function assetsToShares(uint256 assets) private view returns (uint256) {
|
2024-03-12 12:27:47 +01:00
|
|
|
return assets * totalSupply / tokenContract.totalSupply();
|
2024-02-21 22:20:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sharesToAssets(uint256 shares) private view returns (uint256) {
|
2024-03-12 12:27:47 +01:00
|
|
|
return shares * tokenContract.totalSupply() / totalSupply;
|
2024-02-21 22:20:04 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-12 11:38:16 +01:00
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
* TODO: deal with metatransactions: While these are generally available
|
|
|
|
|
* via msg.sender and msg.data, they should not be accessed in such a direct
|
|
|
|
|
* manner, since when dealing with meta-transactions the account sending and
|
|
|
|
|
* paying for execution may not be the actual sender (as far as an application
|
|
|
|
|
* is concerned).
|
|
|
|
|
*/
|
2024-03-12 12:27:47 +01:00
|
|
|
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) public returns(uint256) {
|
2024-02-21 22:20:04 +01:00
|
|
|
|
|
|
|
|
// check lower boundary
|
|
|
|
|
uint256 sharesWanted = assetsToShares(assets);
|
|
|
|
|
if (sharesWanted < minStake) {
|
|
|
|
|
revert SharesTooLow(receiver, assets, sharesWanted, minStake);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-12 11:38:16 +01:00
|
|
|
// run through all suggested positions to snatch
|
|
|
|
|
for (uint i = 0; i < positionsToSnatch.length; i++) {
|
|
|
|
|
StakingPosition storage pos = positions[positionsToSnatch[i]];
|
2024-02-27 17:41:22 +01:00
|
|
|
if (pos.creationTime == 0) {
|
|
|
|
|
//TODO:
|
2024-03-12 11:38:16 +01:00
|
|
|
revert PositionNotFound();
|
2024-02-27 17:41:22 +01:00
|
|
|
}
|
2024-02-21 22:20:04 +01:00
|
|
|
// check that tax lower
|
2024-03-12 12:27:47 +01:00
|
|
|
if (taxRate <= pos.taxRate) {
|
|
|
|
|
revert TaxTooLow(receiver, taxRate, pos.taxRate, i);
|
2024-02-21 22:20:04 +01:00
|
|
|
}
|
|
|
|
|
// dissolve position
|
2024-03-12 12:27:47 +01:00
|
|
|
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
|
|
|
|
_payTax(pos, 0);
|
2024-02-21 22:20:04 +01:00
|
|
|
_exitPosition(pos);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-12 12:27:47 +01:00
|
|
|
// try to make a new position in the free space and hope it is big enough
|
2024-03-12 11:38:16 +01:00
|
|
|
uint256 availableStake = authorizedStake() - outstandingStake;
|
2024-02-21 22:20:04 +01:00
|
|
|
if (sharesWanted > availableStake) {
|
|
|
|
|
revert ExceededAvailableStake(receiver, sharesWanted, availableStake);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// transfer
|
2024-03-12 11:38:16 +01:00
|
|
|
SafeERC20.safeTransferFrom(tokenContract, msg.sender, address(this), assets);
|
2024-02-21 22:20:04 +01:00
|
|
|
|
|
|
|
|
// mint
|
2024-03-12 11:38:16 +01:00
|
|
|
StakingPosition storage sp = positions[lastTokenId++];
|
|
|
|
|
sp.share = sharesWanted;
|
2024-02-21 22:20:04 +01:00
|
|
|
sp.owner = receiver;
|
2024-03-12 12:27:47 +01:00
|
|
|
sp.lastTaxTime = uint32(block.timestamp);
|
|
|
|
|
sp.creationTime = uint32(block.timestamp);
|
|
|
|
|
sp.taxRate = taxRate;
|
2024-02-21 22:20:04 +01:00
|
|
|
|
|
|
|
|
outstandingStake += sharesWanted;
|
|
|
|
|
|
|
|
|
|
return lastTokenId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function exitPosition(uint256 positionID) public {
|
2024-03-12 11:38:16 +01:00
|
|
|
StakingPosition storage pos = positions[positionID];
|
|
|
|
|
if(pos.owner != msg.sender) {
|
|
|
|
|
NoPermission(msg.sender, pos.owner);
|
2024-02-21 22:20:04 +01:00
|
|
|
}
|
2024-03-12 11:38:16 +01:00
|
|
|
// to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION
|
2024-02-23 22:01:23 +01:00
|
|
|
_payTax(pos, TAX_FLOOR_DURATION);
|
2024-02-21 22:20:04 +01:00
|
|
|
_exitPosition(pos);
|
|
|
|
|
}
|
2024-03-12 12:27:47 +01:00
|
|
|
|
2024-02-21 22:20:04 +01:00
|
|
|
function payTax(uint256 positionID) public {
|
2024-03-12 11:38:16 +01:00
|
|
|
StakingPosition storage pos = positions[positionID];
|
2024-03-12 12:27:47 +01:00
|
|
|
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
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
|
2024-03-12 12:27:47 +01:00
|
|
|
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp;
|
2024-02-23 22:01:23 +01:00
|
|
|
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-03-12 12:27:47 +01:00
|
|
|
pos.share = assetsToShares(assetsBefore - taxDue);
|
|
|
|
|
pos.lastTaxTime = uint32(block.timestamp);
|
2024-02-21 22:20:04 +01:00
|
|
|
} else {
|
|
|
|
|
// if nothing left over, liquidate position
|
2024-03-12 11:38:16 +01:00
|
|
|
// TODO: emit event
|
|
|
|
|
outstandingStake -= pos.share;
|
2024-03-12 12:27:47 +01:00
|
|
|
delete pos.owner;
|
|
|
|
|
delete pos.creationTime;
|
2024-02-21 22:20:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-03-12 12:27:47 +01:00
|
|
|
delete pos.owner;
|
|
|
|
|
delete pos.creationTime;
|
2024-02-21 22:20:04 +01:00
|
|
|
SafeERC20.safeTransfer(tokenContract, owner, assets);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|