harb/onchain/src/Stake.sol

272 lines
11 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-03-28 19:55:01 +01:00
pragma solidity ^0.8.19;
2024-02-21 22:20:04 +01:00
2024-03-12 20:22:10 +01:00
import {IERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
2024-03-12 15:29:59 +01:00
import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
2024-04-11 07:28:54 +02:00
import "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
2024-03-12 20:22:10 +01:00
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
2024-03-12 15:29:59 +01:00
import {Math} from "@openzeppelin/utils/math/Math.sol";
2024-02-21 22:20:04 +01:00
import "./interfaces/IStake.sol";
2024-03-12 15:29:59 +01:00
import "./Harb.sol";
2024-02-21 22:20:04 +01:00
2024-03-14 17:31:16 +01:00
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
2024-06-07 11:22:22 +02:00
error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare);
2024-03-14 17:31:16 +01:00
2024-02-21 22:20:04 +01:00
contract Stake is IStake {
2024-03-12 15:29:59 +01:00
using Math for uint256;
2024-02-21 22:20:04 +01:00
2024-03-12 15:29:59 +01:00
uint256 internal DECIMAL_OFFSET = 5 + 2;
2024-03-12 20:22:10 +01:00
uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply
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`.
*/
2024-03-12 20:22:10 +01:00
2024-02-21 22:20:04 +01:00
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
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 share, uint32 creationTime, uint32 taxRate);
2024-04-15 07:08:13 +02:00
event TaxPaid(uint256 indexed positionId, address indexed owner, uint256 taxAmount);
event PositionRemoved(uint256 indexed positionId, uint256 share, uint32 lastTaxTime);
2024-06-07 11:22:22 +02:00
event PositionShrunk(uint256 indexed positionId, uint256 share, uint32 lastTaxTime, uint256 sharesTaken);
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;
2024-03-12 20:22:10 +01:00
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 15:29:59 +01:00
IERC20Metadata 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 public nextPositionId;
2024-02-21 22:20:04 +01:00
uint256 public minStake;
2024-03-14 12:40:57 +01:00
mapping(uint256 => StakingPosition) public positions;
2024-02-21 22:20:04 +01:00
2024-03-12 20:22:10 +01:00
constructor(address _tokenContract) {
2024-03-12 15:29:59 +01:00
tokenContract = IERC20Metadata(_tokenContract);
2024-03-12 20:22:10 +01:00
totalSupply = 10 ** (tokenContract.decimals() + DECIMAL_OFFSET);
2024-03-12 15:29:59 +01:00
taxPool = Harb(_tokenContract).TAX_POOL();
2024-02-21 22:20:04 +01:00
}
2024-03-12 20:22:10 +01:00
function dormantSupply() public view override returns (uint256) {
2024-02-21 22:20:04 +01:00
return totalSupply * (100 - MAX_STAKE) / 100;
}
2024-03-12 20:22:10 +01:00
function authorizedStake() private view returns (uint256) {
2024-03-12 11:38:16 +01:00
return totalSupply * MAX_STAKE / 100;
}
2024-03-12 15:29:59 +01:00
function assetsToShares(uint256 assets, Math.Rounding rounding) private view returns (uint256) {
return assets.mulDiv(totalSupply, tokenContract.totalSupply(), rounding);
//return assets.mulDiv(totalSupply, tokenContract.totalSupply() + 1, rounding);
2024-02-21 22:20:04 +01:00
}
2024-03-12 15:29:59 +01:00
function sharesToAssets(uint256 shares, Math.Rounding rounding) private view returns (uint256) {
//return shares.mulDiv(tokenContract.totalSupply() + 1, totalSupply, rounding);
return shares.mulDiv(tokenContract.totalSupply(), totalSupply, rounding);
2024-02-21 22:20:04 +01:00
}
2024-04-11 07:28:54 +02:00
function permitAndSnatch(
uint256 assets,
address receiver,
uint32 taxRate,
uint256[] calldata positionsToSnatch,
// address owner,
// address spender,
// uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external
returns (uint256 positionId)
{
ERC20Permit(address(tokenContract)).permit(receiver, address(this), assets, deadline, v, r, s);
return snatch(assets, receiver, taxRate, positionsToSnatch);
}
2024-06-07 11:22:22 +02:00
2024-03-12 20:22:10 +01:00
/**
* TODO: deal with metatransactions: While these are generally available
2024-03-12 11:38:16 +01:00
* 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 20:22:10 +01:00
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch)
public
returns (uint256 positionId)
2024-03-12 20:22:10 +01:00
{
2024-02-21 22:20:04 +01:00
// check lower boundary
2024-03-12 15:29:59 +01:00
uint256 sharesWanted = assetsToShares(assets, Math.Rounding.Down);
2024-02-21 22:20:04 +01:00
if (sharesWanted < minStake) {
revert SharesTooLow(receiver, assets, sharesWanted, minStake);
}
2024-04-03 21:43:12 +02:00
// TODO: check that position size is multiple of minStake
2024-02-21 22:20:04 +01:00
2024-06-07 11:22:22 +02:00
uint256 smallestPositionShare = totalSupply;
2024-03-12 11:38:16 +01:00
// run through all suggested positions to snatch
2024-06-07 11:22:22 +02:00
for (uint256 i = 0; i < positionsToSnatch.length - 1; i++) {
2024-03-12 11:38:16 +01:00
StakingPosition storage pos = positions[positionsToSnatch[i]];
2024-02-27 17:41:22 +01:00
if (pos.creationTime == 0) {
2024-03-12 20:22:10 +01:00
//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
}
2024-06-07 11:22:22 +02:00
if (pos.share < smallestPositionShare) {
smallestPositionShare = pos.share;
}
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?
2024-04-15 07:08:13 +02:00
_payTax(positionsToSnatch[i], pos, 0);
_exitPosition(positionsToSnatch[i], pos);
2024-02-21 22:20:04 +01:00
}
2024-03-12 20:22:10 +01:00
2024-03-12 11:38:16 +01:00
uint256 availableStake = authorizedStake() - outstandingStake;
2024-06-07 11:22:22 +02:00
// handle last position
if (positionsToSnatch.length > 0) {
uint256 index = positionsToSnatch.length - 1;
StakingPosition storage pos = positions[positionsToSnatch[index]];
if (pos.creationTime == 0) {
//TODO:
revert PositionNotFound();
}
// check that tax lower
if (taxRate <= pos.taxRate) {
revert TaxTooLow(receiver, taxRate, pos.taxRate, index);
}
if (pos.share < smallestPositionShare) {
smallestPositionShare = pos.share;
}
// dissolve position
_payTax(positionsToSnatch[index], pos, 0);
uint256 lastBitNeeded = sharesWanted - availableStake;
_shrinkPosition(positionsToSnatch[index], pos, lastBitNeeded);
availableStake += lastBitNeeded;
}
2024-02-21 22:20:04 +01:00
if (sharesWanted > availableStake) {
revert ExceededAvailableStake(receiver, sharesWanted, availableStake);
}
2024-06-07 11:22:22 +02:00
// avoid greeving where more positions are freed than needed.
if (availableStake - sharesWanted > smallestPositionShare) {
revert TooMuchSnatch(receiver, sharesWanted, availableStake, smallestPositionShare);
}
2024-02-21 22:20:04 +01:00
// 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
positionId = nextPositionId++;
StakingPosition storage sp = positions[positionId];
2024-03-12 11:38:16 +01:00
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;
emit PositionCreated(positionId, sp.owner, sp.share, sp.creationTime, sp.taxRate);
2024-02-21 22:20:04 +01:00
}
2024-04-15 07:08:13 +02:00
function changeTax(uint256 positionID, uint32 taxRate) public {
StakingPosition storage pos = positions[positionID];
if (pos.owner != msg.sender) {
revert NoPermission(msg.sender, pos.owner);
}
// to prevent snatch-and-change grieving attack, pay TAX_FLOOR_DURATION
require(taxRate > pos.taxRate);
_payTax(positionID, pos, TAX_FLOOR_DURATION);
pos.taxRate = taxRate;
}
function exitPosition(uint256 positionId) public {
StakingPosition storage pos = positions[positionId];
2024-03-12 20:22:10 +01:00
if (pos.owner != msg.sender) {
2024-03-12 15:29:59 +01:00
revert 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-04-15 07:08:13 +02:00
_payTax(positionId, pos, TAX_FLOOR_DURATION);
_exitPosition(positionId, pos);
2024-02-21 22:20:04 +01:00
}
2024-03-12 20:22:10 +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-04-15 07:08:13 +02:00
_payTax(positionID, pos, 0);
2024-02-21 22:20:04 +01:00
}
2024-03-12 15:29:59 +01:00
function taxDue(uint256 positionID, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
StakingPosition storage pos = positions[positionID];
// ihet = Implied Holding Expiry Timestamp
2024-03-12 20:22:10 +01:00
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
? pos.creationTime + taxFloorDuration
: block.timestamp;
2024-03-12 15:29:59 +01:00
uint256 elapsedTime = ihet - pos.lastTaxTime;
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
amountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
}
2024-04-15 07:08:13 +02:00
function _payTax(uint256 positionID, StakingPosition storage pos, uint256 taxFloorDuration) private {
2024-02-23 22:01:23 +01:00
// ihet = Implied Holding Expiry Timestamp
2024-03-12 20:22:10 +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;
2024-03-12 15:29:59 +01:00
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
uint256 taxAmountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
if (taxAmountDue >= assetsBefore) {
2024-02-21 22:20:04 +01:00
// can not pay more tax than value of position
2024-03-12 15:29:59 +01:00
taxAmountDue = assetsBefore;
2024-02-21 22:20:04 +01:00
}
2024-03-12 15:29:59 +01:00
SafeERC20.safeTransfer(tokenContract, taxPool, taxAmountDue);
2024-04-15 07:08:13 +02:00
emit TaxPaid(positionID, pos.owner, taxAmountDue);
2024-03-12 15:29:59 +01:00
if (assetsBefore - taxAmountDue > 0) {
2024-02-21 22:20:04 +01:00
// if something left over, update storage
2024-03-12 15:29:59 +01:00
pos.share = assetsToShares(assetsBefore - taxAmountDue, Math.Rounding.Down);
2024-03-12 12:27:47 +01:00
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
outstandingStake -= pos.share;
2024-04-15 07:08:13 +02:00
emit PositionRemoved(positionID, pos.share, pos.lastTaxTime);
2024-03-12 12:27:47 +01:00
delete pos.owner;
delete pos.creationTime;
2024-02-21 22:20:04 +01:00
}
}
function _exitPosition(uint256 positionId, 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-03-12 15:29:59 +01:00
uint256 assets = sharesToAssets(pos.share, Math.Rounding.Down);
emit PositionRemoved(positionId, pos.share, pos.lastTaxTime);
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);
}
2024-06-07 11:22:22 +02:00
function _shrinkPosition(uint256 positionId, StakingPosition storage pos, uint256 sharesToTake) private {
require (sharesToTake > pos.share, "position too small");
pos.share -= sharesToTake;
uint256 assets = sharesToAssets(sharesToTake, Math.Rounding.Down);
emit PositionShrunk(positionId, pos.share, pos.lastTaxTime, sharesToTake);
SafeERC20.safeTransfer(tokenContract, pos.owner, assets);
}
2024-02-21 22:20:04 +01:00
}