first test pass
This commit is contained in:
parent
307f98840b
commit
9279e0c045
6 changed files with 67 additions and 94 deletions
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue