Files
fluxswap/test/StakingGauge.t.sol

210 lines
6.8 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;
import {Test} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {Vm} from "forge-std/Vm.sol";
import {StakingGauge} from "../src/StakingGauge.sol";
import {FluxToken} from "../src/FluxToken.sol";
import {TestErc20} from "../src/TestErc20.sol";
contract StakingGaugeTest is Test {
StakingGauge private gauge;
TestErc20 private stakingToken;
FluxToken private rewardToken;
address private alice = address(0xA11CE);
address private bob = address(0xB0B);
uint256 private constant funded = 7000e18;
function setUp() public {
stakingToken = new TestErc20("Liquidity", "LP", 0);
rewardToken = new FluxToken(address(this));
gauge = new StakingGauge(address(stakingToken), address(rewardToken), address(this));
rewardToken.mint(address(gauge), funded);
gauge.notifyRewardAmount(funded);
stakingToken.mint(alice, 1e24);
stakingToken.mint(bob, 1e24);
vm.prank(alice);
stakingToken.approve(address(gauge), type(uint256).max);
vm.prank(bob);
stakingToken.approve(address(gauge), type(uint256).max);
}
function testSingleStakerEarnsWholeReward() public {
vm.prank(alice);
gauge.stake(100e18);
vm.warp(block.timestamp + 7 days);
uint256 earned = gauge.earned(alice);
assertApproxEqRel(earned, funded, 1e15);
vm.prank(alice);
gauge.getReward();
assertApproxEqRel(rewardToken.balanceOf(alice), funded, 1e15);
}
function testTwoStakersSplitProportionally() public {
vm.prank(alice);
gauge.stake(100e18);
vm.prank(bob);
gauge.stake(300e18);
vm.warp(block.timestamp + 7 days);
uint256 aliceEarned = gauge.earned(alice);
uint256 bobEarned = gauge.earned(bob);
assertApproxEqRel(aliceEarned, funded / 4, 1e15);
assertApproxEqRel(bobEarned, (funded * 3) / 4, 1e15);
assertApproxEqRel(aliceEarned + bobEarned, funded, 1e15);
}
function testStakeTransfersIn() public {
vm.prank(alice);
gauge.stake(100e18);
assertEq(stakingToken.balanceOf(address(gauge)), 100e18);
assertEq(gauge.totalSupply(), 100e18);
assertEq(gauge.balanceOf(alice), 100e18);
}
function testWithdrawReturnsStake() public {
vm.prank(alice);
gauge.stake(100e18);
vm.warp(block.timestamp + 1 days);
vm.prank(alice);
gauge.withdraw(40e18);
assertEq(gauge.balanceOf(alice), 60e18);
assertEq(stakingToken.balanceOf(alice), 1e24 - 60e18);
}
function testExitWithdrawsAndClaims() public {
vm.prank(alice);
gauge.stake(100e18);
vm.warp(block.timestamp + 7 days);
vm.prank(alice);
gauge.exit();
assertEq(gauge.balanceOf(alice), 0);
assertEq(stakingToken.balanceOf(alice), 1e24);
assertApproxEqRel(rewardToken.balanceOf(alice), funded, 1e15);
}
function testLateStakerEarnsLess() public {
vm.prank(alice);
gauge.stake(100e18);
vm.warp(block.timestamp + 3 days + 12 hours);
vm.prank(bob);
gauge.stake(100e18);
vm.warp(block.timestamp + 3 days + 12 hours);
assertGt(gauge.earned(alice), gauge.earned(bob));
}
function testNotifyRevertsWhenUnderfunded() public {
vm.warp(block.timestamp + 8 days);
vm.expectRevert(bytes("StakingGauge: REWARD_TOO_HIGH"));
gauge.notifyRewardAmount(1e30);
}
function testOnlyOwnerNotifies() public {
vm.prank(alice);
vm.expectRevert(bytes("StakingGauge: FORBIDDEN"));
gauge.notifyRewardAmount(1e18);
}
function testFuzzEarnedNeverExceedsFunded(uint96 stakeAmount, uint32 elapsed) public {
uint256 amount = bound(uint256(stakeAmount), 1e6, 1e24);
uint256 dt = bound(uint256(elapsed), 0, 14 days);
vm.prank(alice);
gauge.stake(amount);
vm.warp(block.timestamp + dt);
assertLe(gauge.earned(alice), funded);
}
}
contract StakingHandler {
Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
StakingGauge public gauge;
TestErc20 public stakingToken;
FluxToken public rewardToken;
uint256 public paidOut;
constructor(StakingGauge gaugeAddress, TestErc20 staking, FluxToken reward) {
gauge = gaugeAddress;
stakingToken = staking;
rewardToken = reward;
stakingToken.mint(address(this), 1e30);
stakingToken.approve(address(gauge), type(uint256).max);
}
function bound(uint256 x, uint256 min, uint256 max) private pure returns (uint256) {
if (max <= min) return min;
return min + (x % (max - min + 1));
}
function doStake(uint256 amountSeed) external {
uint256 amount = bound(amountSeed, 1, 1e24);
gauge.stake(amount);
}
function doWithdraw(uint256 amountSeed) external {
uint256 have = gauge.balanceOf(address(this));
if (have == 0) return;
gauge.withdraw(bound(amountSeed, 1, have));
}
function doClaim() external {
uint256 before = rewardToken.balanceOf(address(this));
gauge.getReward();
paidOut += rewardToken.balanceOf(address(this)) - before;
}
function doWarp(uint32 dtSeed) external {
vm.warp(block.timestamp + bound(uint256(dtSeed), 1, 2 days));
}
}
contract StakingGaugeInvariantTest is StdInvariant, Test {
StakingGauge private gauge;
TestErc20 private stakingToken;
FluxToken private rewardToken;
StakingHandler private handler;
uint256 private constant funded = 7000e18;
function setUp() public {
stakingToken = new TestErc20("Liquidity", "LP", 0);
rewardToken = new FluxToken(address(this));
gauge = new StakingGauge(address(stakingToken), address(rewardToken), address(this));
rewardToken.mint(address(gauge), funded);
gauge.notifyRewardAmount(funded);
handler = new StakingHandler(gauge, stakingToken, rewardToken);
bytes4[] memory selectors = new bytes4[](4);
selectors[0] = StakingHandler.doStake.selector;
selectors[1] = StakingHandler.doWithdraw.selector;
selectors[2] = StakingHandler.doClaim.selector;
selectors[3] = StakingHandler.doWarp.selector;
targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
targetContract(address(handler));
}
function invariantRewardsNeverExceedFunding() public view {
assertLe(handler.paidOut(), funded);
assertEq(rewardToken.balanceOf(address(gauge)) + handler.paidOut(), funded);
}
function invariantStakingTokenFullyBacked() public view {
assertEq(stakingToken.balanceOf(address(gauge)), gauge.totalSupply());
}
}