From 9279e0c045a2fc541e5474211d8a3c677a2d786c Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Tue, 12 Mar 2024 15:29:59 +0100 Subject: [PATCH] first test pass --- onchain/src/Counter.sol | 14 -------- onchain/src/Harb.sol | 2 ++ onchain/src/Stake.sol | 57 ++++++++++++++++++++------------ onchain/src/interfaces/IHarb.sol | 11 ------ onchain/test/Counter.t.sol | 24 -------------- onchain/test/Harb.t.sol | 53 +++++++++++++++-------------- 6 files changed, 67 insertions(+), 94 deletions(-) delete mode 100644 onchain/src/Counter.sol delete mode 100644 onchain/src/interfaces/IHarb.sol delete mode 100644 onchain/test/Counter.t.sol diff --git a/onchain/src/Counter.sol b/onchain/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/onchain/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/onchain/src/Harb.sol b/onchain/src/Harb.sol index 92ff556..6591175 100644 --- a/onchain/src/Harb.sol +++ b/onchain/src/Harb.sol @@ -17,6 +17,8 @@ import { TwabController } from "pt-v5-twab-controller/TwabController.sol"; */ contract Harb is ERC20, ERC20Permit { + address public constant TAX_POOL = address(2); + /* ============ Public Variables ============ */ /// @notice Address of the TwabController used to keep track of balances. diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index f80c238..b714cc5 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -3,12 +3,16 @@ pragma solidity ^0.8.13; import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol"; +import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import {Math} from "@openzeppelin/utils/math/Math.sol"; import "./interfaces/IStake.sol"; -import "./interfaces/IHarb.sol"; +import "./Harb.sol"; contract Stake is IStake { + using Math for uint256; + uint256 internal DECIMAL_OFFSET = 5 + 2; uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply uint256 internal constant MAX_TAX = 1000; // max 1000% tax per year uint256 internal constant TAX_RATE_BASE = 100; @@ -32,7 +36,7 @@ contract Stake is IStake { } uint256 public immutable totalSupply; - IERC20 private immutable tokenContract; + IERC20Metadata private immutable tokenContract; address private immutable taxPool; uint256 public outstandingStake; uint256 private lastTokenId; @@ -43,26 +47,28 @@ contract Stake is IStake { constructor( address _tokenContract ) { - tokenContract = IERC20(_tokenContract); - IHarb harb = IHarb(_tokenContract); - totalSupply = 100 * 10 ** 5 * harb.decimals(); - taxPool = harb.taxPool(); + tokenContract = IERC20Metadata(_tokenContract); + + totalSupply = 10**(tokenContract.decimals() + DECIMAL_OFFSET); + taxPool = Harb(_tokenContract).TAX_POOL(); } function dormantSupply() public view override returns(uint256) { return totalSupply * (100 - MAX_STAKE) / 100; } - function authorizedStake() private pure returns(uint256) { + function authorizedStake() private view returns(uint256) { return totalSupply * MAX_STAKE / 100; } - function assetsToShares(uint256 assets) private view returns (uint256) { - return assets * totalSupply / tokenContract.totalSupply(); + 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); } - function sharesToAssets(uint256 shares) private view returns (uint256) { - return shares * tokenContract.totalSupply() / totalSupply; + 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); } /** @@ -76,7 +82,7 @@ contract Stake is IStake { function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) public returns(uint256) { // check lower boundary - uint256 sharesWanted = assetsToShares(assets); + uint256 sharesWanted = assetsToShares(assets, Math.Rounding.Down); if (sharesWanted < minStake) { revert SharesTooLow(receiver, assets, sharesWanted, minStake); } @@ -124,7 +130,7 @@ contract Stake is IStake { function exitPosition(uint256 positionID) public { StakingPosition storage pos = positions[positionID]; if(pos.owner != msg.sender) { - NoPermission(msg.sender, pos.owner); + revert NoPermission(msg.sender, pos.owner); } // to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION _payTax(pos, TAX_FLOOR_DURATION); @@ -137,21 +143,30 @@ contract Stake is IStake { _payTax(pos, 0); } + function taxDue(uint256 positionID, uint256 taxFloorDuration) public view returns (uint256 amountDue) { + StakingPosition storage pos = positions[positionID]; + // ihet = Implied Holding Expiry Timestamp + uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp; + 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; + } + function _payTax(StakingPosition storage pos, uint256 taxFloorDuration) private { // ihet = Implied Holding Expiry Timestamp uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp; uint256 elapsedTime = ihet - pos.lastTaxTime; - uint256 assetsBefore = sharesToAssets(pos.share); - uint256 taxDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE; - if (taxDue >= assetsBefore) { + uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down); + uint256 taxAmountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE; + if (taxAmountDue >= assetsBefore) { // can not pay more tax than value of position - taxDue = assetsBefore; + taxAmountDue = assetsBefore; } - SafeERC20.safeTransfer(tokenContract, taxPool, taxDue); - if (assetsBefore - taxDue > 0) { + SafeERC20.safeTransfer(tokenContract, taxPool, taxAmountDue); + if (assetsBefore - taxAmountDue > 0) { // if something left over, update storage - pos.share = assetsToShares(assetsBefore - taxDue); + pos.share = assetsToShares(assetsBefore - taxAmountDue, Math.Rounding.Down); pos.lastTaxTime = uint32(block.timestamp); } else { // if nothing left over, liquidate position @@ -165,7 +180,7 @@ contract Stake is IStake { function _exitPosition(StakingPosition storage pos) private { outstandingStake -= pos.share; address owner = pos.owner; - uint256 assets = sharesToAssets(pos.share); + uint256 assets = sharesToAssets(pos.share, Math.Rounding.Down); delete pos.owner; delete pos.creationTime; SafeERC20.safeTransfer(tokenContract, owner, assets); diff --git a/onchain/src/interfaces/IHarb.sol b/onchain/src/interfaces/IHarb.sol deleted file mode 100644 index 6fea33a..0000000 --- a/onchain/src/interfaces/IHarb.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.13; - -import { IERC20Metadata } from "@openzeppelin/token/ERC20/ERC20.sol"; - -interface IHarb is IERC20Metadata { - - function taxPool() external view returns(address); - -} diff --git a/onchain/test/Counter.t.sol b/onchain/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/onchain/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol index 18ea792..7cc3992 100644 --- a/onchain/test/Harb.t.sol +++ b/onchain/test/Harb.t.sol @@ -7,62 +7,67 @@ import { TwabController } from "pt-v5-twab-controller/TwabController.sol"; import "../src/Harb.sol"; import "../src/Stake.sol"; -contract BloodXTest is Test { +contract HarbTest is Test { Harb public harb; Stake public stake; - uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; function setUp() public { TwabController tc = new TwabController(60*60*24, uint32(block.timestamp)); - harb = new Harb("name", "SYM", tc); + harb = new Harb("HARB", "HARB", tc); stake = new Stake(address(harb)); harb.setStakingPool(address(stake)); } function test_MintStakeUnstake(address account, uint256 amount) public { - vm.assume(amount > 1); - vm.assume(amount < MAX_INT / 100000 ether); + vm.assume(amount > 10000); + vm.assume(amount < 2**93); // TWAB limit = 2**96 vm.assume(account != address(0)); + vm.assume(account != address(1)); // TWAB sponsorship address + vm.assume(account != address(2)); // tax pool address vm.assume(account != address(harb)); vm.assume(account != address(stake)); // test mint uint256 totalSupplyBefore = harb.totalSupply(); uint256 balanceBefore = harb.balanceOf(account); + harb.setLiquidityManager(account); + vm.prank(account); harb.mint(amount); uint256 totalAfter = harb.totalSupply(); assertEq(totalAfter, totalSupplyBefore + amount, "total supply should match"); assertEq(harb.balanceOf(account), balanceBefore + amount, "balance should match"); // test stake - uint256 newStake = amount / 2 * 100000 ether / totalAfter; { - uint256 outstandingBefore = stake.totalSupply(); - (,,,,uint256 stakeBalanceBefore) = stake.positions(1); + vm.prank(account); + harb.mint(amount * 4); + assertEq(stake.outstandingStake(), 0, "init failure"); + vm.prank(account); + harb.approve(address(stake), amount); + uint256[] memory empty; vm.prank(account); - harb.stake(account, amount / 2); - assertEq(harb.totalSupply(), totalSupplyBefore + (amount - (amount / 2)), "total supply should match after stake"); - assertEq(harb.balanceOf(account), balanceBefore + (amount - (amount / 2)), "balance should match after stake"); - assertEq(outstandingBefore + newStake, stake.totalSupply(), "outstanding supply should match"); - assertEq(stakeBalanceBefore + newStake, stake.positions(1).share, "balance of stake account should match"); + stake.snatch(amount, account, 1, empty); + assertEq(harb.totalSupply(), totalAfter * 5, "total supply should match after stake"); + assertEq(harb.balanceOf(account), amount * 4, "balance should match after stake"); + assertEq(harb.balanceOf(address(stake)), amount, "balance should match after stake"); + (uint256 share, address owner, uint32 creationTime, uint32 lastTaxTime, uint32 taxRate) = stake.positions(0); + assertEq(share, stake.totalSupply() / 5, "share should match"); + assertEq(owner, account, "owners should match"); + assertEq(taxRate, 1, "tax rate should match"); } // test unstake { - (uint256 totalBefore, uint256 leftBefore,) = stake.getUnstakeSlot(account); + uint256 timeBefore = block.timestamp; + vm.warp(timeBefore + 60 * 60 * 24 * 4); + uint256 taxDue = stake.taxDue(0, 60 * 60 * 24 * 3); + console.logUint(taxDue); + console.log("tax due :%i", taxDue); vm.prank(account); - stake.unstake(account, newStake / 2); - uint256 timeBefore = block.timestamp; - vm.warp(timeBefore + 60 * 60 * 36); - stake.unstakeTick(account); - (uint256 total, uint256 left, uint256 start) = stake.getUnstakeSlot(account); - assertEq(total, totalBefore + (newStake / 2), "total unstake should match"); - assertApproxEqAbs(left, leftBefore + (newStake / 4), 1); - assertEq(start, timeBefore, "time unstake should match"); - vm.warp(timeBefore + 60 * 60 * 72); - stake.unstakeTick(account); + stake.exitPosition(0); + assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e15, "balance should match"); } }