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
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-06-13 08:28:42 +02:00
uint256 [ ] public TAX_RATES = [ 1 , 3 , 5 , 8 , 12 , 18 , 24 , 30 , 40 , 50 , 60 , 80 , 100 , 130 , 180 , 250 , 320 , 420 , 540 , 700 , 920 , 1200 , 1600 ] ; //TODO: increate until 3 days take all position
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
2024-03-18 12:42:30 +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 ) ;
2024-03-18 12:42:30 +01:00
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-03-18 12:42:30 +01:00
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 ;
2024-03-18 12:42:30 +01:00
uint256 public nextPositionId ;
2024-06-13 08:28:42 +02:00
uint256 public minStake ; // TODO: handle this
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-06-13 08:28:42 +02:00
nextPositionId = 654321 ;
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-13 08:28:42 +02:00
event DEBUG ( uint256 line ) ;
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
2024-03-18 12:42:30 +01:00
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-06-13 08:28:42 +02:00
emit DEBUG ( authorizedStake ( ) ) ;
2024-04-03 21:43:12 +02:00
// TODO: check that position size is multiple of minStake
2024-06-09 16:06:41 +02:00
// TODO: check that tax rate within limits of array
2024-02-21 22:20:04 +01:00
2024-06-07 11:22:22 +02:00
uint256 smallestPositionShare = totalSupply ;
2024-06-13 08:28:42 +02:00
emit DEBUG ( outstandingStake ) ;
2024-06-07 12:33:20 +02:00
uint256 availableStake = authorizedStake ( ) - outstandingStake ;
2024-06-07 11:22:22 +02:00
2024-06-13 08:28:42 +02:00
emit DEBUG ( 2 ) ;
if ( positionsToSnatch . length >= 2 ) {
// run through all but last positions to snatch
2024-06-07 12:33:20 +02:00
for ( uint256 i = 0 ; i < positionsToSnatch . length - 1 ; i ++ ) {
StakingPosition storage pos = positions [ positionsToSnatch [ i ] ] ;
if ( pos . creationTime == 0 ) {
//TODO:
revert PositionNotFound ( ) ;
}
// check that tax lower
if ( taxRate <= pos . taxRate ) {
revert TaxTooLow ( receiver , taxRate , pos . taxRate , i ) ;
}
if ( pos . share < smallestPositionShare ) {
smallestPositionShare = pos . share ;
}
// dissolve position
// TODO: what if someone calls payTax and exitPosition in the same transaction?
_payTax ( positionsToSnatch [ i ] , pos , 0 ) ;
_exitPosition ( positionsToSnatch [ i ] , pos ) ;
2024-06-07 11:22:22 +02:00
}
2024-06-13 08:28:42 +02:00
}
availableStake = authorizedStake ( ) - outstandingStake ;
2024-03-12 20:22:10 +01:00
2024-06-13 08:28:42 +02:00
if ( positionsToSnatch . length > 0 ) {
// handle last position, either shrink or snatch
2024-06-07 11:22:22 +02:00
uint256 index = positionsToSnatch . length - 1 ;
2024-06-09 16:06:41 +02:00
StakingPosition storage lastPos = positions [ positionsToSnatch [ index ] ] ;
if ( lastPos . creationTime == 0 ) {
2024-06-07 11:22:22 +02:00
//TODO:
revert PositionNotFound ( ) ;
}
// check that tax lower
2024-06-09 16:06:41 +02:00
if ( taxRate <= lastPos . taxRate ) {
revert TaxTooLow ( receiver , taxRate , lastPos . taxRate , index ) ;
2024-06-07 11:22:22 +02:00
}
2024-06-09 16:06:41 +02:00
if ( lastPos . share < smallestPositionShare ) {
smallestPositionShare = lastPos . share ;
2024-06-07 11:22:22 +02:00
}
// dissolve position
2024-06-09 16:06:41 +02:00
_payTax ( positionsToSnatch [ index ] , lastPos , 0 ) ;
2024-06-13 08:28:42 +02:00
emit DEBUG ( sharesWanted ) ;
emit DEBUG ( availableStake ) ;
if ( availableStake > sharesWanted ) {
revert TooMuchSnatch ( receiver , sharesWanted , availableStake , smallestPositionShare ) ;
}
uint256 lastSharesNeeded = sharesWanted - availableStake ;
if ( lastSharesNeeded > lastPos . share * 80 / 100 ) {
_exitPosition ( positionsToSnatch [ index ] , lastPos ) ;
} else {
_shrinkPosition ( positionsToSnatch [ index ] , lastPos , lastSharesNeeded ) ;
}
2024-06-07 11:22:22 +02:00
}
2024-06-13 08:28:42 +02:00
availableStake = authorizedStake ( ) - outstandingStake ;
2024-06-07 11:22:22 +02:00
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
2024-03-18 12:42:30 +01:00
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 ;
2024-03-18 12:42:30 +01:00
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 ;
}
2024-03-18 12:42:30 +01:00
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 ) ;
2024-03-18 12:42:30 +01:00
_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 ) ;
2024-06-09 16:06:41 +02:00
amountDue = assetsBefore * TAX_RATES [ pos . taxRate ] * elapsedTime / ( 365 * 24 * 60 * 60 ) / TAX_RATE_BASE ;
2024-03-12 15:29:59 +01:00
}
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 ) ;
2024-06-09 16:06:41 +02:00
uint256 taxAmountDue = assetsBefore * TAX_RATES [ pos . taxRate ] * elapsedTime / ( 365 * 24 * 60 * 60 ) / TAX_RATE_BASE ;
2024-03-12 15:29:59 +01:00
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-06-13 08:28:42 +02:00
uint256 shareAfterTax = assetsToShares ( assetsBefore - taxAmountDue , Math . Rounding . Down ) ;
outstandingStake -= pos . share - shareAfterTax ;
pos . share = shareAfterTax ;
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
}
}
2024-03-18 12:42:30 +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 ) ;
2024-03-18 12:42:30 +01: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
SafeERC20 . safeTransfer ( tokenContract , owner , assets ) ;
}
2024-06-07 11:22:22 +02:00
function _shrinkPosition ( uint256 positionId , StakingPosition storage pos , uint256 sharesToTake ) private {
2024-06-13 08:28:42 +02:00
require ( sharesToTake < pos . share , " position too small " ) ;
2024-06-07 11:22:22 +02:00
uint256 assets = sharesToAssets ( sharesToTake , Math . Rounding . Down ) ;
2024-06-13 08:28:42 +02:00
pos . share -= sharesToTake ;
2024-06-07 11:22:22 +02:00
emit PositionShrunk ( positionId , pos . share , pos . lastTaxTime , sharesToTake ) ;
SafeERC20 . safeTransfer ( tokenContract , pos . owner , assets ) ;
}
2024-02-21 22:20:04 +01:00
}