first test pass

This commit is contained in:
JulesCrown 2024-03-12 15:29:59 +01:00
parent 307f98840b
commit 9279e0c045
6 changed files with 67 additions and 94 deletions

View file

@ -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++;
}
}

View file

@ -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.

View file

@ -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);

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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.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");
harb.mint(amount * 4);
assertEq(stake.outstandingStake(), 0, "init failure");
vm.prank(account);
harb.approve(address(stake), amount);
uint256[] memory empty;
vm.prank(account);
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);
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);
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.exitPosition(0);
assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e15, "balance should match");
}
}