more staking tests
This commit is contained in:
parent
36833cab7f
commit
54d2c2040a
4 changed files with 258 additions and 11 deletions
|
|
@ -102,6 +102,9 @@ contract Harb is ERC20, ERC20Permit {
|
|||
/// @param _amount Amount of tokens to mint
|
||||
function mint(uint256 _amount) external onlyLiquidityManager {
|
||||
_mint(address(liquidityManager), _amount);
|
||||
if (previousTotalSupply == 0) {
|
||||
previousTotalSupply = twabController.totalSupply(address(this));
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Allows the liquidityManager to burn tokens from a its account
|
||||
|
|
|
|||
|
|
@ -19,13 +19,14 @@ contract Stake {
|
|||
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 internal constant MIN_SUPPLY_FRACTION = 3000;
|
||||
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`.
|
||||
*/
|
||||
|
||||
error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId);
|
||||
error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake);
|
||||
error StakeTooLow(address receiver, uint256 assets, uint256 minStake);
|
||||
error NoPermission(address requester, address owner);
|
||||
error PositionNotFound(uint256 positionId, address requester);
|
||||
|
||||
|
|
@ -107,9 +108,9 @@ contract Stake {
|
|||
{
|
||||
// check that position size is at least minStake
|
||||
// to prevent excessive fragmentation, increasing snatch cost
|
||||
uint256 minStake = harb.previousTotalSupply() / 3000;
|
||||
if (sharesWanted < minStake) {
|
||||
revert SharesTooLow(receiver, assets, sharesWanted, minStake);
|
||||
uint256 minStake = harb.previousTotalSupply() / MIN_SUPPLY_FRACTION;
|
||||
if (assets < minStake) {
|
||||
revert StakeTooLow(receiver, assets, minStake);
|
||||
}
|
||||
}
|
||||
require(taxRate < TAX_RATES.length, "tax rate out of bounds");
|
||||
|
|
@ -126,7 +127,7 @@ contract Stake {
|
|||
}
|
||||
// check that tax lower
|
||||
if (taxRate <= pos.taxRate) {
|
||||
revert TaxTooLow(receiver, taxRate, pos.taxRate, i);
|
||||
revert TaxTooLow(receiver, taxRate, pos.taxRate, positionsToSnatch[i]);
|
||||
}
|
||||
if (pos.share < smallestPositionShare) {
|
||||
smallestPositionShare = pos.share;
|
||||
|
|
@ -149,7 +150,7 @@ contract Stake {
|
|||
}
|
||||
// check that tax lower
|
||||
if (taxRate <= lastPos.taxRate) {
|
||||
revert TaxTooLow(receiver, taxRate, lastPos.taxRate, index);
|
||||
revert TaxTooLow(receiver, taxRate, lastPos.taxRate, positionsToSnatch[index]);
|
||||
}
|
||||
if (lastPos.share < smallestPositionShare) {
|
||||
smallestPositionShare = lastPos.share;
|
||||
|
|
@ -265,6 +266,7 @@ contract Stake {
|
|||
emit PositionRemoved(positionId, pos.share, pos.lastTaxTime);
|
||||
delete pos.owner;
|
||||
delete pos.creationTime;
|
||||
delete pos.share;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,6 +277,7 @@ contract Stake {
|
|||
emit PositionRemoved(positionId, pos.share, pos.lastTaxTime);
|
||||
delete pos.owner;
|
||||
delete pos.creationTime;
|
||||
delete pos.share;
|
||||
SafeERC20.safeTransfer(harb, owner, assets);
|
||||
}
|
||||
|
||||
|
|
@ -282,6 +285,7 @@ contract Stake {
|
|||
require (sharesToTake < pos.share, "position too small");
|
||||
uint256 assets = sharesToAssets(sharesToTake);
|
||||
pos.share -= sharesToTake;
|
||||
outstandingStake -= sharesToTake;
|
||||
emit PositionShrunk(positionId, pos.share, pos.lastTaxTime, sharesToTake);
|
||||
SafeERC20.safeTransfer(harb, pos.owner, assets);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,11 +162,11 @@ contract BaseLineLPTest is PoolSerializer {
|
|||
lm.slide();
|
||||
appendPossitions(lm, pool, token0isWeth);
|
||||
|
||||
// large sell into floor
|
||||
harb.setLiquidityManager(address(account));
|
||||
vm.prank(account);
|
||||
// large sell into floor
|
||||
vm.startPrank(address(lm));
|
||||
harb.mint(3600000 ether);
|
||||
harb.setLiquidityManager(address(lm));
|
||||
harb.transfer(address(account), 3600000 ether);
|
||||
vm.stopPrank();
|
||||
pool.swap(
|
||||
account, // Recipient of the output tokens
|
||||
!token0isWeth,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import "forge-std/Test.sol";
|
|||
import "forge-std/console.sol";
|
||||
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
|
||||
import "../src/Harb.sol";
|
||||
import "../src/Stake.sol";
|
||||
import {TooMuchSnatch, Stake} from "../src/Stake.sol";
|
||||
|
||||
contract StakeTest is Test {
|
||||
TwabController tc;
|
||||
|
|
@ -103,4 +103,244 @@ contract StakeTest is Test {
|
|||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSnatchFunction() public {
|
||||
uint256 initialStake1 = 1 ether;
|
||||
uint256 initialStake2 = 2 ether;
|
||||
address firstStaker = makeAddr("firstStaker");
|
||||
address secondStaker = makeAddr("secondStaker");
|
||||
address newStaker = makeAddr("newStaker");
|
||||
uint256 snatchAmount = 1.5 ether;
|
||||
|
||||
// Mint and distribute tokens
|
||||
vm.startPrank(liquidityManager);
|
||||
harb.mint((initialStake1 + initialStake2) * 5);
|
||||
harb.transfer(firstStaker, initialStake1);
|
||||
harb.transfer(secondStaker, initialStake2);
|
||||
harb.transfer(newStaker, snatchAmount);
|
||||
vm.stopPrank();
|
||||
|
||||
// Setup initial stakers
|
||||
uint256 positionId1 = setupStaker(firstStaker, initialStake1, 1);
|
||||
uint256 positionId2 = setupStaker(secondStaker, initialStake2, 5);
|
||||
|
||||
// Snatch setup
|
||||
vm.startPrank(newStaker);
|
||||
harb.approve(address(stakingPool), snatchAmount);
|
||||
uint256 snatchShares = stakingPool.assetsToShares(snatchAmount);
|
||||
uint256[] memory targetPositions = new uint256[](2);
|
||||
targetPositions[0] = positionId1;
|
||||
targetPositions[1] = positionId2;
|
||||
|
||||
// Perform snatch
|
||||
uint256 newPositionId = stakingPool.snatch(snatchAmount, newStaker, 10, targetPositions);
|
||||
|
||||
// Verify new position
|
||||
assertPosition(newPositionId, snatchShares, 10);
|
||||
|
||||
// Check old positions
|
||||
verifyPositionShrunkOrRemoved(1, initialStake1);
|
||||
verifyPositionShrunkOrRemoved(2, initialStake2);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function setupStaker(address staker, uint256 amount, uint32 taxRate) private returns (uint256 positionId) {
|
||||
vm.startPrank(staker);
|
||||
harb.approve(address(stakingPool), amount);
|
||||
uint256[] memory empty;
|
||||
positionId = stakingPool.snatch(amount, staker, taxRate, empty);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function assertPosition(uint256 positionId, uint256 expectedShares, uint32 expectedTaxRate) private view {
|
||||
(uint256 shares, , , , uint32 taxRate) = stakingPool.positions(positionId);
|
||||
assertEq(shares, expectedShares, "Incorrect share amount for new position");
|
||||
assertEq(taxRate, expectedTaxRate, "Incorrect tax rate for new position");
|
||||
}
|
||||
|
||||
function verifyPositionShrunkOrRemoved(uint256 positionId, uint256 initialStake) private view {
|
||||
(uint256 remainingShare, , , , ) = stakingPool.positions(positionId);
|
||||
uint256 expectedInitialShares = stakingPool.assetsToShares(initialStake);
|
||||
bool positionRemoved = remainingShare == 0;
|
||||
bool positionShrunk = remainingShare < expectedInitialShares;
|
||||
|
||||
assertTrue(positionRemoved || positionShrunk, "Position was not correctly shrunk or removed");
|
||||
}
|
||||
|
||||
function testRevert_SharesTooLow() public {
|
||||
address staker = makeAddr("staker");
|
||||
vm.startPrank(liquidityManager);
|
||||
harb.mint(10 ether);
|
||||
uint256 tooSmallStake = harb.previousTotalSupply() / 4000; // Less than minStake calculation
|
||||
harb.transfer(staker, tooSmallStake);
|
||||
vm.stopPrank();
|
||||
|
||||
vm.startPrank(staker);
|
||||
harb.approve(address(stakingPool), tooSmallStake);
|
||||
|
||||
uint256[] memory empty;
|
||||
vm.expectRevert(abi.encodeWithSelector(Stake.StakeTooLow.selector, staker, tooSmallStake, harb.previousTotalSupply() / 3000));
|
||||
stakingPool.snatch(tooSmallStake, staker, 1, empty);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testRevert_TaxTooLow() public {
|
||||
address existingStaker = makeAddr("existingStaker");
|
||||
address newStaker = makeAddr("newStaker");
|
||||
vm.startPrank(liquidityManager);
|
||||
harb.mint(10 ether);
|
||||
harb.transfer(existingStaker, 1 ether);
|
||||
harb.transfer(newStaker, 1 ether);
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 positionId = setupStaker(existingStaker, 1 ether, 5); // Existing staker with tax rate 5
|
||||
|
||||
vm.startPrank(newStaker);
|
||||
harb.transfer(newStaker, 1 ether);
|
||||
harb.approve(address(stakingPool), 1 ether);
|
||||
|
||||
uint256[] memory positions = new uint256[](1);
|
||||
positions[0] = positionId; // Assuming position ID 1 has tax rate 5
|
||||
|
||||
vm.expectRevert(abi.encodeWithSelector(Stake.TaxTooLow.selector, newStaker, 5, 5, positionId));
|
||||
stakingPool.snatch(1 ether, newStaker, 5, positions); // Same tax rate should fail
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testRevert_TooMuchSnatch() public {
|
||||
address staker = makeAddr("staker");
|
||||
address ambitiousStaker = makeAddr("ambitiousStaker");
|
||||
|
||||
vm.startPrank(liquidityManager);
|
||||
harb.mint(20 ether);
|
||||
harb.transfer(staker, 2 ether);
|
||||
harb.transfer(ambitiousStaker, 1 ether);
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 positionId = setupStaker(staker, 2 ether, 10);
|
||||
|
||||
vm.startPrank(ambitiousStaker);
|
||||
harb.approve(address(stakingPool), 1 ether);
|
||||
|
||||
uint256[] memory positions = new uint256[](1);
|
||||
positions[0] = positionId;
|
||||
vm.expectRevert(abi.encodeWithSelector(TooMuchSnatch.selector, ambitiousStaker, 500000 ether, 1000000 ether, 1000000 ether));
|
||||
stakingPool.snatch(1 ether, ambitiousStaker, 20, positions);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testRevert_PositionNotFound() public {
|
||||
address staker = makeAddr("staker");
|
||||
|
||||
vm.startPrank(liquidityManager);
|
||||
harb.mint(10 ether);
|
||||
harb.transfer(staker, 1 ether);
|
||||
vm.stopPrank();
|
||||
|
||||
vm.startPrank(staker);
|
||||
harb.approve(address(stakingPool), 1 ether);
|
||||
|
||||
uint256[] memory nonExistentPositions = new uint256[](1);
|
||||
nonExistentPositions[0] = 999; // Assumed non-existent position ID
|
||||
|
||||
vm.expectRevert(abi.encodeWithSelector(Stake.PositionNotFound.selector, 999, staker));
|
||||
stakingPool.snatch(1 ether, staker, 15, nonExistentPositions);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSuccessfulChangeTaxAndReverts() public {
|
||||
uint32 newTaxRate = 3; // New valid tax rate
|
||||
|
||||
address staker = makeAddr("staker");
|
||||
|
||||
vm.startPrank(liquidityManager);
|
||||
harb.mint(10 ether);
|
||||
harb.transfer(staker, 1 ether);
|
||||
vm.stopPrank();
|
||||
|
||||
vm.startPrank(staker);
|
||||
harb.approve(address(stakingPool), 1 ether);
|
||||
uint256[] memory empty;
|
||||
uint256 positionId = stakingPool.snatch(1 ether, staker, 1, empty);
|
||||
|
||||
uint32 invalidTaxRate = 9999; // Assuming this is out of bounds
|
||||
vm.expectRevert(bytes("tax rate out of bounds"));
|
||||
stakingPool.changeTax(positionId, invalidTaxRate);
|
||||
|
||||
stakingPool.changeTax(positionId, newTaxRate);
|
||||
vm.stopPrank();
|
||||
|
||||
// Verify the change
|
||||
(, , , , uint32 taxRate) = stakingPool.positions(positionId);
|
||||
assertEq(taxRate, newTaxRate, "Tax rate did not update correctly");
|
||||
|
||||
// notOwner tries to change tax rate
|
||||
address notOwner = makeAddr("notOwner");
|
||||
vm.prank(notOwner);
|
||||
vm.expectRevert(abi.encodeWithSelector(Stake.NoPermission.selector, notOwner, staker));
|
||||
stakingPool.changeTax(positionId, newTaxRate * 2);
|
||||
}
|
||||
|
||||
function testNormalTaxPayment() public {
|
||||
address staker = makeAddr("staker");
|
||||
|
||||
vm.startPrank(liquidityManager);
|
||||
harb.mint(10 ether);
|
||||
harb.transfer(staker, 1 ether);
|
||||
vm.stopPrank();
|
||||
|
||||
vm.startPrank(staker);
|
||||
harb.approve(address(stakingPool), 1 ether);
|
||||
uint256[] memory empty;
|
||||
uint256 positionId = stakingPool.snatch(1 ether, staker, 5, empty); // Using tax rate index 5, which is 18% per year
|
||||
(uint256 shareBefore, , , , ) = stakingPool.positions(positionId);
|
||||
|
||||
|
||||
// Immediately after staking, no tax due
|
||||
stakingPool.payTax(positionId);
|
||||
// Verify no change in position
|
||||
(uint256 share, , , , ) = stakingPool.positions(positionId);
|
||||
assertEq(share, shareBefore, "Share should not change when no tax is due");
|
||||
|
||||
// Move time forward 30 days
|
||||
vm.warp(block.timestamp + 30 days);
|
||||
stakingPool.payTax(positionId);
|
||||
vm.stopPrank();
|
||||
|
||||
// Check that the tax was paid and position updated
|
||||
(share, , , , ) = stakingPool.positions(positionId);
|
||||
uint256 daysElapsed = 30;
|
||||
uint256 taxRate = 18; // Corresponding to 18% annually
|
||||
uint256 daysInYear = 365;
|
||||
uint256 taxBase = 100;
|
||||
|
||||
uint256 taxFractionForTime = taxRate * daysElapsed * 1 ether / daysInYear / taxBase;
|
||||
uint256 expectedShareAfterTax = (1 ether - taxFractionForTime) * 1000000;
|
||||
|
||||
assertTrue(share < shareBefore, "Share should decrease correctly after tax payment 1");
|
||||
assertEq(share, expectedShareAfterTax, "Share should decrease correctly after tax payment 2");
|
||||
}
|
||||
|
||||
function testLiquidation() public {
|
||||
address staker = makeAddr("staker");
|
||||
|
||||
vm.startPrank(liquidityManager);
|
||||
harb.mint(10 ether);
|
||||
harb.transfer(staker, 1 ether);
|
||||
vm.stopPrank();
|
||||
|
||||
vm.startPrank(staker);
|
||||
harb.approve(address(stakingPool), 1 ether);
|
||||
uint256[] memory empty;
|
||||
uint256 positionId = stakingPool.snatch(1 ether, staker, 12, empty); // Using tax rate index 5, which is 100% per year
|
||||
vm.warp(block.timestamp + 365 days); // Move time forward to ensure maximum tax due
|
||||
stakingPool.payTax(positionId);
|
||||
vm.stopPrank();
|
||||
|
||||
// Verify position is liquidated
|
||||
(uint256 share, , , , ) = stakingPool.positions(positionId);
|
||||
assertEq(share, 0, "Share should be zero after liquidation");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue