From b94f5559dd01d3dcf0224fd73ce8d5572a8c44d4 Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Thu, 13 Jun 2024 10:50:09 +0200 Subject: [PATCH] implemented minStake --- onchain/src/BaseLineLP.sol | 2 ++ onchain/src/Harb.sol | 6 ++++ onchain/src/Stake.sol | 56 ++++++++++++++++++-------------------- onchain/test/Harb.t.sol | 6 ++-- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/onchain/src/BaseLineLP.sol b/onchain/src/BaseLineLP.sol index a5715fc..ee93497 100644 --- a/onchain/src/BaseLineLP.sol +++ b/onchain/src/BaseLineLP.sol @@ -386,6 +386,7 @@ contract BaseLineLP { } // call this function when price has moved up 15% + // TODO: write a bot that calls this function regularly function shift() external { require(positions[Stage.ANCHOR].liquidity > 0, "Not initialized"); // Fetch the current tick from the Uniswap V3 pool @@ -425,6 +426,7 @@ contract BaseLineLP { ethInAnchor = (ethInAnchor > ethBalance / 10) ? ethBalance / 10 : ethInAnchor; currentTick = currentTick / TICK_SPACING * TICK_SPACING; + harb.setPreviousTotalSupply(harb.totalSupply()); _set(sqrtPriceX96, currentTick, ethInAnchor); } diff --git a/onchain/src/Harb.sol b/onchain/src/Harb.sol index a6f40b3..5acda30 100644 --- a/onchain/src/Harb.sol +++ b/onchain/src/Harb.sol @@ -39,6 +39,8 @@ contract Harb is ERC20, ERC20Permit { uint256 public sumTaxCollected; + uint256 public previousTotalSupply; + struct UbiTitle { uint256 sumTaxCollected; uint256 time; @@ -107,6 +109,10 @@ contract Harb is ERC20, ERC20Permit { _burn(address(liquidityManager), _amount); } + function setPreviousTotalSupply(uint256 _ts) external onlyLiquidityManager { + previousTotalSupply = _ts; + } + /* ============ Public ERC20 Overrides ============ */ /// @inheritdoc ERC20 diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index e422db0..97929ad 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -20,7 +20,7 @@ contract Stake is IStake { uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply uint256 internal constant TAX_RATE_BASE = 100; uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time. - 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 + 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, 2000, 2600, 3400, 4400, 5700, 7500, 9700]; /** * @dev Attempted to deposit more assets than the max amount for `receiver`. */ @@ -44,18 +44,17 @@ contract Stake is IStake { } uint256 public immutable totalSupply; - IERC20Metadata private immutable tokenContract; + Harb private immutable harb; address private immutable taxPool; uint256 public outstandingStake; uint256 public nextPositionId; - uint256 public minStake; // TODO: handle this mapping(uint256 => StakingPosition) public positions; - constructor(address _tokenContract) { - tokenContract = IERC20Metadata(_tokenContract); + constructor(address _harb) { + harb = Harb(_harb); - totalSupply = 10 ** (tokenContract.decimals() + DECIMAL_OFFSET); - taxPool = Harb(_tokenContract).TAX_POOL(); + totalSupply = 10 ** (harb.decimals() + DECIMAL_OFFSET); + taxPool = Harb(_harb).TAX_POOL(); nextPositionId = 654321; } @@ -68,13 +67,14 @@ contract Stake is IStake { } 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); + return assets.mulDiv(totalSupply, harb.totalSupply(), rounding); + //return assets.mulDiv(totalSupply, harb.totalSupply() + 1, rounding); } 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); + //return shares.mulDiv(harb.totalSupply() + 1, totalSupply, rounding); + // TODO: should the average total supply be used for this calculation? + return shares.mulDiv(harb.totalSupply(), totalSupply, rounding); } function permitAndSnatch( @@ -92,12 +92,10 @@ contract Stake is IStake { ) external returns (uint256 positionId) { - ERC20Permit(address(tokenContract)).permit(receiver, address(this), assets, deadline, v, r, s); + ERC20Permit(address(harb)).permit(receiver, address(this), assets, deadline, v, r, s); return snatch(assets, receiver, taxRate, positionsToSnatch); } - event DEBUG(uint256 line); - /** * TODO: deal with metatransactions: While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct @@ -111,19 +109,19 @@ contract Stake is IStake { { // check lower boundary uint256 sharesWanted = assetsToShares(assets, Math.Rounding.Down); - if (sharesWanted < minStake) { - revert SharesTooLow(receiver, assets, sharesWanted, minStake); + { + // check that position size is multiple of minStake + // to prevent excessive fragmentation, increasing snatch cost + uint256 minStake = harb.previousTotalSupply() / 3000; + if (sharesWanted < minStake) { + revert SharesTooLow(receiver, assets, sharesWanted, minStake); + } } - emit DEBUG(authorizedStake()); - // TODO: check that position size is multiple of minStake - // TODO: check that tax rate within limits of array + require(taxRate < TAX_RATES.length, "tax rate out of bounds"); uint256 smallestPositionShare = totalSupply; - emit DEBUG(outstandingStake); uint256 availableStake = authorizedStake() - outstandingStake; - emit DEBUG(2); - if (positionsToSnatch.length >= 2) { // run through all but last positions to snatch for (uint256 i = 0; i < positionsToSnatch.length - 1; i++) { @@ -164,8 +162,6 @@ contract Stake is IStake { } // dissolve position _payTax(positionsToSnatch[index], lastPos, 0); - emit DEBUG(sharesWanted); - emit DEBUG(availableStake); if (availableStake > sharesWanted) { revert TooMuchSnatch(receiver, sharesWanted, availableStake, smallestPositionShare); } @@ -188,7 +184,7 @@ contract Stake is IStake { } // transfer - SafeERC20.safeTransferFrom(tokenContract, msg.sender, address(this), assets); + SafeERC20.safeTransferFrom(harb, msg.sender, address(this), assets); // mint positionId = nextPositionId++; @@ -204,12 +200,13 @@ contract Stake is IStake { } function changeTax(uint256 positionID, uint32 taxRate) public { + require(taxRate < TAX_RATES.length, "tax rate out of bounds"); 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); + require(taxRate > pos.taxRate, "tax too low to snatch"); _payTax(positionID, pos, TAX_FLOOR_DURATION); pos.taxRate = taxRate; } @@ -224,6 +221,7 @@ contract Stake is IStake { _exitPosition(positionId, pos); } + // TODO: write a bot that calls this function regularly function payTax(uint256 positionID) public { StakingPosition storage pos = positions[positionID]; // TODO: what if someone calls payTax and exitPosition in the same transaction? @@ -253,7 +251,7 @@ contract Stake is IStake { // can not pay more tax than value of position taxAmountDue = assetsBefore; } - SafeERC20.safeTransfer(tokenContract, taxPool, taxAmountDue); + SafeERC20.safeTransfer(harb, taxPool, taxAmountDue); emit TaxPaid(positionID, pos.owner, taxAmountDue); if (assetsBefore - taxAmountDue > 0) { // if something left over, update storage @@ -277,7 +275,7 @@ contract Stake is IStake { emit PositionRemoved(positionId, pos.share, pos.lastTaxTime); delete pos.owner; delete pos.creationTime; - SafeERC20.safeTransfer(tokenContract, owner, assets); + SafeERC20.safeTransfer(harb, owner, assets); } function _shrinkPosition(uint256 positionId, StakingPosition storage pos, uint256 sharesToTake) private { @@ -285,6 +283,6 @@ contract Stake is IStake { uint256 assets = sharesToAssets(sharesToTake, Math.Rounding.Down); pos.share -= sharesToTake; emit PositionShrunk(positionId, pos.share, pos.lastTaxTime, sharesToTake); - SafeERC20.safeTransfer(tokenContract, pos.owner, assets); + SafeERC20.safeTransfer(harb, pos.owner, assets); } } diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol index 6234ec8..15975e3 100644 --- a/onchain/test/Harb.t.sol +++ b/onchain/test/Harb.t.sol @@ -99,7 +99,7 @@ contract HarbTest is Test { assertEq(harb.balanceOf(account), amount * 4, "balance should match after stake"); assertEq(harb.balanceOf(address(stake)), amount, "balance should match after stake"); // check stake position - (uint256 share, address owner, uint32 creationTime, uint32 lastTaxTime, uint32 taxRate) = stake.positions(0); + (uint256 share, address owner, uint32 creationTime, uint32 lastTaxTime, uint32 taxRate) = stake.positions(654321); assertEq(share, stake.totalSupply() / 5, "share should match"); assertEq(owner, account, "owners should match"); assertEq(creationTime, block.timestamp, "time should match"); @@ -122,11 +122,11 @@ contract HarbTest is Test { // advance the time uint256 timeBefore = block.timestamp; vm.warp(timeBefore + (60 * 60 * 24 * 4)); - uint256 taxDue = stake.taxDue(0, 60 * 60 * 24 * 3); + uint256 taxDue = stake.taxDue(654321, 60 * 60 * 24 * 3); uint256 sumTaxCollectedBefore = harb.sumTaxCollected(); vm.prank(account); - stake.exitPosition(0); + stake.exitPosition(654321); assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e14, "account balance should match"); assertEq(harb.balanceOf(TAX_POOL), taxDue, "tax pool balance should match"); assertEq(sumTaxCollectedBefore + taxDue, harb.sumTaxCollected(), "collected tax should have increased");