diff --git a/.gitmodules b/.gitmodules index 1e20c4a5..fcce7e3b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,9 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/brockelmore/forge-std +[submodule "lib/prb-math"] + path = lib/prb-math + url = https://github.com/paulrberg/prb-math diff --git a/README.md b/README.md index 23b23e62..9c98caa7 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ brownie pm install OpenZeppelin/openzeppelin-contracts@4.6.0 yarn install ``` -## Running contract tests +## Running contract tests (brownie) ```bash cd contracts @@ -42,6 +42,15 @@ brownie test --network hardhat _If this command reverts with an error it may be an incompatability with python 3.10. Try python 3.9 instead ([pyenv](https://github.com/pyenv/pyenv) is a good solution for managing multiple python versions)._ +## Running contract tests (forge) + +The OGV staking contracts use forge for tests. + +```bash +forge install +forge test +``` + ## Running a local node Copy `dev.env` to `.env` and fill out the `PROVIDER_URL` diff --git a/contracts/OgvStaking.sol b/contracts/OgvStaking.sol index dc0135e2..894a3499 100644 --- a/contracts/OgvStaking.sol +++ b/contracts/OgvStaking.sol @@ -143,7 +143,7 @@ contract OgvStaking is ERC20Votes { oldAmount, duration ); - require(newEnd > oldEnd, "New lockup must be longer"); + require(newEnd > oldEnd, "Staking: New lockup must be longer"); lockup.end = uint128(newEnd); lockup.points = newPoints; lockups[msg.sender][lockupId] = lockup; diff --git a/contracts/tests/MockOGV.sol b/contracts/tests/MockOGV.sol new file mode 100644 index 00000000..d8dbfb50 --- /dev/null +++ b/contracts/tests/MockOGV.sol @@ -0,0 +1,12 @@ +import { ERC20 } from "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/token/ERC20/ERC20.sol"; + +contract MockOgv is ERC20 { + + constructor() ERC20("OGV","OGV") { + + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 87b13b8d..678123e9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,10 @@ [default] src = 'contracts' +test = 'tests' remappings = [ "OpenZeppelin/openzeppelin-contracts@02fcc75bb7f35376c22def91b0fb9bc7a50b9458/=./lib/openzeppelin-contracts", - "OpenZeppelin/openzeppelin-contracts-upgradeable@a16f26a063cd018c4c986832c3df332a131f53b9/=./lib/openzeppelin-contracts-upgradeable" + "OpenZeppelin/openzeppelin-contracts-upgradeable@a16f26a063cd018c4c986832c3df332a131f53b9/=./lib/openzeppelin-contracts-upgradeable", + "OpenZeppelin/openzeppelin-contracts@4.6.0/=./lib/openzeppelin-contracts", + "OpenZeppelin/openzeppelin-contracts-upgradeable@4.6.0/=./lib/openzeppelin-contracts-upgradeable", + "paulrberg/prb-math@2.5.0/=./lib/prb-math" ] diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 00000000..56451005 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 564510058ab3db01577b772c275e081e678373f2 diff --git a/lib/prb-math b/lib/prb-math new file mode 160000 index 00000000..701b1bad --- /dev/null +++ b/lib/prb-math @@ -0,0 +1 @@ +Subproject commit 701b1badb9a0951f27e344602726ead71f138b1a diff --git a/tests/staking/DelegationTest.t.sol b/tests/staking/DelegationTest.t.sol new file mode 100644 index 00000000..50771803 --- /dev/null +++ b/tests/staking/DelegationTest.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/OgvStaking.sol"; +import "../../contracts/RewardsSource.sol"; +import "../../contracts/tests/MockOgv.sol"; + +// +// Sanity test of OpenZeppelin's voting and deletegation. +// + +contract DelegationTest is Test { + MockOgv ogv; + OgvStaking staking; + RewardsSource source; + + address oak = address(0x42); + address aspen = address(0x43); + address taz = address(0x44); + address team = address(0x50); + + uint256 constant EPOCH = 1 days; + + uint256 POINTS = 0; + + function setUp() public { + vm.startPrank(team); + ogv = new MockOgv(); + source = new RewardsSource(address(ogv)); + staking = new OgvStaking(address(ogv), EPOCH, address(source)); + source.setRewardsTarget(address(staking)); + vm.stopPrank(); + + ogv.mint(oak, 1000 ether); + ogv.mint(aspen, 1000 ether); + ogv.mint(taz, 100000000 ether); + + vm.prank(oak); + ogv.approve(address(staking), 1e70); + vm.prank(aspen); + ogv.approve(address(staking), 1e70); + vm.prank(taz); + ogv.approve(address(staking), 1e70); + + vm.prank(oak); + staking.stake(1 ether, 100 days); + vm.prank(aspen); + staking.stake(2 ether, 100 days); + + POINTS = staking.balanceOf(oak); + } + + function testSelfDelegate() external { + vm.roll(1); + assertEq(staking.getVotes(oak), 0, "zero until delegated"); + assertEq(staking.getPastVotes(oak, block.number - 1), 0); + assertEq(staking.delegates(oak), address(0)); + + vm.roll(2); + vm.prank(oak); + staking.delegate(oak); + + assertEq( + staking.getVotes(oak), + 1 * POINTS, + "can vote after delegation" + ); + assertEq(staking.getPastVotes(oak, block.number - 1), 0); + assertEq(staking.delegates(oak), oak, "self is delegate"); + + vm.roll(3); + vm.prank(oak); + staking.delegate(address(0)); + + assertEq(staking.getVotes(oak), 0, "zero after delegation removed"); + assertEq(staking.getPastVotes(oak, block.number - 1), 1 * POINTS); + assertEq(staking.delegates(oak), address(0)); + } + + function testDelegate() external { + vm.roll(1); + assertEq(staking.getVotes(oak), 0, "zero until delegated"); + assertEq(staking.getPastVotes(oak, block.number - 1), 0); + assertEq(staking.delegates(oak), address(0)); + + vm.roll(2); + vm.prank(oak); + staking.delegate(aspen); + + assertEq( + staking.getVotes(aspen), + 1 * POINTS, + "can vote after delegation" + ); + assertEq(staking.getPastVotes(aspen, block.number - 1), 0); + assertEq(staking.delegates(oak), aspen, "delegated"); + + vm.roll(3); + vm.prank(aspen); + staking.delegate(aspen); // Self delegate + + assertEq(staking.getVotes(aspen), 3 * POINTS, "two users points"); + assertEq(staking.getPastVotes(aspen, block.number - 1), 1 * POINTS); + assertEq(staking.delegates(aspen), aspen); + } +} diff --git a/tests/staking/OgvStaking.t.sol b/tests/staking/OgvStaking.t.sol new file mode 100644 index 00000000..b39ce066 --- /dev/null +++ b/tests/staking/OgvStaking.t.sol @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/OgvStaking.sol"; +import "../../contracts/RewardsSource.sol"; +import "../../contracts/tests/MockOgv.sol"; + +contract OgvStakingTest is Test { + MockOgv ogv; + OgvStaking staking; + RewardsSource source; + + address alice = address(0x42); + address bob = address(0x43); + address team = address(0x44); + + uint256 constant EPOCH = 1 days; + + function setUp() public { + vm.startPrank(team); + ogv = new MockOgv(); + source = new RewardsSource(address(ogv)); + staking = new OgvStaking(address(ogv), EPOCH, address(source)); + source.setRewardsTarget(address(staking)); + vm.stopPrank(); + + ogv.mint(alice, 1000 ether); + ogv.mint(bob, 1000 ether); + ogv.mint(team, 100000000 ether); + + vm.prank(alice); + ogv.approve(address(staking), 1e70); + vm.prank(bob); + ogv.approve(address(staking), 1e70); + vm.prank(team); + ogv.approve(address(source), 1e70); + } + + function testStakeUnstake() public { + vm.startPrank(alice); + (uint256 previewPoints, uint256 previewEnd) = staking.previewPoints( + 10 ether, + 10 days + ); + + uint256 beforeOgv = ogv.balanceOf(alice); + uint256 beforeOgvStaking = ogv.balanceOf(address(staking)); + + staking.stake(10 ether, 10 days); + + assertEq(ogv.balanceOf(alice), beforeOgv - 10 ether); + assertEq(ogv.balanceOf(address(staking)), beforeOgvStaking + 10 ether); + assertEq(staking.balanceOf(alice), previewPoints); + ( + uint128 lockupAmount, + uint128 lockupEnd, + uint256 lockupPoints + ) = staking.lockups(alice, 0); + assertEq(lockupAmount, 10 ether); + assertEq(lockupEnd, EPOCH + 10 days); + assertEq(lockupEnd, previewEnd); + assertEq(lockupPoints, previewPoints); + assertEq( + staking.accRewardPerShare(), + staking.rewardDebtPerShare(alice) + ); + + vm.warp(31 days); + staking.unstake(0); + + assertEq(ogv.balanceOf(alice), beforeOgv); + assertEq(ogv.balanceOf(address(staking)), 0); + (lockupAmount, lockupEnd, lockupPoints) = staking.lockups(alice, 0); + assertEq(lockupAmount, 0); + assertEq(lockupEnd, 0); + assertEq(lockupPoints, 0); + assertEq( + staking.accRewardPerShare(), + staking.rewardDebtPerShare(alice) + ); + } + + function testMatchedDurations() public { + vm.prank(alice); + staking.stake(10 ether, 1000 days, alice); + + vm.warp(EPOCH + 900 days); + vm.prank(bob); + staking.stake(10 ether, 100 days, bob); + + // Now both have 10 OGV staked for 100 days remaining + // which should mean that they have the same number of points + assertEq(staking.balanceOf(alice), staking.balanceOf(bob)); + } + + function testPreStaking() public { + vm.prank(alice); + staking.stake(100 ether, 100 days, alice); + + vm.warp(EPOCH); + vm.prank(bob); + staking.stake(100 ether, 100 days, bob); + + // Both should have the same points + assertEq(staking.balanceOf(alice), staking.balanceOf(bob)); + } + + function testZeroStake() public { + vm.prank(alice); + vm.expectRevert("Staking: Not enough"); + staking.stake(0 ether, 100 days, alice); + } + + function testStakeTooMuch() public { + vm.prank(alice); + vm.expectRevert("Staking: Too much"); + staking.stake(1e70, 100 days, alice); + } + + function testStakeTooLong() public { + vm.prank(alice); + vm.expectRevert("Staking: Too long"); + staking.stake(1 ether, 1700 days, alice); + } + + function testStakeTooShort() public { + vm.prank(alice); + vm.expectRevert("Staking: Too short"); + staking.stake(1 ether, 1 days, alice); + } + + function testExtend() public { + vm.prank(alice); + staking.stake(100 ether, 100 days, alice); + + vm.startPrank(bob); + staking.stake(100 ether, 10 days, bob); + staking.extend(0, 100 days); + + // Both are now locked up for the same amount of time, + // and should have the same points. + assertEq(staking.balanceOf(alice), staking.balanceOf(bob)); + + (uint128 aliceAmount, uint128 aliceEnd, uint256 alicePoints) = staking + .lockups(alice, 0); + (uint128 bobAmount, uint128 bobEnd, uint256 bobPoints) = staking + .lockups(bob, 0); + assertEq(aliceAmount, bobAmount, "same amount"); + assertEq(aliceEnd, bobEnd, "same end"); + assertEq(alicePoints, bobPoints, "same points"); + assertEq(staking.accRewardPerShare(), staking.rewardDebtPerShare(bob)); + } + + function testDoubleExtend() public { + vm.warp(EPOCH + 600 days); + + vm.prank(alice); + staking.stake(100 ether, 100 days, alice); + + vm.startPrank(bob); + staking.stake(100 ether, 10 days, bob); + staking.extend(0, 50 days); + staking.extend(0, 100 days); + + // Both are now locked up for the same amount of time, + // and should have the same points. + assertEq(staking.balanceOf(alice), staking.balanceOf(bob)); + + (uint128 aliceAmount, uint128 aliceEnd, uint256 alicePoints) = staking + .lockups(alice, 0); + (uint128 bobAmount, uint128 bobEnd, uint256 bobPoints) = staking + .lockups(bob, 0); + assertEq(aliceAmount, bobAmount, "same amount"); + assertEq(aliceEnd, bobEnd, "same end"); + assertEq(alicePoints, bobPoints, "same points"); + } + + function testShortExtendFail() public { + vm.prank(alice); + staking.stake(100 ether, 100 days, alice); + + vm.startPrank(bob); + staking.stake(100 ether, 11 days, bob); + vm.expectRevert("Staking: New lockup must be longer"); + staking.extend(0, 10 days); + } + + function testDoubleStake() external { + vm.startPrank(alice); + + uint256 beforeOgv = ogv.balanceOf(alice); + staking.stake(3 ether, 10 days, alice); + uint256 midOgv = ogv.balanceOf(alice); + uint256 midPoints = staking.balanceOf(alice); + staking.stake(5 ether, 40 days, alice); + + vm.warp(EPOCH + 50 days); + staking.unstake(1); + + assertEq(midPoints, staking.balanceOf(alice)); + assertEq(midOgv, ogv.balanceOf(alice)); + + staking.unstake(0); + assertEq(0, staking.balanceOf(alice)); // No points, since all unstaked + assertEq(beforeOgv, ogv.balanceOf(alice)); // All OGV back + } + + function testNoEarlyUnstake() public { + vm.startPrank(alice); + staking.stake(10 ether, 1000 days, alice); + vm.warp(999 days); + vm.expectRevert("Staking: End of lockup not reached"); + staking.unstake(0); + } + + function testCollectRewards() public { + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](3); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 4 ether; + slopes[1].start = uint64(EPOCH + 2 days); + slopes[1].ratePerDay = 2 ether; + slopes[2].start = uint64(EPOCH + 7 days); + slopes[2].ratePerDay = 1 ether; + vm.prank(team); + source.setInflation(slopes); // Add from start + + vm.startPrank(alice); + staking.stake(1 ether, 360 days, alice); + + vm.warp(EPOCH + 2 days); + uint256 beforeOgv = ogv.balanceOf(alice); + uint256 preview = staking.previewRewards(alice); + staking.collectRewards(); + uint256 afterOgv = ogv.balanceOf(alice); + + uint256 collectedRewards = afterOgv - beforeOgv; + assertApproxEqAbs( + collectedRewards, + 8 ether, + 1e8, + "actual amount should be correct" + ); + assertEq(collectedRewards, preview, "preview should match actual"); + assertApproxEqAbs( + preview, + 8 ether, + 1e8, + "preview amount should be correct" + ); + } + + function testCollectedRewardsJumpInOut() public { + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](1); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 2 ether; + + vm.prank(team); + source.setInflation(slopes); + + vm.prank(alice); + staking.stake(1 ether, 10 days, alice); + + // One day later + vm.warp(EPOCH + 1 days); + vm.prank(alice); + staking.collectRewards(); // Alice collects + + vm.prank(bob); + staking.stake(1 ether, 9 days, bob); // Bob stakes + + vm.warp(EPOCH + 2 days); // Alice and bob should split rewards evenly + uint256 aliceBefore = ogv.balanceOf(alice); + uint256 bobBefore = ogv.balanceOf(bob); + vm.prank(alice); + staking.collectRewards(); // Alice collects + vm.prank(bob); + staking.collectRewards(); // Bob collects + assertEq( + ogv.balanceOf(alice) - aliceBefore, + ogv.balanceOf(bob) - bobBefore + ); + } + + function testMultipleUnstake() public { + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](1); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 2 ether; + + vm.prank(team); + source.setInflation(slopes); + + vm.startPrank(alice); + staking.stake(1 ether, 10 days, alice); + vm.warp(EPOCH + 11 days); + staking.unstake(0); + vm.expectRevert("Staking: Already unstaked this lockup"); + staking.unstake(0); + } + + function testCollectRewardsOnExpand() public { + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](1); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 2 ether; + + vm.prank(team); + source.setInflation(slopes); + + vm.prank(alice); + staking.stake(1 ether, 10 days); + vm.prank(bob); + staking.stake(1 ether, 10 days); + + vm.warp(EPOCH + 6 days); + + vm.prank(bob); + staking.collectRewards(); + vm.prank(alice); + staking.extend(0, 10 days); + + assertEq(ogv.balanceOf(alice), ogv.balanceOf(bob)); + } + + function testNoSupplyShortCircuts() public { + uint256 beforeAlice = ogv.balanceOf(alice); + + vm.prank(alice); + staking.previewRewards(alice); + assertEq(ogv.balanceOf(alice), beforeAlice); + + vm.prank(alice); + staking.collectRewards(); + assertEq(ogv.balanceOf(alice), beforeAlice); + + vm.prank(bob); + staking.stake(1 ether, 9 days, bob); + + vm.prank(alice); + staking.previewRewards(alice); + assertEq(ogv.balanceOf(alice), beforeAlice); + + vm.prank(alice); + staking.collectRewards(); + assertEq(ogv.balanceOf(alice), beforeAlice); + } + + function testMultipleStakesSameBlock() public { + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](3); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 4 ether; + slopes[1].start = uint64(EPOCH + 2 days); + slopes[1].ratePerDay = 2 ether; + slopes[2].start = uint64(EPOCH + 7 days); + slopes[2].ratePerDay = 1 ether; + vm.prank(team); + source.setInflation(slopes); // Add from start + + vm.prank(alice); + staking.stake(1 ether, 360 days, alice); + + vm.warp(EPOCH + 9 days); + + vm.prank(alice); + staking.stake(1 ether, 60 days, alice); + vm.prank(bob); + staking.stake(1 ether, 90 days, bob); + vm.prank(alice); + staking.stake(1 ether, 180 days, alice); + vm.prank(bob); + staking.stake(1 ether, 240 days, bob); + vm.prank(alice); + staking.stake(1 ether, 360 days, alice); + vm.prank(alice); + staking.collectRewards(); + vm.prank(alice); + staking.collectRewards(); + } + + function testZeroSupplyRewardDebtPerShare() public { + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](1); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 2 ether; + vm.prank(team); + source.setInflation(slopes); + + vm.prank(alice); + staking.stake(1 ether, 10 days); + vm.prank(bob); + staking.stake(1 ether, 10 days); + + // Alice will unstake, setting her rewardDebtPerShare + vm.warp(EPOCH + 10 days); + vm.prank(alice); + staking.unstake(0); + + // Bob unstakes, setting the total supply to zero + vm.warp(EPOCH + 20 days); + vm.prank(bob); + staking.unstake(0); + + // Alice stakes. + // Even with the total supply being zero, it is important that + // Alice's rewardDebtPerShare per share be set to match the accRewardPerShare + vm.prank(alice); + staking.stake(1 ether, 10 days); + + // Alice unstakes later. + // If rewardDebtPerShare was wrong, this will fail because she will + // try to collect more OGV than the contract has + vm.warp(EPOCH + 30 days); + vm.prank(alice); + staking.unstake(1); + } + + function testFuzzCanAlwaysWithdraw( + uint96 amountA, + uint96 amountB, + uint64 durationA, + uint64 durationB, + uint64 start + ) public { + uint256 HUNDRED_YEARS = 100 * 366 days; + uint256 LAST_START = HUNDRED_YEARS - 1461 days; + vm.warp(start % LAST_START); + + durationA = durationA % uint64(1461 days); + durationB = durationB % uint64(1461 days); + if (durationA < 7 days) { + durationA = 7 days; + } + if (durationB < 7 days) { + durationB = 7 days; + } + if (amountA < 1) { + amountA = 1; + } + if (amountB < 1) { + amountB = 1; + } + + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](1); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 2 ether; + vm.prank(team); + source.setInflation(slopes); + + vm.prank(alice); + ogv.mint(alice, amountA); + vm.prank(alice); + ogv.approve(address(staking), amountA); + vm.prank(alice); + staking.stake(amountA, durationA, alice); + + vm.prank(bob); + ogv.mint(bob, amountB); + vm.prank(bob); + ogv.approve(address(staking), amountB); + vm.prank(bob); + staking.stake(amountB, durationB, bob); + + vm.warp(HUNDRED_YEARS); + vm.prank(alice); + staking.unstake(0); + vm.prank(bob); + staking.unstake(0); + } + + function testFuzzSemiSanePowerFunction(uint256 start) public { + uint256 HUNDRED_YEARS = 100 * 366 days; + start = start % HUNDRED_YEARS; + vm.warp(start); + vm.prank(bob); + staking.stake(1e18, 10 days, bob); + uint256 y = (356 days + start + 10 days) / 365 days; + uint256 maxPoints = 2**y * 1e18; + assertLt(staking.balanceOf(bob), maxPoints); + } +} diff --git a/tests/staking/RewardsSource.t.sol b/tests/staking/RewardsSource.t.sol new file mode 100644 index 00000000..889c7d97 --- /dev/null +++ b/tests/staking/RewardsSource.t.sol @@ -0,0 +1,201 @@ +import "forge-std/Test.sol"; +import "../../contracts/RewardsSource.sol"; +import "../../contracts/tests/MockOgv.sol"; + +contract RewardsSourceTest is Test { + MockOgv ogv; + RewardsSource rewards; + + address staking = address(0x42); + address team = address(0x43); + address alice = address(0x44); + + uint256 constant EPOCH = 1 days; + + event InflationChanged(); + event RewardsTargetChange(address target, address previousTarget); + + function setUp() public { + vm.startPrank(team); + ogv = new MockOgv(); + rewards = new RewardsSource(address(ogv)); + rewards.setRewardsTarget(address(staking)); + vm.stopPrank(); + } + + function testRewardSlopes() public { + vm.prank(team); + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](3); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 4 ether; + slopes[0].end = type(uint64).max; // Test that it's ignored + slopes[1].start = uint64(EPOCH + 2 days); + slopes[1].ratePerDay = 2 ether; + slopes[1].end = type(uint64).max; // Test that it's ignored + slopes[2].start = uint64(EPOCH + 7 days); + slopes[2].ratePerDay = 1 ether; + slopes[2].end = 0; // Test that it's ignored + vm.expectEmit(false, false, false, false); + emit InflationChanged(); + rewards.setInflation(slopes); + + vm.startPrank(staking); + vm.warp(EPOCH - 1000); + assertEq(rewards.collectRewards(), 0 ether, "a"); + + vm.warp(EPOCH + 1 days); + assertEq(rewards.collectRewards(), 4 ether, "b"); + + vm.warp(EPOCH + 2 days); + assertEq(rewards.collectRewards(), 4 ether, "c"); + + vm.warp(EPOCH + 3 days); + assertEq(rewards.collectRewards(), 2 ether, "d"); + + vm.warp(EPOCH + 4 days); + assertEq(rewards.collectRewards(), 2 ether, "e"); + + vm.warp(EPOCH + 8 days); + assertEq(rewards.collectRewards(), (6 ether + 1 ether), "f"); + + vm.warp(EPOCH + 9 days); + assertEq(rewards.collectRewards(), (1 ether), "g"); + + vm.warp(EPOCH + 10 days); + assertEq(rewards.collectRewards(), (1 ether), "h"); + + vm.warp(EPOCH + 11 days); + assertEq(rewards.collectRewards(), (1 ether), "i"); + + vm.warp(EPOCH + 12 days); + assertEq(rewards.collectRewards(), (1 ether), "j"); + + vm.warp(EPOCH + 13 days); + assertEq(rewards.collectRewards(), (1 ether), "k"); + + vm.warp(EPOCH + 14 days); + assertEq(rewards.collectRewards(), (1 ether), "l"); + + vm.warp(EPOCH + 15 days); + assertEq(rewards.collectRewards(), (1 ether), "m"); + } + + function testRewardSlopesSegmentOverlap() public { + vm.prank(team); + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](3); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 4 ether; + slopes[1].start = uint64(EPOCH + 2 days); + slopes[1].ratePerDay = 2 ether; + slopes[2].start = uint64(EPOCH + 7 days); + slopes[2].ratePerDay = 1 ether; + rewards.setInflation(slopes); + + vm.startPrank(staking); + vm.warp(EPOCH - 1000); + assertEq(rewards.collectRewards(), 0 ether, "a"); + + // 2x4 + 5x2 + 8x1 == + vm.warp(EPOCH + 15 days); + assertEq(rewards.collectRewards(), (26 ether), "m"); + } + + function testRewardSlopesStartInside() public { + vm.prank(team); + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](3); + slopes[0].start = uint64(1 days / 2); + slopes[0].ratePerDay = 4 ether; + slopes[1].start = uint64(EPOCH + 2 days); + slopes[1].ratePerDay = 2 ether; + slopes[2].start = uint64(EPOCH + 7 days); + slopes[2].ratePerDay = 1 ether; + rewards.setInflation(slopes); + + vm.startPrank(staking); + assertEq(rewards.collectRewards(), 0 ether, "a"); + vm.warp(EPOCH); + assertEq(rewards.collectRewards(), 2 ether, "b"); + } + + function testBackInTime() public { + vm.prank(team); + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](3); + slopes[0].start = uint64(1 days / 2); + slopes[0].ratePerDay = 4 ether; + slopes[1].start = uint64(EPOCH + 2 days); + slopes[1].ratePerDay = 2 ether; + slopes[2].start = uint64(EPOCH + 7 days); + slopes[2].ratePerDay = 1 ether; + rewards.setInflation(slopes); + + vm.startPrank(staking); + assertEq(rewards.collectRewards(), 0 ether, "a"); + vm.warp(EPOCH); + assertEq(rewards.collectRewards(), 2 ether, "b"); + vm.warp(EPOCH - 1 days / 2); // Back in time + assertEq(rewards.collectRewards(), 0 ether, "b"); + } + + function testRewardSlopesNoSlopes() public { + vm.prank(team); + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](0); + rewards.setInflation(slopes); + + vm.startPrank(staking); + assertEq(rewards.collectRewards(), 0 ether, "a"); + vm.warp(EPOCH + 2 days); + assertEq(rewards.collectRewards(), 0 ether, "b"); + } + + function testRewardSlopesTooManySlopes() public { + vm.prank(team); + vm.expectRevert("Rewards: Too many slopes"); + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](65); + rewards.setInflation(slopes); + } + + function testNonPublicSetInflation() public { + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](0); + vm.prank(alice); + vm.expectRevert("Caller is not the Governor"); + rewards.setInflation(slopes); + } + + function testNoPublicCollect() public { + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](0); + vm.prank(team); + rewards.setInflation(slopes); + vm.prank(alice); + vm.expectRevert("Rewards: Not rewardsTarget"); + rewards.collectRewards(); + } + + function testNonIncreasingSlopes() public { + vm.prank(team); + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](3); + slopes[0].start = uint64(EPOCH); + slopes[1].start = uint64(EPOCH - 1); + slopes[2].start = uint64(EPOCH + 2); + + vm.expectRevert("Rewards: Start times must increase"); + rewards.setInflation(slopes); + } + + function testTooMuchInflation() public { + vm.prank(team); + RewardsSource.Slope[] memory slopes = new RewardsSource.Slope[](1); + slopes[0].start = uint64(EPOCH); + slopes[0].ratePerDay = 2000000000000 ether; + + vm.expectRevert("Rewards: RatePerDay too high"); + rewards.setInflation(slopes); + } + + function testSetRewardsTarget() public { + vm.prank(team); + vm.expectEmit(false, false, false, true); + emit RewardsTargetChange(address(0), address(staking)); + rewards.setRewardsTarget(address(0)); + assertEq(rewards.rewardsTarget(), address(0)); + } +} diff --git a/tests/staking/test_lockup_restrictions.py b/tests/staking/test_lockup_restrictions.py deleted file mode 100644 index cf079fea..00000000 --- a/tests/staking/test_lockup_restrictions.py +++ /dev/null @@ -1,13 +0,0 @@ -import brownie -from brownie import chain, accounts -from ..fixtures import token, staking, rewards - -def test_cant_stake_above_max(staking, token): - token.approve(staking, 100e18) - with brownie.reverts("Staking: Too long"): - staking.stake(1, chain.time() + 86400 * 365 * 5, accounts.default) # 5 years - -def test_cant_stake_zero_tokens(staking, token): - with brownie.reverts("Staking: Not enough"): - staking.stake(0, chain.time() + 86400 * 365, accounts.default) -