From 21734438f6efba0d02930f4d3337a0104d90f0c8 Mon Sep 17 00:00:00 2001 From: toyvo Date: Sun, 3 Mar 2024 10:10:21 -0500 Subject: [PATCH 01/16] resolves 5.1 --- src/RevenueHandler.sol | 4 ++++ src/test/RevenueHandler.t.sol | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/RevenueHandler.sol b/src/RevenueHandler.sol index 2448ab0..0dd618b 100644 --- a/src/RevenueHandler.sol +++ b/src/RevenueHandler.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3 pragma solidity ^0.8.15; +import "lib/forge-std/src/console2.sol"; + import "src/interfaces/IRevenueHandler.sol"; import "src/interfaces/IPoolAdapter.sol"; import "src/interfaces/IVotingEscrow.sol"; @@ -194,6 +196,8 @@ contract RevenueHandler is IRevenueHandler, Ownable { // If the alchemist is defined we know it has an alchemic-token if (alchemists[alchemist] != address(0)) { + require(token == IAlchemistV2(alchemist).debtToken(), "Invalid alchemist/alchemic-token pair"); + (, address[] memory deposits) = IAlchemistV2(alchemist).accounts(recipient); IERC20(token).approve(alchemist, amount); diff --git a/src/test/RevenueHandler.t.sol b/src/test/RevenueHandler.t.sol index f3251ac..088adac 100644 --- a/src/test/RevenueHandler.t.sol +++ b/src/test/RevenueHandler.t.sol @@ -275,6 +275,8 @@ contract RevenueHandlerTest is BaseTest { } function testClaimRevenueOneEpoch() external { + revenueHandler.addAlchemicToken(address(alethAlchemist)); + uint256 revAmt = 1000e18; uint256 tokenId = _setupClaimableRevenue(revAmt); @@ -285,7 +287,11 @@ contract RevenueHandlerTest is BaseTest { uint256 debtAmt = 5000e18; _takeDebt(debtAmt); + hevm.expectRevert(abi.encodePacked("Invalid alchemist/alchemic-token pair")); + revenueHandler.claim(tokenId, alusd, address(alethAlchemist), claimable, address(this)); + revenueHandler.claim(tokenId, alusd, address(alusdAlchemist), claimable, address(this)); + (int256 finalDebt, ) = alusdAlchemist.accounts(address(this)); assertApproxEq(debtAmt - claimable, uint256(finalDebt), uint256(finalDebt) / DELTA); } From c2d0400ca198b4e72416372126b1997c27c84f24 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 4 Mar 2024 10:27:56 -0500 Subject: [PATCH 02/16] resolves 5.2 --- src/Minter.sol | 2 + src/RevenueHandler.sol | 4 +- src/Voter.sol | 2 +- src/gauges/StakingRewards.sol | 2 +- src/test/BaseTest.sol | 4 ++ src/test/FluxToken.t.sol | 4 +- src/test/Minter.t.sol | 76 ++++++++-------------- src/test/PassthroughGauge.t.sol | 2 +- src/test/RevenueHandler.t.sol | 6 +- src/test/Voting.t.sol | 66 +++++++++---------- src/test/VotingEscrow.t.sol | 110 +++++++++++++++----------------- 11 files changed, 126 insertions(+), 152 deletions(-) diff --git a/src/Minter.sol b/src/Minter.sol index ed128c6..7b5966b 100644 --- a/src/Minter.sol +++ b/src/Minter.sol @@ -122,6 +122,8 @@ contract Minter is IMinter { /// @inheritdoc IMinter function updatePeriod() external returns (uint256) { + require(msg.sender == address(voter), "not voter"); + uint256 period = activePeriod; if (block.timestamp >= period + DURATION && initializer == address(0)) { diff --git a/src/RevenueHandler.sol b/src/RevenueHandler.sol index 0dd618b..9620ad8 100644 --- a/src/RevenueHandler.sol +++ b/src/RevenueHandler.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3 pragma solidity ^0.8.15; -import "lib/forge-std/src/console2.sol"; - import "src/interfaces/IRevenueHandler.sol"; import "src/interfaces/IPoolAdapter.sol"; import "src/interfaces/IVotingEscrow.sol"; @@ -49,7 +47,7 @@ contract RevenueHandler is IRevenueHandler, Ownable { uint256 lastClaimEpoch; } - uint256 internal constant WEEK = 1 weeks; + uint256 internal constant WEEK = 2 weeks; uint256 internal constant BPS = 10_000; address public immutable veALCX; diff --git a/src/Voter.sol b/src/Voter.sol index 4ef767c..c125cca 100644 --- a/src/Voter.sol +++ b/src/Voter.sol @@ -27,7 +27,7 @@ contract Voter is IVoter { uint256 internal constant BPS = 10_000; uint256 internal constant MAX_BOOST = 5000; uint256 internal constant MIN_BOOST = 0; - uint256 internal constant DURATION = 7 days; // rewards are released over 7 days + uint256 internal constant DURATION = 2 weeks; // rewards are released over 2 weeks uint256 internal constant BRIBE_LAG = 1 days; uint256 internal index; diff --git a/src/gauges/StakingRewards.sol b/src/gauges/StakingRewards.sol index b9c2fd0..d03842c 100644 --- a/src/gauges/StakingRewards.sol +++ b/src/gauges/StakingRewards.sol @@ -20,7 +20,7 @@ contract StakingRewards is IStakingRewards, ReentrancyGuard, Pausable { address public stakingToken; uint256 public periodFinish = 0; uint256 public rewardRate = 0; - uint256 public rewardsDuration = 7 days; + uint256 public rewardsDuration = 2 weeks; uint256 public lastUpdateTime; uint256 public rewardPerTokenStored; diff --git a/src/test/BaseTest.sol b/src/test/BaseTest.sol index 7a80439..4a81804 100644 --- a/src/test/BaseTest.sol +++ b/src/test/BaseTest.sol @@ -273,4 +273,8 @@ contract BaseTest is DSTestPlus { IBribe(_bribeAddress).notifyRewardAmount(_token, _amount); } + + function newEpoch() public view returns (uint256) { + return IMinter(minter).activePeriod() + IMinter(minter).DURATION() + 1 seconds; + } } diff --git a/src/test/FluxToken.t.sol b/src/test/FluxToken.t.sol index ba55839..3e234a6 100644 --- a/src/test/FluxToken.t.sol +++ b/src/test/FluxToken.t.sol @@ -99,8 +99,8 @@ contract FluxTokenTest is BaseTest { hevm.prank(admin); voter.reset(tokenId); - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); } uint256 unclaimedFluxEnd = flux.getUnclaimedFlux(tokenId); diff --git a/src/test/Minter.t.sol b/src/test/Minter.t.sol index cac8c1b..a215740 100644 --- a/src/test/Minter.t.sol +++ b/src/test/Minter.t.sol @@ -17,10 +17,8 @@ contract MinterTest is BaseTest { function testEpochEmissions() external { uint256 treasuryBalanceBefore = alcx.balanceOf(address(devmsig)); - uint256 period = minter.activePeriod(); - // Set the block timestamp to be the next epoch - hevm.warp(period + nextEpoch); + hevm.warp(newEpoch()); uint256 currentTotalEmissions = minter.circulatingEmissionsSupply(); uint256 epochEmissions = minter.epochEmission(); @@ -42,7 +40,7 @@ contract MinterTest is BaseTest { ); // Mint emissions for epoch - minter.updatePeriod(); + voter.distribute(); uint256 distributorBalance = alcx.balanceOf(address(distributor)); uint256 voterBalance = alcx.balanceOf(address(voter)); @@ -62,15 +60,13 @@ contract MinterTest is BaseTest { // Test reaching emissions tail function testTailEmissions() external { - uint256 period = minter.activePeriod(); - // Mint emissions for the amount of epochs until tail emissions target - hevm.warp(period + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); for (uint8 i = 1; i <= epochsUntilTail; ++i) { - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); } uint256 tailRewards = minter.rewards(); @@ -112,14 +108,10 @@ contract MinterTest is BaseTest { // Verify the setup paramaters and emissions schedule are working as expected function testWeeklyEmissionsSchedule() public { - uint256 period = minter.activePeriod(); - initializeVotingEscrow(); uint256 startingRewards = minter.rewards(); - minter.updatePeriod(); - // Rewards should equal the starting amount contract was initialized with assertEq(startingRewards, rewards); @@ -127,19 +119,19 @@ contract MinterTest is BaseTest { assertEq(distributor.claimable(tokenId), 0); // Fast forward one epoch - hevm.warp(period + nextEpoch); + hevm.warp(newEpoch()); hevm.roll(block.number + 1); - minter.updatePeriod(); + voter.distribute(); // After one epoch rewards amount should decrease by the defined stepdown amount assertEq(minter.stepdown(), startingRewards - minter.rewards()); // Fast forward one epoch - hevm.warp(block.timestamp + nextEpoch); + hevm.warp(newEpoch()); hevm.roll(block.number + 1); - minter.updatePeriod(); + voter.distribute(); // After two epochs the amount of ALCX claimable for a veALCX token should increase assertGt(distributor.claimable(tokenId), 0); @@ -154,16 +146,14 @@ contract MinterTest is BaseTest { // Initial balance of accounts ALCX uint256 alcxBalanceBefore = alcx.balanceOf(admin); - minter.updatePeriod(); - // After no epoch has passed, amount claimable should be 0 assertEq(distributor.claimable(tokenId), 0, "amount claimable should be 0"); // Fast forward one epoch - hevm.warp(block.timestamp + nextEpoch); + hevm.warp(newEpoch()); hevm.roll(block.number + 1); - minter.updatePeriod(); + voter.distribute(); // Initial ALCX balance of the rewards distributor uint256 distributorBalanceBefore = alcx.balanceOf(address(distributor)); @@ -203,13 +193,11 @@ contract MinterTest is BaseTest { // Rewards require an epoch to pass before there is a claimable amount // Rewards are dependent on time in epoch in which veALCX was created function testRewardsWithinEpoch() public { - uint256 period = minter.activePeriod(); - initializeVotingEscrow(); // Get a fresh epoch - hevm.warp(period + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -220,12 +208,12 @@ contract MinterTest is BaseTest { uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, MAXTIME, false); // Finish the epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); // Go to the next epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 claimable1 = distributor.claimable(tokenId1); uint256 claimable2 = distributor.claimable(tokenId2); @@ -235,8 +223,6 @@ contract MinterTest is BaseTest { // Compound claiming adds ALCX rewards into their exisiting veALCX position function testCompoundRewards() public { - uint256 period = minter.activePeriod(); - initializeVotingEscrow(); hevm.startPrank(admin); @@ -244,16 +230,14 @@ contract MinterTest is BaseTest { // Initial amount of BPT locked in a veALCX position (uint256 initLockedAmount, , , ) = veALCX.locked(tokenId); - minter.updatePeriod(); - // After no epoch has passed, amount claimable should be 0 assertEq(distributor.claimable(tokenId), 0, "amount claimable should be 0"); // Fast forward one epoch - hevm.warp(period + nextEpoch); + hevm.warp(newEpoch()); hevm.roll(block.number + 1); - minter.updatePeriod(); + voter.distribute(); (uint256 amount, ) = distributor.amountToCompound(distributor.claimable(tokenId)); @@ -276,10 +260,10 @@ contract MinterTest is BaseTest { assertEq(wethBalanceBefore - wethBalanceAfter, amount); // Fast forward one epoch - hevm.warp(block.timestamp + nextEpoch); + hevm.warp(newEpoch()); hevm.roll(block.number + 1); - minter.updatePeriod(); + voter.distribute(); // Make sure account has enough eth to compound (amount, ) = distributor.amountToCompound(distributor.claimable(tokenId)); @@ -293,22 +277,18 @@ contract MinterTest is BaseTest { // Compound claiming should revert if user doesn't provide enough weth function testCompoundRewardsFailure() public { - uint256 period = minter.activePeriod(); - initializeVotingEscrow(); hevm.startPrank(admin); - minter.updatePeriod(); - assertEq(distributor.claimable(tokenId), 0, "amount claimable should be 0"); assertEq(distributor.claim(tokenId, true), 0, "amount claimed should be 0"); // Fast forward one epoch - hevm.warp(period + nextEpoch); + hevm.warp(newEpoch()); hevm.roll(block.number + 1); - minter.updatePeriod(); + voter.distribute(); // Set weth balance to 0 weth.transfer(dead, weth.balanceOf(admin)); @@ -319,24 +299,20 @@ contract MinterTest is BaseTest { // Compound claiming should revert if user doesn't provide enough weth function testCompoundRewardsFailureETH() public { - uint256 period = minter.activePeriod(); - deal(admin, 100 ether); initializeVotingEscrow(); hevm.startPrank(admin); - minter.updatePeriod(); - assertEq(distributor.claimable(tokenId), 0, "amount claimable should be 0"); assertEq(distributor.claim(tokenId, true), 0, "amount claimed should be 0"); // Fast forward one epoch - hevm.warp(period + nextEpoch); + hevm.warp(newEpoch()); hevm.roll(block.number + 1); - minter.updatePeriod(); + voter.distribute(); // Set weth balance to 0 weth.transfer(dead, weth.balanceOf(admin)); diff --git a/src/test/PassthroughGauge.t.sol b/src/test/PassthroughGauge.t.sol index c1280f9..06228c6 100644 --- a/src/test/PassthroughGauge.t.sol +++ b/src/test/PassthroughGauge.t.sol @@ -44,7 +44,7 @@ contract PassthroughGaugeTest is BaseTest { hevm.warp(block.timestamp + nextEpoch); // Update gauges to get claimable rewards value - minter.updatePeriod(); + voter.distribute(); voter.updateFor(gauges); // Claimable rewards of each gauge diff --git a/src/test/RevenueHandler.t.sol b/src/test/RevenueHandler.t.sol index 088adac..38039b9 100644 --- a/src/test/RevenueHandler.t.sol +++ b/src/test/RevenueHandler.t.sol @@ -8,8 +8,8 @@ import "lib/v2-foundry/src/interfaces/IAlchemistV2.sol"; import "lib/v2-foundry/src/interfaces/IWhitelist.sol"; contract RevenueHandlerTest is BaseTest { - uint256 ONE_EPOCH_TIME = 1 weeks; - uint256 ONE_EPOCH_BLOCKS = (1 weeks) / 12; + uint256 ONE_EPOCH_TIME = 2 weeks; + uint256 ONE_EPOCH_BLOCKS = (2 weeks) / 12; uint256 DELTA = 65; IAlchemistV2 public alusdAlchemist = IAlchemistV2(0x5C6374a2ac4EBC38DeA0Fc1F8716e5Ea1AdD94dd); @@ -262,7 +262,7 @@ contract RevenueHandlerTest is BaseTest { revenueHandler.claim(tokenId1, alusd, address(alusdAlchemist), claimable, address(this)); hevm.warp(period + nextEpoch); - minter.updatePeriod(); + voter.distribute(); claimable = revenueHandler.claimable(tokenId1, alusd); uint256 claimable2 = revenueHandler.claimable(tokenId2, alusd); diff --git a/src/test/Voting.t.sol b/src/test/Voting.t.sol index 676a75c..26c4c42 100644 --- a/src/test/Voting.t.sol +++ b/src/test/Voting.t.sol @@ -21,7 +21,7 @@ contract VotingTest is BaseTest { hevm.warp(period + nextEpoch); hevm.roll(block.number + 1); - minter.updatePeriod(); + voter.distribute(); uint256 distributorBal2 = alcx.balanceOf(address(distributor)); uint256 voterBal2 = alcx.balanceOf(address(voter)); @@ -44,7 +44,7 @@ contract VotingTest is BaseTest { hevm.prank(admin); voter.vote(tokenId, pools, weights, 0); - minter.updatePeriod(); + voter.distribute(); uint256 distributorBal3 = alcx.balanceOf(address(distributor)); uint256 voterBal3 = alcx.balanceOf(address(voter)); @@ -95,7 +95,7 @@ contract VotingTest is BaseTest { // Move forward a week relative to period hevm.warp(period + nextEpoch); - minter.updatePeriod(); + voter.distribute(); hevm.startPrank(admin); voter.claimBribes(bribes, tokens, tokenId); @@ -410,7 +410,7 @@ contract VotingTest is BaseTest { assertEq(TOKEN_100K, IERC20(bal).balanceOf(bribeAddress), "bribe contract missing bribes"); // Epoch start should equal the current block.timestamp rounded to a week - assertEq(block.timestamp - (block.timestamp % (7 days)), IBribe(bribeAddress).getEpochStart(block.timestamp)); + assertEq(block.timestamp - (block.timestamp % (2 weeks)), IBribe(bribeAddress).getEpochStart(block.timestamp)); // Rewards list should increase after adding bribe assertEq(IBribe(bribeAddress).rewardsListLength(), rewardsLength + 1); @@ -540,7 +540,7 @@ contract VotingTest is BaseTest { hevm.prank(beef); voter.vote(tokenId2, pools, weights, 0); - minter.updatePeriod(); + voter.distribute(); uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); uint256 earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); @@ -615,7 +615,7 @@ contract VotingTest is BaseTest { hevm.prank(holder); voter.poke(tokenId3); - minter.updatePeriod(); + voter.distribute(); hevm.warp(block.timestamp + nextEpoch); @@ -665,8 +665,8 @@ contract VotingTest is BaseTest { assertEq(earnedBribes0, 0, "no bribes should be earned yet"); // Start second epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); @@ -688,8 +688,8 @@ contract VotingTest is BaseTest { assertEq(earnedBribes1, IERC20(bal).balanceOf(admin), "admin should receive bribes"); // Start third epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); assertEq(earnedBribes2, earnedBribes1, "earned bribes from previous epoch should remain"); @@ -700,8 +700,8 @@ contract VotingTest is BaseTest { assertEq(earnedBribes1 + earnedBribes2, IERC20(bal).balanceOf(admin), "admin should receive both bribes"); // Start fourth epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); // Add more bribes createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); @@ -720,8 +720,8 @@ contract VotingTest is BaseTest { voter.vote(tokenId1, pools, weights, 0); // Start fifth epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 earnedBribes4 = IBribe(bribeAddress).earned(bal, tokenId1); @@ -843,8 +843,8 @@ contract VotingTest is BaseTest { voter.vote(tokenId1, pools, weights, 0); // ------------------- Start second epoch i+1 - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); assertEq(earnedBribes1, TOKEN_100K, "bribes from voting should be earned"); @@ -856,17 +856,17 @@ contract VotingTest is BaseTest { assertEq(earnedBribes1, IERC20(bal).balanceOf(admin), "admin should receive bribes"); // ------------------- Start third epoch i+3 - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); + hevm.warp(newEpoch()); + voter.distribute(); // in epoch i+3, admin votes again hevm.prank(admin); voter.vote(tokenId1, pools, weights, 0); // ------------------- Start fourth epoch i+4 - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); // INTENDED BEHAVIOUR: since the bribes for epoch i were already claimed in epoch i+1 // --and no more bribes were notified after that-- there should be no available earnings at epoch i+4. @@ -907,8 +907,8 @@ contract VotingTest is BaseTest { assertEq(usedWeight1, usedWeight2, "used weight should be equal"); - hevm.warp(block.timestamp + 6 days); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); hevm.prank(dead); voter.vote(tokenId3, pools, weights, 0); @@ -954,7 +954,7 @@ contract VotingTest is BaseTest { hevm.warp(block.timestamp + 5 weeks); - minter.updatePeriod(); + voter.distribute(); uint256 votingPower2 = veALCX.balanceOfToken(tokenId); @@ -977,8 +977,8 @@ contract VotingTest is BaseTest { // veALCX voting power should decay to 0 function testVotingPowerDecay() public { // Kick off epoch cycle - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, 3 weeks, false); uint256 tokenId2 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -988,7 +988,7 @@ contract VotingTest is BaseTest { tokens[1] = tokenId2; address[] memory pools = new address[](1); - pools[0] = alETHPool; + pools[0] = sushiPoolAddress; uint256[] memory weights = new uint256[](1); weights[0] = 5000; @@ -1001,8 +1001,8 @@ contract VotingTest is BaseTest { uint256 totalWeight1 = voter.totalWeight(); // Move to the next epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); // Move to when token1 expires hevm.warp(block.timestamp + 3 weeks); @@ -1013,7 +1013,7 @@ contract VotingTest is BaseTest { voter.pokeTokens(tokens); hevm.startPrank(admin); - minter.updatePeriod(); + voter.distribute(); // tokenId1 represents user who voted once and expired uint256 usedWeight = voter.usedWeights(tokenId1); @@ -1025,8 +1025,8 @@ contract VotingTest is BaseTest { assertGt(usedWeight1, usedWeight2, "used weight should decrease"); assertGt(totalWeight1, totalWeight2, "total weight should decrease"); - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 balance = veALCX.balanceOfToken(tokenId1); diff --git a/src/test/VotingEscrow.t.sol b/src/test/VotingEscrow.t.sol index fd4153b..ce39526 100644 --- a/src/test/VotingEscrow.t.sol +++ b/src/test/VotingEscrow.t.sol @@ -199,7 +199,7 @@ contract VotingEscrowTest is BaseTest { uint256 voteTimestamp1 = block.timestamp; hevm.warp(block.timestamp + nextEpoch * 2); - minter.updatePeriod(); + voter.distribute(); // Creates a new checkpoint at index 1 createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -207,7 +207,7 @@ contract VotingEscrowTest is BaseTest { uint256 voteTimestamp2 = block.timestamp; hevm.warp(block.timestamp + nextEpoch * 5); - minter.updatePeriod(); + voter.distribute(); // Creates a new checkpoint at index 2 createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -235,8 +235,8 @@ contract VotingEscrowTest is BaseTest { uint256 originalVotingPower = veALCX.balanceOfToken(tokenId); - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 decayedTimestamp = block.timestamp; @@ -247,15 +247,15 @@ contract VotingEscrowTest is BaseTest { uint256 getOriginalVotingPower = veALCX.balanceOfTokenAt(tokenId, originalTimestamp); assertEq(getOriginalVotingPower, originalVotingPower, "voting power should be equal"); - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); // Getting the voting power at a point in time should return the expected result uint256 getDecayedVotingPower = veALCX.balanceOfTokenAt(tokenId, decayedTimestamp); assertEq(getDecayedVotingPower, decayedVotingPower, "voting powers should be equal"); // Token is expired starting in this epoch - hevm.warp(block.timestamp + nextEpoch); + hevm.warp(newEpoch()); uint256 expiredVotingPower = veALCX.balanceOfToken(tokenId); assertEq(expiredVotingPower, 0, "voting power should be 0 after lock expires"); @@ -309,9 +309,9 @@ contract VotingEscrowTest is BaseTest { voter.reset(tokenId); - hevm.warp(block.timestamp + THREE_WEEKS); + hevm.warp(newEpoch()); - minter.updatePeriod(); + voter.distribute(); uint256 unclaimedAlcx = distributor.claimable(tokenId); uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId); @@ -322,7 +322,7 @@ contract VotingEscrowTest is BaseTest { hevm.expectRevert(abi.encodePacked("Cooldown period in progress")); veALCX.withdraw(tokenId); - hevm.warp(block.timestamp + nextEpoch); + hevm.warp(newEpoch()); veALCX.withdraw(tokenId); @@ -346,8 +346,8 @@ contract VotingEscrowTest is BaseTest { function testFluxAccrual() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); - uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, 3 weeks, false); - uint256 tokenId3 = createVeAlcx(holder, TOKEN_1, 3 weeks, false); + uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, 5 weeks, false); + uint256 tokenId3 = createVeAlcx(holder, TOKEN_1, 5 weeks, false); hevm.prank(admin); voter.reset(tokenId1); @@ -364,8 +364,8 @@ contract VotingEscrowTest is BaseTest { assertGt(unclaimedFlux1, unclaimedFlux2, "unclaimed flux should be greater for longer lock"); // Start new epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); hevm.prank(holder); voter.reset(tokenId3); @@ -385,15 +385,15 @@ contract VotingEscrowTest is BaseTest { voter.reset(tokenId1); // Start new epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); hevm.prank(admin); voter.reset(tokenId1); // Start new epoch - hevm.warp(block.timestamp + nextEpoch); - minter.updatePeriod(); + hevm.warp(newEpoch()); + voter.distribute(); uint256 claimable1 = distributor.claimable(tokenId1); uint256 claimable2 = distributor.claimable(tokenId2); @@ -467,17 +467,17 @@ contract VotingEscrowTest is BaseTest { // Check merging of two veALCX function testMergeTokens() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); - uint256 tokenId2 = createVeAlcx(admin, TOKEN_100K, FIVE_WEEKS, false); + uint256 tokenId2 = createVeAlcx(admin, TOKEN_100K, MAXTIME / 2, false); hevm.startPrank(admin); uint256 lockEnd1 = veALCX.lockEnd(tokenId1); - assertEq(lockEnd1, ((block.timestamp + MAXTIME) / 1 weeks) * 1 weeks); + assertEq(lockEnd1, ((block.timestamp + MAXTIME) / ONE_WEEK) * ONE_WEEK); assertEq(veALCX.lockedAmount(tokenId1), TOKEN_1); // Vote to trigger flux accrual - hevm.warp(block.timestamp + nextEpoch); + hevm.warp(newEpoch()); address[] memory pools = new address[](1); pools[0] = alETHPool; @@ -486,9 +486,9 @@ contract VotingEscrowTest is BaseTest { voter.vote(tokenId1, pools, weights, 0); voter.vote(tokenId2, pools, weights, 0); - minter.updatePeriod(); + voter.distribute(); - hevm.warp(block.timestamp + nextEpoch); + hevm.warp(newEpoch()); // Reset to allow merging of tokens voter.reset(tokenId1); @@ -731,9 +731,8 @@ contract VotingEscrowTest is BaseTest { function testGetPastTotalSupply() public { createVeAlcx(admin, TOKEN_1, MAXTIME, false); - minter.updatePeriod(); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.getPastTotalSupply(block.timestamp - 2 days), @@ -741,7 +740,7 @@ contract VotingEscrowTest is BaseTest { "before second update" ); - minter.updatePeriod(); + voter.distribute(); assertGt( veALCX.getPastTotalSupply(block.timestamp - 2 days), @@ -749,8 +748,8 @@ contract VotingEscrowTest is BaseTest { "after second update" ); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.getPastTotalSupply(block.timestamp - 2 days), @@ -758,7 +757,7 @@ contract VotingEscrowTest is BaseTest { "before third update" ); - minter.updatePeriod(); + voter.distribute(); assertGt( veALCX.getPastTotalSupply(block.timestamp - 2 days), @@ -766,8 +765,8 @@ contract VotingEscrowTest is BaseTest { "after third update" ); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.getPastTotalSupply(block.timestamp - 2 days), @@ -779,9 +778,8 @@ contract VotingEscrowTest is BaseTest { function testTotalSupplyAtT() public { createVeAlcx(admin, TOKEN_1, MAXTIME, false); - minter.updatePeriod(); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.totalSupplyAtT(block.timestamp - 2 days), @@ -789,7 +787,7 @@ contract VotingEscrowTest is BaseTest { "before second update" ); - minter.updatePeriod(); + voter.distribute(); assertGt( veALCX.totalSupplyAtT(block.timestamp - 2 days), @@ -797,8 +795,8 @@ contract VotingEscrowTest is BaseTest { "after second update" ); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.totalSupplyAtT(block.timestamp - 2 days), @@ -806,12 +804,12 @@ contract VotingEscrowTest is BaseTest { "before third update" ); - minter.updatePeriod(); + voter.distribute(); // Check that the RewardsDistributor and veALCX are in sync uint256 timeCursor = distributor.timeCursor(); - uint256 veSupply = distributor.veSupply(timeCursor - 1 weeks); - uint256 supplyAt = veALCX.totalSupplyAtT(timeCursor - 1 weeks); + uint256 veSupply = distributor.veSupply(timeCursor - ONE_WEEK); + uint256 supplyAt = veALCX.totalSupplyAtT(timeCursor - ONE_WEEK); assertEq(veSupply, supplyAt, "veSupply should equal supplyAt"); @@ -821,8 +819,8 @@ contract VotingEscrowTest is BaseTest { "after third update" ); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.totalSupplyAtT(block.timestamp - 2 days), @@ -834,9 +832,8 @@ contract VotingEscrowTest is BaseTest { function testBalanceOfTokenAt() public { uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false); - minter.updatePeriod(); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.balanceOfTokenAt(tokenId, block.timestamp - 2 days), @@ -844,7 +841,7 @@ contract VotingEscrowTest is BaseTest { "before second update" ); - minter.updatePeriod(); + voter.distribute(); deal(bpt, address(this), TOKEN_1); IERC20(bpt).approve(address(veALCX), TOKEN_1); veALCX.depositFor(tokenId, TOKEN_1); @@ -855,8 +852,8 @@ contract VotingEscrowTest is BaseTest { "after second update" ); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.balanceOfTokenAt(tokenId, block.timestamp - 2 days), @@ -864,7 +861,7 @@ contract VotingEscrowTest is BaseTest { "before third update" ); - minter.updatePeriod(); + voter.distribute(); deal(bpt, address(this), TOKEN_1); IERC20(bpt).approve(address(veALCX), TOKEN_1); veALCX.depositFor(tokenId, TOKEN_1); @@ -875,8 +872,8 @@ contract VotingEscrowTest is BaseTest { "after third update" ); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); + hevm.roll(block.number + ONE_WEEK / 12); assertGt( veALCX.balanceOfTokenAt(tokenId, block.timestamp - 2 days), @@ -890,7 +887,6 @@ contract VotingEscrowTest is BaseTest { uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false); - minter.updatePeriod(); hevm.warp(block.timestamp + 3 days); hevm.roll(block.number + 3 days / 12); @@ -920,9 +916,7 @@ contract VotingEscrowTest is BaseTest { function testManipulatePastSupplyWithDeposit() public { uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false); - minter.updatePeriod(); - hevm.warp(block.timestamp + 1 weeks + 1); - hevm.roll(block.number + 1 weeks / 12); + hevm.warp(newEpoch()); uint256 t2Dp1 = block.timestamp - (2 days + 1); uint256 t2Dm1 = block.timestamp - (2 days - 1); @@ -935,7 +929,7 @@ contract VotingEscrowTest is BaseTest { deal(bpt, address(this), TOKEN_1); IERC20(bpt).approve(address(veALCX), TOKEN_1); veALCX.depositFor(tokenId, TOKEN_1); - minter.updatePeriod(); + voter.distribute(); assertEq(veALCX.totalSupplyAtT(t2Dp1), bal2DaysPlus1, "after deposit, 2 days + 1"); assertEq(veALCX.totalSupplyAtT(t2Dm1), bal2DaysMinus1, "after deposit, 2 days - 1"); @@ -948,7 +942,7 @@ contract VotingEscrowTest is BaseTest { assertEq(numCheckpoints, 1, "numCheckpoints should be 1"); hevm.warp(block.timestamp + nextEpoch * 45); - minter.updatePeriod(); + voter.distribute(); uint256 totalPower = veALCX.totalSupply(); uint256 tokenPower = veALCX.balanceOfToken(tokenId1); From 8c3b18a57e9b5a4a4e2361965c9bc9c75264b5e9 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 4 Mar 2024 21:26:55 -0500 Subject: [PATCH 03/16] resolves 5.3 --- src/test/Voting.t.sol | 56 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/test/Voting.t.sol b/src/test/Voting.t.sol index 26c4c42..463f246 100644 --- a/src/test/Voting.t.sol +++ b/src/test/Voting.t.sol @@ -879,6 +879,62 @@ contract VotingTest is BaseTest { voter.claimBribes(bribes, tokens, tokenId1); } + function testBugVotingSupply() public { + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); + uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, MAXTIME, false); + address bribeAddress = voter.bribes(address(sushiGauge)); + + // Add BAL bribes to sushiGauge + createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + address[] memory bribes = new address[](1); + bribes[0] = address(bribeAddress); + address[][] memory tokens = new address[][](1); + tokens[0] = new address[](1); + tokens[0][0] = bal; + + // in epoch i, attacker votes with balance x + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + hevm.prank(beef); + voter.vote(tokenId2, pools, weights, 0); + + // ------------------- Start second epoch i+1 + hevm.warp(newEpoch()); + createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); + + // attacker calls Voter.distribute(), which triggers Bribe.resetVoting(), therefore setting totalVoting = 0. + // the attacker votes again with balance x which triggers a new supply checkpoint which includes only attacker's vote x + // Attacker calls Bribe.getRewardForOwner() which sends all rewards of an epoch to the attacker. + hevm.startPrank(admin); + voter.distribute(); + voter.vote(tokenId1, pools, weights, 0); + voter.claimBribes(bribes, tokens, tokenId1); + hevm.stopPrank(); + + // If this attack is successful then an additional voter should not be able to claim bribes + // Additional voter votes + hevm.prank(beef); + voter.vote(tokenId2, pools, weights, 0); + + hevm.warp(newEpoch()); + voter.distribute(); + + hevm.prank(admin); + voter.claimBribes(bribes, tokens, tokenId1); + hevm.prank(beef); + voter.claimBribes(bribes, tokens, tokenId2); + + // This test failing indicates that the attack was successful + assertEq(IERC20(bal).balanceOf(admin), IERC20(bal).balanceOf(beef), "earned bribes are not equal"); + } + // Voting power should be dependent on epoch at which vote is cast function testVotingPower() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); From 826692d74c8a5df3a5594009590470318e66c33e Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 4 Mar 2024 21:44:51 -0500 Subject: [PATCH 04/16] resolves 5.4 --- src/test/Voting.t.sol | 55 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/test/Voting.t.sol b/src/test/Voting.t.sol index 463f246..1e199a4 100644 --- a/src/test/Voting.t.sol +++ b/src/test/Voting.t.sol @@ -935,6 +935,61 @@ contract VotingTest is BaseTest { assertEq(IERC20(bal).balanceOf(admin), IERC20(bal).balanceOf(beef), "earned bribes are not equal"); } + function testBugExtraCheckpoint() public { + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); + uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, MAXTIME, false); + address bribeAddress = voter.bribes(address(sushiGauge)); + + // Add BAL bribes to sushiGauge + createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + address[] memory bribes = new address[](1); + bribes[0] = address(bribeAddress); + address[][] memory tokens = new address[][](1); + tokens[0] = new address[](1); + tokens[0][0] = bal; + + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + hevm.prank(beef); + voter.vote(tokenId2, pools, weights, 0); + + // Start second epoch i+1 + hevm.warp(newEpoch()); + voter.distribute(); + createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); + + // When claiming a reward in contract Bribe a new checkpoint is added for the tokenId + hevm.prank(admin); + voter.claimBribes(bribes, tokens, tokenId1); + + // A checkpoint in an epoch might exist even if the user did not actively vote. + // The user can therefore claim rewards for this epoch in the future, + // causing solvency issues as the users that voted cannot receive their share of bribes. + + // If this is the case beef wouldn't be able to claim all of the bribes from epoch i + 1 + hevm.prank(beef); + voter.vote(tokenId2, pools, weights, 0); + + hevm.warp(newEpoch()); + voter.distribute(); + + // Beef should earn all of the bribes from epoch i + 1 in addition to their bribes from epoch i + assertEq(IBribe(bribeAddress).earned(bal, tokenId2), (TOKEN_100K / 2) + TOKEN_100K, "earned bribes incorrect"); + + hevm.prank(beef); + voter.claimBribes(bribes, tokens, tokenId2); + + // This test failing indicates that the attack was successful + assertGt(IERC20(bal).balanceOf(beef), IERC20(bal).balanceOf(admin), "beef should capture more bribes"); + } + // Voting power should be dependent on epoch at which vote is cast function testVotingPower() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); From 1c69e7b95b8325e1658dd2628becd6421ddcec21 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 08:24:29 -0400 Subject: [PATCH 05/16] resolves 5.4 --- src/Bribe.sol | 1 - src/test/Voting.t.sol | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Bribe.sol b/src/Bribe.sol index 2d02950..d3034f0 100644 --- a/src/Bribe.sol +++ b/src/Bribe.sol @@ -281,7 +281,6 @@ contract Bribe is IBribe { lastEarn[tokens[i]][tokenId] = block.timestamp; _writeCheckpoint(tokenId, balanceOf[tokenId]); - _writeSupplyCheckpoint(); _safeTransfer(tokens[i], _owner, _reward); diff --git a/src/test/Voting.t.sol b/src/test/Voting.t.sol index 1e199a4..f3ae124 100644 --- a/src/test/Voting.t.sol +++ b/src/test/Voting.t.sol @@ -735,6 +735,43 @@ contract VotingTest is BaseTest { ); } + function testGetRewardForOwner() public { + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); + + address bribeAddress = voter.bribes(address(sushiGauge)); + + // Add BAL bribes to sushiGauge + createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + address[] memory bribes = new address[](1); + bribes[0] = address(bribeAddress); + address[][] memory tokens = new address[][](1); + tokens[0] = new address[](1); + tokens[0][0] = bal; + + // in epoch i, user votes with balance x + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + // Start second epoch i+1 + hevm.warp(newEpoch()); + createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); + + // Claim bribes from epoch i + hevm.prank(admin); + voter.claimBribes(bribes, tokens, tokenId1); + + hevm.warp(newEpoch()); + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("no rewards to claim")); + voter.claimBribes(bribes, tokens, tokenId1); + } + function testBugTotalBribeWeights() public { // epoch i uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); From aa911e7d810ef4b21bdfa5c055d5676c000a8265 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 09:13:51 -0400 Subject: [PATCH 06/16] resolves 5.7 --- src/Voter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Voter.sol b/src/Voter.sol index c125cca..181646d 100644 --- a/src/Voter.sol +++ b/src/Voter.sol @@ -25,7 +25,7 @@ contract Voter is IVoter { address public immutable bribefactory; uint256 internal constant BPS = 10_000; - uint256 internal constant MAX_BOOST = 5000; + uint256 internal constant MAX_BOOST = 10000; uint256 internal constant MIN_BOOST = 0; uint256 internal constant DURATION = 2 weeks; // rewards are released over 2 weeks uint256 internal constant BRIBE_LAG = 1 days; From e46caf61ea5f0f1d45dbd8217cf51f413ab05f19 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 09:27:24 -0400 Subject: [PATCH 07/16] resolves 5.8 --- src/Bribe.sol | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/Bribe.sol b/src/Bribe.sol index d3034f0..7d56374 100644 --- a/src/Bribe.sol +++ b/src/Bribe.sol @@ -115,31 +115,21 @@ contract Bribe is IBribe { /// @inheritdoc IBribe function swapOutRewardToken(uint256 oldTokenIndex, address oldToken, address newToken) external { - require(msg.sender == voter); - require(IVoter(voter).isWhitelisted(newToken), "bribe tokens must be whitelisted"); - require(rewards[oldTokenIndex] == oldToken); - require(newToken != address(0)); + require(msg.sender == voter, "Only voter can execute"); + require(IVoter(voter).isWhitelisted(newToken), "New token must be whitelisted"); + require(rewards[oldTokenIndex] == oldToken, "Old token mismatch"); + require(newToken != address(0), "New token cannot be zero address"); - isReward[oldToken] = false; - isReward[newToken] = true; - - // Check if the newToken already exists in the rewards list + // Check that the newToken does not already exist in the rewards array for (uint256 i = 0; i < rewards.length; i++) { - if (rewards[i] == newToken) { - // If it exists, swap out the old token - rewards[oldTokenIndex] = rewards[i]; - - // Then remove the duplicate - rewards[i] = rewards[rewards.length - 1]; - rewards.pop(); - break; - } + require(rewards[i] != newToken, "New token already exists"); } - // If the old token wasn't updated, swap it out - if (rewards[oldTokenIndex] != newToken) { - rewards[oldTokenIndex] = newToken; - } + isReward[oldToken] = false; + isReward[newToken] = true; + + // Since we've now ensured the new token doesn't exist, we can safely update + rewards[oldTokenIndex] = newToken; emit RewardTokenSwapped(oldToken, newToken); } From ff254819f9ce0abc15e9b88367c4a8132b18c292 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 09:37:20 -0400 Subject: [PATCH 08/16] resolves 5.10 --- src/Bribe.sol | 1 + src/governance/L2Governor.sol | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/Bribe.sol b/src/Bribe.sol index 7d56374..485fee7 100644 --- a/src/Bribe.sol +++ b/src/Bribe.sol @@ -74,6 +74,7 @@ contract Bribe is IBribe { /// @inheritdoc IBribe function lastTimeRewardApplicable(address token) public view returns (uint256) { + // Return the current period if it's still active, otherwise return the next period return Math.min(block.timestamp, periodFinish[token]); } diff --git a/src/governance/L2Governor.sol b/src/governance/L2Governor.sol index d60c319..3feac14 100644 --- a/src/governance/L2Governor.sol +++ b/src/governance/L2Governor.sol @@ -227,6 +227,8 @@ abstract contract L2Governor is Context, ERC165, EIP712, IGovernor, IERC721Recei */ function proposalEta(uint256 proposalId) external view virtual returns (uint256) { uint256 eta = _timelock.getTimestamp(_timelockIds[proposalId]); + // Returns the timestamp at which an operation becomes ready (0 for + // * unset operations, 1 for done operations) return eta == 1 ? 0 : eta; // _DONE_TIMESTAMP (1) should be replaced with a 0 value } From a1c4ee5a7abeec62eb78581426bb720219c18339 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 09:53:43 -0400 Subject: [PATCH 09/16] resolves 5.11 --- src/Voter.sol | 15 --------------- src/VotingEscrow.sol | 10 ---------- src/interfaces/IVoter.sol | 16 ---------------- src/interfaces/IVotingEscrow.sol | 4 ---- 4 files changed, 45 deletions(-) diff --git a/src/Voter.sol b/src/Voter.sol index 181646d..1d443ca 100644 --- a/src/Voter.sol +++ b/src/Voter.sol @@ -277,21 +277,6 @@ contract Voter is IVoter { emit GaugeRevived(_gauge); } - /// @inheritdoc IVoter - function attachTokenToGauge(uint256 tokenId, address account) external { - require(isGauge[msg.sender]); - require(isAlive[msg.sender]); // killed gauges cannot attach tokens to themselves - if (tokenId > 0) IVotingEscrow(veALCX).attach(tokenId); - emit Attach(account, msg.sender, tokenId); - } - - /// @inheritdoc IVoter - function detachTokenFromGauge(uint256 tokenId, address account) external { - require(isGauge[msg.sender]); - if (tokenId > 0) IVotingEscrow(veALCX).detach(tokenId); - emit Detach(account, msg.sender, tokenId); - } - /// @inheritdoc IVoter function notifyRewardAmount(uint256 amount) external { require(msg.sender == minter, "only minter can send rewards"); diff --git a/src/VotingEscrow.sol b/src/VotingEscrow.sol index 631f5c3..c7dfb60 100644 --- a/src/VotingEscrow.sol +++ b/src/VotingEscrow.sol @@ -552,16 +552,6 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { voted[_tokenId] = false; } - function attach(uint256 _tokenId) external { - require(msg.sender == voter); - attachments[_tokenId] = attachments[_tokenId] + 1; - } - - function detach(uint256 _tokenId) external { - require(msg.sender == voter); - attachments[_tokenId] = attachments[_tokenId] - 1; - } - function setfluxMultiplier(uint256 _fluxMultiplier) external { require(msg.sender == admin, "not admin"); require(_fluxMultiplier > 0, "fluxMultiplier must be greater than 0"); diff --git a/src/interfaces/IVoter.sol b/src/interfaces/IVoter.sol index f421eee..5c590ab 100644 --- a/src/interfaces/IVoter.sol +++ b/src/interfaces/IVoter.sol @@ -110,22 +110,6 @@ interface IVoter { */ function createGauge(address _pool, GaugeType _gaugeType) external returns (address); - /** - * @notice Attach veALCX token to a gauge - * @param _tokenId ID of the token being attached - * @param account Address of owner - * @dev Can only be called by an active gauge - */ - function attachTokenToGauge(uint256 _tokenId, address account) external; - - /** - * @notice Detach veALCX token to a gauge - * @param _tokenId ID of the token being detached - * @param account Address of owner - * @dev Can only be called by a gauge - */ - function detachTokenFromGauge(uint256 _tokenId, address account) external; - /** * @notice Send the distribution of emissions to the Voter contract * @param amount Amount of rewards being distributed diff --git a/src/interfaces/IVotingEscrow.sol b/src/interfaces/IVotingEscrow.sol index 0b89471..85abbbb 100644 --- a/src/interfaces/IVotingEscrow.sol +++ b/src/interfaces/IVotingEscrow.sol @@ -191,10 +191,6 @@ interface IVotingEscrow { function abstain(uint256 tokenId) external; - function attach(uint256 tokenId) external; - - function detach(uint256 tokenId) external; - /** * @notice Record global data to checkpoint */ From c3ac67d78af2a49f7a00a670090691b48ab44f22 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 15:26:56 -0400 Subject: [PATCH 10/16] resolves 5.12 --- src/governance/L2Governor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/governance/L2Governor.sol b/src/governance/L2Governor.sol index 3feac14..615d25f 100644 --- a/src/governance/L2Governor.sol +++ b/src/governance/L2Governor.sol @@ -602,7 +602,7 @@ abstract contract L2Governor is Context, ERC165, EIP712, IGovernor, IERC721Recei /** * @dev Relays a transaction or function call to an arbitrary target. In cases where the governance executor * is some contract other than the governor itself, like when using a timelock, this function can be invoked - * in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. + * in a governance proposal. * Note that if the executor is simply the governor itself, use of `relay` is redundant. */ function relay(address target, uint256 value, bytes calldata data) external virtual onlyGovernance { From ea04a0d53a307b353cf5efe765547ee3e10f8afc Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 17:01:42 -0400 Subject: [PATCH 11/16] resolves 5.15 --- src/governance/TimelockExecutor.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/governance/TimelockExecutor.sol b/src/governance/TimelockExecutor.sol index 01e0423..5ab6224 100644 --- a/src/governance/TimelockExecutor.sol +++ b/src/governance/TimelockExecutor.sol @@ -100,7 +100,10 @@ contract TimelockExecutor is AccessControl, IERC721Receiver, IERC1155Receiver { * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, IERC165) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId; + return + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + interfaceId == type(AccessControl).interfaceId; } /** From 697918d355e48e4f310a1602a64675afc497f55e Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 17:09:48 -0400 Subject: [PATCH 12/16] resolves 5.16 --- src/governance/TimelockExecutor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/governance/TimelockExecutor.sol b/src/governance/TimelockExecutor.sol index 5ab6224..36c5699 100644 --- a/src/governance/TimelockExecutor.sol +++ b/src/governance/TimelockExecutor.sol @@ -279,7 +279,7 @@ contract TimelockExecutor is AccessControl, IERC721Receiver, IERC1155Receiver { bytes32 predecessor, bytes32 descriptionHash, uint256 chainId - ) public payable virtual onlyRole(EXECUTOR_ROLE) { + ) public virtual onlyRole(EXECUTOR_ROLE) { require(targets.length == values.length, "TimelockExecutor: length mismatch"); require(targets.length == payloads.length, "TimelockExecutor: length mismatch"); From b17f3d46ada9f199b7167adac81568f7f7ed80e1 Mon Sep 17 00:00:00 2001 From: toyvo Date: Mon, 1 Apr 2024 17:29:30 -0400 Subject: [PATCH 13/16] resolves 5.19 --- src/governance/L2Governor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/governance/L2Governor.sol b/src/governance/L2Governor.sol index 615d25f..2ea0d01 100644 --- a/src/governance/L2Governor.sol +++ b/src/governance/L2Governor.sol @@ -438,7 +438,7 @@ abstract contract L2Governor is Context, ERC165, EIP712, IGovernor, IERC721Recei bytes[] memory calldatas, bytes32 descriptionHash, uint256 chainId - ) internal virtual onlyGovernance returns (uint256) { + ) public virtual onlyGovernance returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash, chainId); ProposalState status = state(proposalId); From 32ceaa9e029aa227b7b7c0a0d6483c08dcea04e8 Mon Sep 17 00:00:00 2001 From: toyvo Date: Tue, 16 Apr 2024 10:53:43 -0400 Subject: [PATCH 14/16] docs updates --- src/AlchemixGovernor.sol | 12 +++ src/BaseGauge.sol | 8 +- src/Bribe.sol | 24 ++++- src/CurveEthPoolAdapter.sol | 2 + src/CurveMetaPoolAdapter.sol | 2 + src/FluxToken.sol | 21 +++-- src/Minter.sol | 18 +++- src/RevenueHandler.sol | 18 +++- src/RewardPoolManager.sol | 12 ++- src/RewardsDistributor.sol | 18 +++- src/Voter.sol | 68 ++++++++++---- src/VotingEscrow.sol | 105 ++++++++++++++-------- src/factories/BribeFactory.sol | 3 + src/factories/GaugeFactory.sol | 5 ++ src/gauges/CurveGauge.sol | 10 +-- src/interfaces/IBaseGauge.sol | 15 +++- src/interfaces/IBribe.sol | 20 +++++ src/interfaces/IFluxToken.sol | 12 +++ src/interfaces/IMinter.sol | 6 ++ src/interfaces/IPoolAdapter.sol | 27 +++++- src/interfaces/IRewardsDistributor.sol | 6 ++ src/interfaces/IVoter.sol | 69 +++++++++++++- src/interfaces/IVotingEscrow.sol | 120 ++++++++++++++++++++++++- 23 files changed, 511 insertions(+), 90 deletions(-) diff --git a/src/AlchemixGovernor.sol b/src/AlchemixGovernor.sol index 40b496d..4ffb634 100644 --- a/src/AlchemixGovernor.sol +++ b/src/AlchemixGovernor.sol @@ -61,6 +61,10 @@ contract AlchemixGovernor is L2Governor, L2GovernorVotes, L2GovernorVotesQuorumF emit AdminUpdated(pendingAdmin); } + /** + * @dev Set the % of total supply required to create a proposal + * @param numerator The new numerator value for the quorum fraction + */ function setProposalNumerator(uint256 numerator) external { require(msg.sender == admin, "not admin"); require(numerator <= MAX_PROPOSAL_NUMERATOR, "numerator too high"); @@ -68,12 +72,20 @@ contract AlchemixGovernor is L2Governor, L2GovernorVotes, L2GovernorVotesQuorumF emit ProposalNumberSet(numerator); } + /** + * @dev Set the value for the voting delay as defined in L2Governor.sol + * @param newDelay The new number of days voting will be delayed by + */ function setVotingDelay(uint256 newDelay) external { require(msg.sender == admin, "not admin"); votingDelay = newDelay; emit VotingDelaySet(votingDelay); } + /** + * @dev Set the value for the voting period as defined in L2Governor.sol + * @param newPeriod The new number of days voting will be open for + */ function setVotingPeriod(uint256 newPeriod) external { require(msg.sender == admin, "not admin"); votingPeriod = newPeriod; diff --git a/src/BaseGauge.sol b/src/BaseGauge.sol index 6f10f78..6bc7261 100644 --- a/src/BaseGauge.sol +++ b/src/BaseGauge.sol @@ -17,13 +17,13 @@ abstract contract BaseGauge is IBaseGauge { uint256 internal constant BRIBE_LAG = 1 days; uint256 internal constant MAX_REWARD_TOKENS = 16; - address public ve; // Ve token used for gauges + address public ve; // veALCX token used for gauges address public bribe; // Address of bribe contract address public voter; // Address of voter contract address public admin; address public pendingAdmin; - address public receiver; - address public rewardToken; + address public receiver; // Address that receives the ALCX rewards + address public rewardToken; // Address of the reward token // Re-entrancy check uint256 internal _unlocked = 1; @@ -38,6 +38,7 @@ abstract contract BaseGauge is IBaseGauge { View functions */ + /// @inheritdoc IBaseGauge function getVotingStage(uint256 timestamp) external pure returns (VotingStage) { uint256 modTime = timestamp % (1 weeks); if (modTime < BRIBE_LAG) { @@ -63,6 +64,7 @@ abstract contract BaseGauge is IBaseGauge { admin = pendingAdmin; } + /// @inheritdoc IBaseGauge function updateReceiver(address _receiver) external { require(msg.sender == admin, "not admin"); require(_receiver != address(0), "cannot be zero address"); diff --git a/src/Bribe.sol b/src/Bribe.sol index 485fee7..35fb9cb 100644 --- a/src/Bribe.sol +++ b/src/Bribe.sol @@ -13,19 +13,29 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; * @notice Implementation of bribe contract to be used with gauges */ contract Bribe is IBribe { - uint256 internal constant DURATION = 2 weeks; // Rewards released over voting period + /// @notice Rewards released over voting period + uint256 internal constant DURATION = 2 weeks; + /// @notice Duration of time when bribes are accepted uint256 internal constant BRIBE_LAG = 1 days; + /// @notice Maximum number of reward tokens a gauge can accept uint256 internal constant MAX_REWARD_TOKENS = 16; /// @notice The number of checkpoints uint256 public supplyNumCheckpoints; + /// @notice Number of voting period checkpoints uint256 public votingNumCheckpoints; + /// @notice Total votes allocated to the gauge uint256 public totalSupply; + /// @notice Total current votes in a voting period (this is reset each period) uint256 public totalVoting; + /// @notice veALCX contract address address public immutable veALCX; + /// @notice Voter contract address address public immutable voter; - address public gauge; // Address of the gauge that the bribes are for + /// @notice Address of the gauge that the bribes are for + address public gauge; + /// @notice List of reward tokens address[] public rewards; /// @notice A record of balance checkpoints for each account, by index @@ -34,11 +44,17 @@ contract Bribe is IBribe { mapping(uint256 => uint256) public numCheckpoints; /// @notice A record of balance checkpoints for each token, by index mapping(uint256 => SupplyCheckpoint) public supplyCheckpoints; + /// @notice A record of balance checkpoints for each voting period mapping(uint256 => VotingCheckpoint) public votingCheckpoints; + /// @notice A record of reward tokens that are accepted mapping(address => bool) public isReward; + /// @notice A record of token rewards per epoch for each reward token mapping(address => mapping(uint256 => uint256)) public tokenRewardsPerEpoch; + /// @notice Current votes allocated of a veALCX voter mapping(uint256 => uint256) public balanceOf; + /// @notice The end of the current voting period mapping(address => uint256) public periodFinish; + /// @notice The last time rewards were claimed mapping(address => mapping(uint256 => uint256)) public lastEarn; // Re-entrancy check @@ -166,6 +182,7 @@ contract Bribe is IBribe { return lower; } + /// @inheritdoc IBribe function getPriorVotingIndex(uint256 timestamp) public view returns (uint256) { uint256 nCheckpoints = votingNumCheckpoints; if (nCheckpoints == 0) { @@ -198,6 +215,7 @@ contract Bribe is IBribe { return lower; } + /// @inheritdoc IBribe function earned(address token, uint256 tokenId) public view returns (uint256) { if (numCheckpoints[tokenId] == 0) { return 0; @@ -279,6 +297,7 @@ contract Bribe is IBribe { } } + /// @inheritdoc IBribe function deposit(uint256 amount, uint256 tokenId) external { require(msg.sender == voter); @@ -294,6 +313,7 @@ contract Bribe is IBribe { emit Deposit(msg.sender, tokenId, amount); } + /// @inheritdoc IBribe function withdraw(uint256 amount, uint256 tokenId) external { require(msg.sender == voter); diff --git a/src/CurveEthPoolAdapter.sol b/src/CurveEthPoolAdapter.sol index b5e72ab..785d875 100644 --- a/src/CurveEthPoolAdapter.sol +++ b/src/CurveEthPoolAdapter.sol @@ -23,6 +23,7 @@ contract CurveEthPoolAdapter is IPoolAdapter { } } + /// @inheritdoc IPoolAdapter function getDy( address inputToken, address outputToken, @@ -31,6 +32,7 @@ contract CurveEthPoolAdapter is IPoolAdapter { return ICurveStableSwap(pool).get_dy(tokenIds[inputToken], tokenIds[outputToken], inputAmount); } + /// @inheritdoc IPoolAdapter function melt( address inputToken, address outputToken, diff --git a/src/CurveMetaPoolAdapter.sol b/src/CurveMetaPoolAdapter.sol index 23cbf3c..5872439 100644 --- a/src/CurveMetaPoolAdapter.sol +++ b/src/CurveMetaPoolAdapter.sol @@ -19,6 +19,7 @@ contract CurveMetaPoolAdapter is IPoolAdapter { } } + /// @inheritdoc IPoolAdapter function getDy( address inputToken, address outputToken, @@ -27,6 +28,7 @@ contract CurveMetaPoolAdapter is IPoolAdapter { return ICurveMetaSwap(pool).get_dy_underlying(tokenIds[inputToken], tokenIds[outputToken], inputAmount); } + /// @inheritdoc IPoolAdapter function melt( address inputToken, address outputToken, diff --git a/src/FluxToken.sol b/src/FluxToken.sol index fa138d8..efc2b9a 100644 --- a/src/FluxToken.sol +++ b/src/FluxToken.sol @@ -24,14 +24,23 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { /// @dev The address which enables the minting of tokens. address public minter; + /// @notice The address of the voter contract. address public voter; + /// @notice The address of the veALCX contract. address public veALCX; - address public alchemechNFT; // TOKE - address public patronNFT; // ETH - address public admin; // the timelock executor - address public pendingAdmin; // the timelock executor + /// @notice The address of the alchemechNFT contract (valued in TOKE). + address public alchemechNFT; + /// @notice The address of the patronNFT contract (valued in ETH). + address public patronNFT; + /// @notice The address of the admin and will be the timelock executor. + address public admin; + /// @notice The address of the pending admin. + address public pendingAdmin; + /// @notice The date the contract was deployed. uint256 public deployDate; - uint256 public alchemechMultiplier = 5; // .05% ratio of flux for alchemechNFT holders + /// @notice The multiplier for alchemechNFT holders (.05% ratio of FLUX for alchemechNFT holders) + uint256 public alchemechMultiplier = 5; + /// @notice The ratio of FLUX patron NFT holders receive (.4%) uint256 public bptMultiplier = 40; uint256 public immutable oneYear = 365 days; @@ -94,6 +103,7 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { patronNFT = _patronNFT; } + /// @inheritdoc IFluxToken function setNftMultiplier(uint256 _nftMultiplier) external { require(msg.sender == admin, "not admin"); require(_nftMultiplier != 0, "FluxToken: nftMultiplier cannot be zero"); @@ -101,6 +111,7 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { alchemechMultiplier = _nftMultiplier; } + /// @inheritdoc IFluxToken function setBptMultiplier(uint256 _bptMultiplier) external { require(msg.sender == admin, "not admin"); require(_bptMultiplier != 0, "FluxToken: bptMultiplier cannot be zero"); diff --git a/src/Minter.sol b/src/Minter.sol index 7b5966b..7aaadd8 100644 --- a/src/Minter.sol +++ b/src/Minter.sol @@ -23,16 +23,25 @@ contract Minter is IMinter { // Allows minting once per epoch (epoch = 2 week, reset every Thursday 00:00 UTC) uint256 public immutable DURATION = 2 weeks; uint256 public immutable BPS = 10_000; - uint256 public constant TAIL_EMISSIONS_RATE = 2194e18; // Tail emissions rate + /// @notice The ALCX tail emissions rate + uint256 public constant TAIL_EMISSIONS_RATE = 2194e18; + /// @notice Amount of emissions minted per epoch uint256 public epochEmissions; + /// @notice The active period uint256 public activePeriod; + /// @notice The stepdown rate of emissions uint256 public stepdown; + /// @notice The current amount of ALCX rewards uint256 public rewards; + /// @notice The current circulating supply of ALCX emissions uint256 public supply; - uint256 public veAlcxEmissionsRate; // bps of emissions going to veALCX holders - uint256 public timeEmissionsRate; // bps of emissions going to TIME stakers - uint256 public treasuryEmissionsRate; // bps of emissions going to treasury + /// @notice The bps of emissions going to veALCX holders + uint256 public veAlcxEmissionsRate; + /// @notice The bps of emissions going to TIME stakers + uint256 public timeEmissionsRate; + /// @notice The bps of emissions going to treasury + uint256 public treasuryEmissionsRate; address public admin; address public pendingAdmin; @@ -105,6 +114,7 @@ contract Minter is IMinter { emit AdminUpdated(pendingAdmin); } + /// @inheritdoc IMinter function setTreasury(address _treasury) external { require(msg.sender == admin, "not admin"); require(_treasury != address(0), "treasury cannot be 0x0"); diff --git a/src/RevenueHandler.sol b/src/RevenueHandler.sol index 9620ad8..b5ab020 100644 --- a/src/RevenueHandler.sol +++ b/src/RevenueHandler.sol @@ -50,14 +50,24 @@ contract RevenueHandler is IRevenueHandler, Ownable { uint256 internal constant WEEK = 2 weeks; uint256 internal constant BPS = 10_000; + /// @notice The veALCX contract address. address public immutable veALCX; + /// @notice The list of revenue tokens. address[] public revenueTokens; - mapping(address => address) public alchemists; // alchemist => alchemic-token - mapping(address => RevenueTokenConfig) public revenueTokenConfigs; // token => RevenueTokenConfig - mapping(uint256 => mapping(address => uint256)) public epochRevenues; // epoch => (debtToken => epoch revenue) - mapping(uint256 => mapping(address => Claimable)) public userCheckpoints; // tokenId => (debtToken => Claimable) + /// @notice A mapping of alchemists to their alchemic-tokens. + mapping(address => address) public alchemists; + /// @notice A mapping of revenue tokens to their configurations. + mapping(address => RevenueTokenConfig) public revenueTokenConfigs; + /// @notice A mapping of epoch to a mapping of debtToken to epoch revenue. + mapping(uint256 => mapping(address => uint256)) public epochRevenues; + /// @notice A mapping of tokenId to a mapping of debtToken to a user's claimable amount. + mapping(uint256 => mapping(address => Claimable)) public userCheckpoints; + + /// @notice The current epoch. uint256 public currentEpoch; + /// @notice The address of the treasury. address public treasury; + /// @notice The percentage of revenue that goes to the treasury. uint256 public treasuryPct; constructor(address _veALCX, address _treasury, uint256 _treasuryPct) Ownable() { diff --git a/src/RewardPoolManager.sol b/src/RewardPoolManager.sol index b8bfbd1..2fb1175 100644 --- a/src/RewardPoolManager.sol +++ b/src/RewardPoolManager.sol @@ -12,15 +12,23 @@ contract RewardPoolManager is IRewardPoolManager { uint256 internal constant MAX_REWARD_POOL_TOKENS = 10; + /// @notice The address of the admin address public admin; + /// @notice The address of the pending admin address public pendingAdmin; + /// @notice The address of the veALCX contract address public veALCX; - address public rewardPool; // destination for poolToken + /// @notice The address of the reward pool (Aura) + address public rewardPool; + /// @notice The address of the treasury address public treasury; - address public poolToken; // BPT + /// @notice The address of the pool token to send to the reward pool (ALCXBPT) + address public poolToken; + /// @notice The reward pool tokens (ex: AURA, BAL) address[] public rewardPoolTokens; + /// @notice Mapping of reward pool tokens mapping(address => bool) public isRewardPoolToken; constructor(address _admin, address _veALCX, address _poolToken, address _rewardPool, address _treasury) { diff --git a/src/RewardsDistributor.sol b/src/RewardsDistributor.sol index a79b79e..8861571 100644 --- a/src/RewardsDistributor.sol +++ b/src/RewardsDistributor.sol @@ -27,22 +27,35 @@ contract RewardsDistributor is IRewardsDistributor, ReentrancyGuard { address public immutable BURN_ADDRESS = address(0); uint256 public immutable BPS = 10_000; + /// @notice Balancer pool ID specific to the ALCX/WETH pool bytes32 public balancerPoolId; + /// @notice The start time of the epoch uint256 public startTime; + /// @notice Tracking of the epoch timestamp uint256 public timeCursor; + /// @notice The last time rewards were claimed for a veALCX holder uint256 public lastTokenTime; + /// @notice The last balance of the rewards token uint256 public tokenLastBalance; + /// @notice The address of the VotingEscrow (veALCX) contract address public immutable votingEscrow; + /// @notice The address of the rewards token address public rewardsToken; + /// @notice The address of token locked in the VotingEscrow contract address public lockedToken; + /// @notice The depositor address of rewards (Minter) address public depositor; + /// @notice The total weight of veALCX tokens uint256[1000000000000000] public veSupply; + /// @notice The total rewards distributed uint256[1000000000000000] public tokensPerWeek; + /// @notice The time cursor of a veALCX holder mapping(uint256 => uint256) public timeCursorOf; + /// @notice The epoch of a veALCX holder mapping(uint256 => uint256) public userEpochOf; IWETH9 public immutable WETH; @@ -123,14 +136,13 @@ contract RewardsDistributor is IRewardsDistributor, ReentrancyGuard { External functions */ - /** - * @notice - */ + /// @inheritdoc IRewardsDistributor function checkpointToken() external { assert(msg.sender == depositor); _checkpointToken(); } + /// @inheritdoc IRewardsDistributor function checkpointTotalSupply() external { _checkpointTotalSupply(); } diff --git a/src/Voter.sol b/src/Voter.sol index 1d443ca..60074d7 100644 --- a/src/Voter.sol +++ b/src/Voter.sol @@ -19,40 +19,62 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; contract Voter is IVoter { address internal immutable base; // Base token, ALCX - address public immutable veALCX; // veALCX that governs these contracts - address public immutable FLUX; // FLUX token distributed to veALCX holders + address public immutable veALCX; + /// @notice FLUX contract address + address public immutable FLUX; + /// @notice Gauge factory contract address address public immutable gaugefactory; + /// @notice Bribe factory contract address address public immutable bribefactory; uint256 internal constant BPS = 10_000; uint256 internal constant MAX_BOOST = 10000; uint256 internal constant MIN_BOOST = 0; - uint256 internal constant DURATION = 2 weeks; // rewards are released over 2 weeks + /// @notice Rewards are released over this duration (2 weeks) + uint256 internal constant DURATION = 2 weeks; uint256 internal constant BRIBE_LAG = 1 days; uint256 internal index; + /// @notice Minter contract address address public minter; address public admin; // should be set to the timelock admin address public pendingAdmin; - address public emergencyCouncil; // credibly neutral party similar to Curve's Emergency DAO - - uint256 public totalWeight; // total voting weight - uint256 public boostMultiplier = 10000; // max bps veALCX voting power can be boosted by - - address[] public pools; // all pools viable for incentives - - mapping(address => address) public gauges; // pool => gauge - mapping(address => address) public poolForGauge; // gauge => pool - mapping(address => address) public bribes; // gauge => bribe - mapping(address => uint256) public weights; // pool => weight - mapping(uint256 => mapping(address => uint256)) public votes; // token => pool => votes - mapping(uint256 => address[]) public poolVote; // token => pools - mapping(uint256 => uint256) public usedWeights; // token => total voting weight of user - mapping(uint256 => uint256) public lastVoted; // token => timestamp of last vote, to ensure one vote per epoch + /// @notice Address of credibly neutral party similar to Curve's Emergency DAO + address public emergencyCouncil; + + /// @notice Total voting weight + uint256 public totalWeight; + /// @notice Max bps veALCX voting power can be boosted by + uint256 public boostMultiplier = 10000; + + /// @notice All pools viable for incentives + address[] public pools; + + /// @notice pool => gauge + mapping(address => address) public gauges; + /// @notice gauge => pool + mapping(address => address) public poolForGauge; + /// @notice gauge => bribe + mapping(address => address) public bribes; + /// @notice pool => weight + mapping(address => uint256) public weights; + /// @notice token => pool => votes + mapping(uint256 => mapping(address => uint256)) public votes; + /// @notice token => pools + mapping(uint256 => address[]) public poolVote; + /// @notice token => total voting weight of user + mapping(uint256 => uint256) public usedWeights; + /// @notice token => timestamp of last vote, to ensure one vote per epoch + mapping(uint256 => uint256) public lastVoted; + /// @notice return if address is a gauge mapping(address => bool) public isGauge; + /// @notice return if address is whitelisted mapping(address => bool) public isWhitelisted; + /// @notice return if gauge is alive mapping(address => bool) public isAlive; + /// @notice return the supply index of a gauge mapping(address => uint256) internal supplyIndex; + /// @notice reutrn the claimable amount for a gauge mapping(address => uint256) public claimable; constructor(address _ve, address _gauges, address _bribes, address _flux, address _token) { @@ -102,10 +124,12 @@ contract Voter is IVoter { return (IVotingEscrow(veALCX).balanceOfToken(_tokenId) * boostMultiplier) / BPS; } + /// @inheritdoc IVoter function length() external view returns (uint256) { return pools.length; } + /// @inheritdoc IVoter function getPoolVote(uint256 _tokenId) external view returns (address[] memory) { return poolVote[_tokenId]; } @@ -114,6 +138,7 @@ contract Voter is IVoter { External functions */ + /// @inheritdoc IVoter function setMinter(address _minter) external { require(msg.sender == admin, "not admin"); require(_minter != address(0), "FluxToken: minter cannot be zero address"); @@ -132,6 +157,7 @@ contract Voter is IVoter { emit AdminUpdated(pendingAdmin); } + /// @inheritdoc IVoter function setEmergencyCouncil(address _council) public { require(msg.sender == emergencyCouncil, "not emergency council"); require(_council != address(0), "cannot be zero address"); @@ -139,6 +165,7 @@ contract Voter is IVoter { emit EmergencyCouncilUpdated(_council); } + /// @inheritdoc IVoter function swapReward(address gaugeAddress, uint256 tokenIndex, address oldToken, address newToken) external { require(msg.sender == admin); IBribe(bribes[gaugeAddress]).swapOutRewardToken(tokenIndex, oldToken, newToken); @@ -293,26 +320,31 @@ contract Voter is IVoter { emit NotifyReward(msg.sender, base, amount); } + /// @inheritdoc IVoter function updateFor(address[] memory _gauges) external { for (uint256 i = 0; i < _gauges.length; i++) { _updateFor(_gauges[i]); } } + /// @inheritdoc IVoter function updateForRange(uint256 start, uint256 end) public { for (uint256 i = start; i < end; i++) { _updateFor(gauges[pools[i]]); } } + /// @inheritdoc IVoter function updateAll() external { updateForRange(0, pools.length); } + /// @inheritdoc IVoter function updateGauge(address _gauge) external { _updateFor(_gauge); } + /// @inheritdoc IVoter function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external { require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId)); diff --git a/src/VotingEscrow.sol b/src/VotingEscrow.sol index c7dfb60..77264b5 100644 --- a/src/VotingEscrow.sol +++ b/src/VotingEscrow.sol @@ -28,9 +28,13 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { string public constant version = "1.0.0"; uint8 public immutable decimals = 18; + /// @notice Duration of an epoch uint256 public constant EPOCH = 2 weeks; + /// @notice Maximum number of delegates a token can have uint256 public constant MAX_DELEGATES = 1024; // avoid too much gas + /// @notice Maximum time a lock can be set for uint256 public constant MAXTIME = 365 days; + /// @notice Multiplier for the slope of the decay uint256 public constant MULTIPLIER = 2; uint256 internal immutable WEEK = 1 weeks; @@ -42,31 +46,53 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { /// @dev Current count of token uint256 internal tokenId; + /// @notice address of ALCX token address public immutable ALCX; + /// @notice address of FLUX token address public immutable FLUX; + /// @notice address of BPT token (ALCX-BPT LP token) address public immutable BPT; - address public rewardPoolManager; // destination for BPT - address public admin; // the timelock executor - address public pendingAdmin; // the timelock executor + /// @notice address of the reward pool manager contract + address public rewardPoolManager; + /// @notice address of the admin + address public admin; + /// @notice address of the pending admin + address public pendingAdmin; + /// @notice address of the voter contract address public voter; + /// @notice address of the rewards distributor contract address public distributor; + /// @notice address of the treasury address public treasury; + /// @notice total supply of BPT locked in veALCX uint256 public supply; - uint256 public claimFeeBps = 5000; // Fee for claiming early in bps - uint256 public fluxMultiplier; // Multiplier for flux reward accrual - uint256 public fluxPerVeALCX; // Percent of veALCX power needed in flux in order to unlock early + /// @notice fee for claiming without compounding + uint256 public claimFeeBps = 5000; + /// @notice Determines amount of time it will take to accrue the FLUX needed to unlock early for a max locked token + uint256 public fluxMultiplier; + /// @notice Amount of FLUX a veALCX holder can claim per epoch + uint256 public fluxPerVeALCX; + /// @notice epoch counter uint256 public epoch; + /// @notice Returns the LockedBalance of a tokenId mapping(uint256 => LockedBalance) public locked; + /// @notice Sets the block number for a tokenId when there is an ownership change mapping(uint256 => uint256) public ownershipChange; - mapping(uint256 => Point) public pointHistory; // epoch -> unsigned point - mapping(uint256 => Point[1000000000]) public userPointHistory; // user -> Point[userEpoch] - mapping(uint256 => uint256) public userFirstEpoch; // user -> epoch + /// @notice Mapping of epoch -> unsigned point + mapping(uint256 => Point) public pointHistory; + /// @notice Mapping of user -> Point[userEpoch] + mapping(uint256 => Point[1000000000]) public userPointHistory; + /// @notice Mapping of user -> epoch + mapping(uint256 => uint256) public userFirstEpoch; + /// @notice Mapping of user -> point mapping(uint256 => uint256) public userPointEpoch; - mapping(uint256 => int256) public slopeChanges; // time -> signed slope change - mapping(uint256 => uint256) public attachments; + /// @notice Mapping of time -> signed slope change + mapping(uint256 => int256) public slopeChanges; + /// @notice Mapping of token to whether the token has voted mapping(uint256 => bool) public voted; + /// @notice Mapping of token to whether the token is a reward pool token mapping(address => bool) public isRewardPoolToken; /// @dev Mapping from token ID to the address that owns it. @@ -170,6 +196,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { return locked[_tokenId].end; } + /// @inheritdoc IVotingEscrow function isMaxLocked(uint256 _tokenId) public view returns (bool) { return locked[_tokenId].maxLockEnabled; } @@ -184,10 +211,12 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { return locked[_tokenId].cooldown; } + /// @inheritdoc IVotingEscrow function getPointHistory(uint256 _loc) external view returns (Point memory) { return pointHistory[_loc]; } + /// @inheritdoc IVotingEscrow function getUserPointHistory(uint256 _tokenId, uint256 _loc) external view returns (Point memory) { return userPointHistory[_tokenId][_loc]; } @@ -220,26 +249,19 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { return ownerToTokenIdList[_owner][_tokenIndex]; } + /// @inheritdoc IVotingEscrow function isApprovedOrOwner(address _spender, uint256 _tokenId) external view returns (bool) { return _isApprovedOrOwner(_spender, _tokenId); } - /** - * @notice Overrides the standard `Comp.sol` delegates mapping to return - * the delegator's own address if they haven't delegated. - * This avoids having to delegate to oneself. - */ - function delegates(address delegator) public view returns (address) { + /// @inheritdoc IVotingEscrow + function delegates(address delegator) public view override(IVotes, IVotingEscrow) returns (address) { address current = _delegates[delegator]; return current == address(0) ? delegator : current; } - /** - * @notice Gets the current votes balance for `account` - * @param account The address to get votes balance - * @return The number of current votes for `account` - */ - function getVotes(address account) external view returns (uint256) { + /// @inheritdoc IVotingEscrow + function getVotes(address account) external view override(IVotes, IVotingEscrow) returns (uint256) { uint32 nCheckpoints = numCheckpoints[account]; if (nCheckpoints == 0) { return 0; @@ -254,6 +276,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { return votes; } + /// @inheritdoc IVotingEscrow function getPastVotesIndex(address account, uint256 timestamp) public view returns (uint32) { uint32 nCheckpoints = numCheckpoints[account]; if (nCheckpoints == 0) { @@ -290,10 +313,11 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { return lower; } - /** - * @dev Returns the amount of votes that `account` had at the end of a past `timestamp`. - */ - function getPastVotes(address account, uint256 timestamp) public view returns (uint256) { + /// @inheritdoc IVotingEscrow + function getPastVotes( + address account, + uint256 timestamp + ) public view override(IVotes, IVotingEscrow) returns (uint256) { uint32 _checkIndex = getPastVotesIndex(account, timestamp); uint256 votes = 0; @@ -312,7 +336,8 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { return votes; } - function getPastTotalSupply(uint256 timestamp) external view returns (uint256) { + /// @inheritdoc IVotingEscrow + function getPastTotalSupply(uint256 timestamp) external view override(IVotes, IVotingEscrow) returns (uint256) { return totalSupplyAtT(timestamp); } @@ -330,21 +355,20 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { return ragequitAmount; } - /** - * @notice Returns current token URI metadata - * @param _tokenId ID of the token to fetch URI for. - */ - function tokenURI(uint256 _tokenId) external view returns (string memory) { + /// @inheritdoc IVotingEscrow + function tokenURI(uint256 _tokenId) external view override(IERC721Metadata, IVotingEscrow) returns (string memory) { require(idToOwner[_tokenId] != address(0), "Query for nonexistent token"); LockedBalance memory _locked = locked[_tokenId]; return _tokenURI(_tokenId, _balanceOfTokenAt(_tokenId, block.timestamp), _locked.end, _locked.amount); } + /// @inheritdoc IVotingEscrow function balanceOfToken(uint256 _tokenId) external view returns (uint256) { if (ownershipChange[_tokenId] == block.number) return 0; return _balanceOfTokenAt(_tokenId, block.timestamp); } + /// @inheritdoc IVotingEscrow function balanceOfTokenAt(uint256 _tokenId, uint256 _time) external view returns (uint256) { return _balanceOfTokenAt(_tokenId, _time); } @@ -525,33 +549,39 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { revert("function not supported"); } + /// @inheritdoc IVotingEscrow function setVoter(address _voter) external { require(msg.sender == admin, "not admin"); voter = _voter; emit VoterUpdated(_voter); } + /// @inheritdoc IVotingEscrow function setRewardsDistributor(address _distributor) external { require(msg.sender == admin, "not admin"); distributor = _distributor; emit RewardsDistributorUpdated(_distributor); } + /// @inheritdoc IVotingEscrow function setRewardPoolManager(address _rewardPoolManager) external { require(msg.sender == admin, "not admin"); rewardPoolManager = _rewardPoolManager; } + /// @inheritdoc IVotingEscrow function voting(uint256 _tokenId) external { require(msg.sender == voter); voted[_tokenId] = true; } + /// @inheritdoc IVotingEscrow function abstain(uint256 _tokenId) external { require(msg.sender == voter); voted[_tokenId] = false; } + /// @inheritdoc IVotingEscrow function setfluxMultiplier(uint256 _fluxMultiplier) external { require(msg.sender == admin, "not admin"); require(_fluxMultiplier > 0, "fluxMultiplier must be greater than 0"); @@ -570,20 +600,23 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { emit AdminUpdated(pendingAdmin); } + /// @inheritdoc IVotingEscrow function setfluxPerVeALCX(uint256 _fluxPerVeALCX) external { require(msg.sender == admin, "not admin"); fluxPerVeALCX = _fluxPerVeALCX; emit FluxPerVeALCXUpdated(_fluxPerVeALCX); } + /// @inheritdoc IVotingEscrow function setClaimFee(uint256 _claimFeeBps) external { require(msg.sender == admin, "not admin"); claimFeeBps = _claimFeeBps; emit ClaimFeeUpdated(_claimFeeBps); } + /// @inheritdoc IVotingEscrow function merge(uint256 _from, uint256 _to) external { - require(attachments[_from] == 0 && !voted[_from], "attached"); + require(!voted[_from], "voting in progress for token"); require(_from != _to, "must be different tokens"); require(_isApprovedOrOwner(msg.sender, _from)); require(_isApprovedOrOwner(msg.sender, _to)); @@ -707,7 +740,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { */ function withdraw(uint256 _tokenId) public nonreentrant { require(_isApprovedOrOwner(msg.sender, _tokenId)); - require(attachments[_tokenId] == 0 && !voted[_tokenId], "attached"); + require(!voted[_tokenId], "voting in progress for token"); LockedBalance memory _locked = locked[_tokenId]; @@ -891,7 +924,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { */ function _transferFrom(address _from, address _to, uint256 _tokenId, address _sender) internal { require(_to != address(0), "to address is zero address"); - require(attachments[_tokenId] == 0 && !voted[_tokenId], "attached"); + require(!voted[_tokenId], "voting in progress for token"); require(_isApprovedOrOwner(_sender, _tokenId)); require(idToOwner[_tokenId] == _from, "from address is not owner"); diff --git a/src/factories/BribeFactory.sol b/src/factories/BribeFactory.sol index 569bc0f..76a01b4 100644 --- a/src/factories/BribeFactory.sol +++ b/src/factories/BribeFactory.sol @@ -3,6 +3,9 @@ pragma solidity ^0.8.15; import "src/Bribe.sol"; +/// @title Bribe Factory +/// @notice Factory to create bribe contracts +/// @dev Voter will call createBribe for each new gauge created contract BribeFactory { function createBribe() external returns (address) { return address(new Bribe(msg.sender)); diff --git a/src/factories/GaugeFactory.sol b/src/factories/GaugeFactory.sol index 2c3883a..ee82f7e 100644 --- a/src/factories/GaugeFactory.sol +++ b/src/factories/GaugeFactory.sol @@ -4,6 +4,11 @@ pragma solidity ^0.8.15; import "src/gauges/PassthroughGauge.sol"; import "src/gauges/CurveGauge.sol"; +/// @title Gauge Factory +/// @notice Factory to create new gauge contracts +/// @dev Voter will call createGauge to create a new gauge +/// @dev CurveGauge is an exmaple of a gauge extending _passthroughRewards logic +/// @dev Passthrough Gauges will relay rewards to a receiver address (ex: Sushi Pool) contract GaugeFactory { function createCurveGauge(address _bribe, address _ve) external returns (address) { return address(new CurveGauge(_bribe, _ve, msg.sender)); diff --git a/src/gauges/CurveGauge.sol b/src/gauges/CurveGauge.sol index c1d5ec7..1461255 100644 --- a/src/gauges/CurveGauge.sol +++ b/src/gauges/CurveGauge.sol @@ -17,16 +17,16 @@ import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; contract CurveGauge is BaseGauge { using SafeERC20 for IERC20; - // Votium pool index (subject to change) + /// @notice Votium pool index (subject to change) uint256 poolIndex; - // Proposal id from snapshot url + /// @notice Proposal id from snapshot url bytes32 proposal; - // Flag to determine if proposal has been updated + /// @notice Flag to determine if proposal has been updated bool proposalUpdated; - // Flag to determine if gauge has been setup with necessary variables + /// @notice Flag to determine if gauge has been setup with necessary variables bool initialized; event ProposalUpdated(bytes32 indexed newProposal, bool proposalUpdated); @@ -94,7 +94,7 @@ contract CurveGauge is BaseGauge { */ /** - * @notice Pass rewards to votium contract + * @notice Logic to pass rewards to votium contract * @param _amount Amount of rewards */ function _passthroughRewards(uint256 _amount) internal override { diff --git a/src/interfaces/IBaseGauge.sol b/src/interfaces/IBaseGauge.sol index 4e6e07b..21514c0 100644 --- a/src/interfaces/IBaseGauge.sol +++ b/src/interfaces/IBaseGauge.sol @@ -54,9 +54,22 @@ interface IBaseGauge { function acceptAdmin() external; + /** + * @notice Get the current voting stage + * @param timestamp The timestamp to check the voting stage + */ + function getVotingStage(uint256 timestamp) external pure returns (VotingStage); + /** * @notice Distribute the appropriate rewards to a gauge - * @param amount The amount of rewards being sent + * @dev This function may require different implementation depending on destination + * @param amount The amount of rewards being sent */ function notifyRewardAmount(uint256 amount) external; + + /** + * @notice Update the receiver address + * @param _receiver The destination address of the rewards + */ + function updateReceiver(address _receiver) external; } diff --git a/src/interfaces/IBribe.sol b/src/interfaces/IBribe.sol index edc6121..7e9350f 100644 --- a/src/interfaces/IBribe.sol +++ b/src/interfaces/IBribe.sol @@ -118,12 +118,32 @@ interface IBribe { */ function swapOutRewardToken(uint256 i, address oldToken, address newToken) external; + /** + * @notice Get the total amount of votes for a given epoch + * @param timestamp The timestamp to check the votes + * @return uint256 The total amount of votes + */ function getPriorVotingIndex(uint256 timestamp) external view returns (uint256); + /** + * @notice Amount of bribes earned by an account for a given reward token + * @param token Address of the reward token + * @return uint256 veALCX tokenId + */ function earned(address token, uint256 tokenId) external view returns (uint256); + /** + * @notice Called by voter to allocate votes to a given gauge + * @param amount Amount of votes to allocate + * @param tokenId veALCX tokenId + */ function deposit(uint256 amount, uint256 tokenId) external; + /** + * @notice Called by voter to withdraw votes from a gauge + * @param amount Amount of votes to withdraw + * @param tokenId veALCX tokenId + */ function withdraw(uint256 amount, uint256 tokenId) external; /** diff --git a/src/interfaces/IFluxToken.sol b/src/interfaces/IFluxToken.sol index d86d969..1bb4514 100644 --- a/src/interfaces/IFluxToken.sol +++ b/src/interfaces/IFluxToken.sol @@ -104,4 +104,16 @@ interface IFluxToken is IERC20 { * @param _nft Which NFT to calculate claimable flux for */ function getClaimableFlux(uint256 _amount, address _nft) external view returns (uint256); + + /** + * @notice Set the multiplier of FLUX alchemech NFT holders receive + * @param _nftMultiplier Multiplier to set + */ + function setNftMultiplier(uint256 _nftMultiplier) external; + + /** + * @notice Set the multiplier of FLUX patron NFT holders receive + * @param _bptMultiplier Multiplier to set + */ + function setBptMultiplier(uint256 _bptMultiplier) external; } diff --git a/src/interfaces/IMinter.sol b/src/interfaces/IMinter.sol index bf536f8..ae5e1c5 100644 --- a/src/interfaces/IMinter.sol +++ b/src/interfaces/IMinter.sol @@ -44,6 +44,12 @@ interface IMinter { */ event TreasuryUpdated(address treasury); + /** + * @notice Set the treasury address + * @param _treasury Address of the treasury + */ + function setTreasury(address _treasury) external; + /** * @notice Sets the emissions rate of rewards sent to veALCX stakers * @param _veAlcxEmissionsRate The rate in BPS diff --git a/src/interfaces/IPoolAdapter.sol b/src/interfaces/IPoolAdapter.sol index a532b81..c36a1bf 100644 --- a/src/interfaces/IPoolAdapter.sol +++ b/src/interfaces/IPoolAdapter.sol @@ -2,7 +2,30 @@ pragma solidity ^0.8.15; interface IPoolAdapter { + /** + * @notice Get the address of the pool + */ function pool() external view returns (address); + + /** + * @notice Get the amount of output token for a given input token and amount + * @param inputToken input token address + * @param outputToken output token address + * @param inputAmount input token amount + */ function getDy(address inputToken, address outputToken, uint256 inputAmount) external view returns (uint256); - function melt(address inputToken, address outputToken, uint256 inputAmount, uint256 minimumAmountOut) external returns (uint256); -} \ No newline at end of file + + /** + * @notice Melt the input token to the output token + * @param inputToken input token address + * @param outputToken output token address + * @param inputAmount input token amount + * @param minimumAmountOut minimum output token amount + */ + function melt( + address inputToken, + address outputToken, + uint256 inputAmount, + uint256 minimumAmountOut + ) external returns (uint256); +} diff --git a/src/interfaces/IRewardsDistributor.sol b/src/interfaces/IRewardsDistributor.sol index 3a7722b..86866b4 100644 --- a/src/interfaces/IRewardsDistributor.sol +++ b/src/interfaces/IRewardsDistributor.sol @@ -24,8 +24,14 @@ interface IRewardsDistributor { */ event Claimed(uint256 tokenId, uint256 amount, uint256 claimEpoch, uint256 maxEpoch); + /** + * @notice Checkpoint token balance minted in rewards distributor + */ function checkpointToken() external; + /** + * @notice Checkpoint supply + */ function checkpointTotalSupply() external; /** diff --git a/src/interfaces/IVoter.sol b/src/interfaces/IVoter.sol index 5c590ab..4be043a 100644 --- a/src/interfaces/IVoter.sol +++ b/src/interfaces/IVoter.sol @@ -10,9 +10,7 @@ interface IVoter { event Abstained(address indexed voter, address indexed pool, uint256 tokenId, uint256 weight); event AdminUpdated(address newAdmin); - event Attach(address indexed owner, address indexed gauge, uint256 tokenId); event Deposit(address indexed account, address indexed gauge, uint256 tokenId, uint256 amount); - event Detach(address indexed owner, address indexed gauge, uint256 tokenId); event DistributeReward(address indexed sender, address indexed gauge, uint256 amount); event EmergencyCouncilUpdated(address newCouncil); event GaugeCreated(address indexed gauge, address creator, address indexed bribe, address indexed pool); @@ -25,6 +23,7 @@ interface IVoter { event Whitelisted(address indexed whitelister, address indexed token); event RemovedFromWhitelist(address indexed whitelister, address indexed token); + /// @notice VeALCX contract address function veALCX() external view returns (address); function admin() external view returns (address); @@ -35,8 +34,6 @@ interface IVoter { function isWhitelisted(address token) external view returns (bool); - function getPoolVote(uint256 tokenId) external view returns (address[] memory); - /** * @notice Whitelist a token to be a permitted bribe token * @param _token address of the token @@ -63,6 +60,38 @@ interface IVoter { */ function maxFluxBoost(uint256 _tokenId) external view returns (uint256); + /** + * @notice get the pool vote for a given tokenId + * @param _tokenId ID of the veALCX token + */ + function getPoolVote(uint256 _tokenId) external view returns (address[] memory); + + /** + * @notice get the length of the pools array + */ + function length() external view returns (uint256); + + /** + * @notice Set the Minter contract address + * @param _minter address of the Minter contract + */ + function setMinter(address _minter) external; + + /** + * @notice Set the Emergency Council address + * @param _council address of the Emergency Council contract + */ + function setEmergencyCouncil(address _council) external; + + /** + * @notice Swap a new reward token for an old reward token + * @param gaugeAddress Address of the gauge to swap reward for + * @param tokenIndex Current index of the reward token in the rewards array + * @param oldToken The old token reward address + * @param newToken The new token reward address + */ + function swapReward(address gaugeAddress, uint256 tokenIndex, address oldToken, address newToken) external; + /** * @notice Set the max veALCX voting power can be boosted by with flux * @param _boostMultiplier BPS of boost @@ -120,4 +149,36 @@ interface IVoter { * @notice Distribute rewards and bribes to all gauges */ function distribute() external; + + /** + * @notice Update the rewards and voting of a list of gauges + * @param _gauges Array of gauge addresses to update + */ + function updateFor(address[] memory _gauges) external; + + /** + * @notice Update a list of gauges given a range of pool indexes + * @param start start index + * @param end end index + */ + function updateForRange(uint256 start, uint256 end) external; + + /** + * @notice Update all gauges in the pools array + */ + function updateAll() external; + + /** + * @notice Update the gauge for a specific pool + * @param _gauge Address of the gauge to update + */ + function updateGauge(address _gauge) external; + + /** + * @notice Claim the bribes for a given token id + * @param _bribes Array of bribe addresses to claim from + * @param _tokens Array of bribe addresses and coenciding token addresses to claim + * @param _tokenId ID of the veALCX token to claim bribes for + */ + function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external; } diff --git a/src/interfaces/IVotingEscrow.sol b/src/interfaces/IVotingEscrow.sol index 85abbbb..7642754 100644 --- a/src/interfaces/IVotingEscrow.sol +++ b/src/interfaces/IVotingEscrow.sol @@ -86,6 +86,28 @@ interface IVotingEscrow { function epoch() external view returns (uint256); + /** + * @notice Set the flux multiplier value (ex: 4) + * @param _fluxMultiplier Value to set + * @dev Represents a multiplier of the max lock time it will take to accrue the FLUX needed to unlock early + * @dev Example: 4x multipler with 1 year max lock will take 4 years to accrue the FLUX needed to unlock early + */ + function setfluxMultiplier(uint256 _fluxMultiplier) external; + + /** + * @notice Set the flux per veALCX value + * @param _fluxPerVeALCX Value to set + * @dev This is the amount of FLUX a veALCX holder can claim per epoch + */ + function setfluxPerVeALCX(uint256 _fluxPerVeALCX) external; + + /** + * @notice Set the claim fee for claiming ALCX rewards early + * @param _claimFeeBps Basis points of the claim fee + * @dev Example: With 5000 claim fee 100 ALCX reward will result in 50 ALCX fee + */ + function setClaimFee(uint256 _claimFeeBps) external; + /** * @notice Get timestamp when `_tokenId`'s lock finishes * @param tokenId ID of the token @@ -114,8 +136,17 @@ interface IVotingEscrow { */ function cooldownEnd(uint256 tokenId) external view returns (uint256); + /** + * @notice Get the point history for a given epoch + * @param loc Epoch number + */ function getPointHistory(uint256 loc) external view returns (Point memory); + /** + * @notice Get the point history for a given user + * @param tokenId ID of the token + * @param loc Epoch number + */ function getUserPointHistory(uint256 tokenId, uint256 loc) external view returns (Point memory); /** @@ -170,10 +201,69 @@ interface IVotingEscrow { */ function isApprovedForAll(address _owner, address _operator) external view returns (bool); - function isApprovedOrOwner(address, uint256) external view returns (bool); + /** + * @notice Return if an address is either the owner or approved delegate for a token + * @param _address Address to check + * @param _tokenId ID of the token to check + */ + function isApprovedOrOwner(address _address, uint256 _tokenId) external view returns (bool); + /** + * @notice Set the address of the voter contract + * @param voter Address of the voter contract + */ function setVoter(address voter) external; + /** + * @notice Set the address of the rewards distributor contract + * @param _distributor Address of the rewards distributor contract + */ + function setRewardsDistributor(address _distributor) external; + + /** + * @notice Set the address of the reward pool manager contract + * @param _rewardPoolManager Address of the reward pool manager contract + */ + function setRewardPoolManager(address _rewardPoolManager) external; + + /** + * @notice Overrides the standard `Comp.sol` delegates mapping to return + * the delegator's own address if they haven't delegated. + * This avoids having to delegate to oneself. + */ + function delegates(address delegator) external view returns (address); + + /** + * @notice Gets the current votes balance for `account` + * @param account The address to get votes balance + * @return The number of current votes for `account` + */ + function getVotes(address account) external view returns (uint256); + + /** + * @notice Gets the current votes balance for `account` at a specific timestamp + * @param account The address to get votes balance + * @param timestamp The timestamp to get votes balance at + * @return The number of current votes for `account` + */ + function getPastVotesIndex(address account, uint256 timestamp) external view returns (uint32); + + /** + * @dev Returns the amount of votes that `account` had at the end of a past `timestamp`. + */ + function getPastVotes(address account, uint256 timestamp) external view returns (uint256); + + /** + * @notice Get the total supply at a given timestamp + */ + function getPastTotalSupply(uint256 timestamp) external view returns (uint256); + + /** + * @notice Returns current token URI metadata + * @param _tokenId ID of the token to fetch URI for. + */ + function tokenURI(uint256 _tokenId) external view returns (string memory); + /** * @dev Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for this token. * Throws if `_from` is not the current owner. @@ -187,8 +277,18 @@ interface IVotingEscrow { */ function transferFrom(address _from, address _to, uint256 _tokenId) external; + /** + * @notice Set a token to currently be voting + * @param tokenId ID of the token + * @dev Can only be called by the voter contract + */ function voting(uint256 tokenId) external; + /** + * @notice Set a token to currently be abstaining + * @param tokenId ID of the token + * @dev Can only be called by the voter contract + */ function abstain(uint256 tokenId) external; /** @@ -213,8 +313,19 @@ interface IVotingEscrow { */ function depositFor(uint256 tokenId, uint256 value) external; + /** + * @notice Voting power of a given tokenId + * @param tokenId ID of the token + * @return uint256 Voting power of the token + */ function balanceOfToken(uint256 tokenId) external view returns (uint256); + /** + * @notice Voting power of a given tokenId at a given time + * @param _tokenId ID of the token + * @param _time Timestamp to check balance at + * @return uint256 Voting power of the token + */ function balanceOfTokenAt(uint256 _tokenId, uint256 _time) external view returns (uint256); /** @@ -254,4 +365,11 @@ interface IVotingEscrow { * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility */ function totalSupplyAtT(uint256 t) external view returns (uint256); + + /** + * @notice Merge two veALCX tokens + * @param _from ID of the token to merge from (will be burned) + * @param _to ID of the token to merge into + */ + function merge(uint256 _from, uint256 _to) external; } From 3e872752358b764edd550a7aeec1991b16538827 Mon Sep 17 00:00:00 2001 From: toyvo Date: Tue, 16 Apr 2024 11:00:18 -0400 Subject: [PATCH 15/16] ignore docs file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c5b3ad0..6f362be 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ out/ report/ lcov.info notes.md -.gas-snapshot \ No newline at end of file +.gas-snapshot +/docs \ No newline at end of file From 9e14da88d8db05794623d8ab5f449451a10c15ac Mon Sep 17 00:00:00 2001 From: toyvo <94640047+toyv0@users.noreply.github.com> Date: Thu, 18 Apr 2024 23:00:45 -0400 Subject: [PATCH 16/16] test coverage (#196) * test coverage * updated test summary --- Makefile | 18 +- src/BaseGauge.sol | 55 ++--- src/Bribe.sol | 30 +-- src/FluxToken.sol | 4 +- src/RewardsDistributor.sol | 4 +- src/Voter.sol | 38 +--- src/VotingEscrow.sol | 29 +-- src/gauges/PassthroughGauge.sol | 9 +- src/interfaces/IBaseGauge.sol | 6 - src/interfaces/IVoter.sol | 18 -- src/test/AlchemixGovernor.t.sol | 24 +++ src/test/FluxToken.t.sol | 143 +++++++++++++ src/test/Minter.t.sol | 19 ++ src/test/RevenueHandler.t.sol | 38 ++++ src/test/RewardPoolManager.t.sol | 167 +++++++++++++++ src/test/Voting.t.sol | 349 ++++++++++++++++++++++++++++++- src/test/VotingEscrow.t.sol | 152 +++++++++----- 17 files changed, 913 insertions(+), 190 deletions(-) create mode 100644 src/test/RewardPoolManager.t.sol diff --git a/Makefile b/Makefile index aaac73b..651bf43 100644 --- a/Makefile +++ b/Makefile @@ -74,4 +74,20 @@ test_file_block_debug :; FOUNDRY_PROFILE=$(PROFILE) forge test $(FORK_URL) $(MAT test_file_debug_test :; FOUNDRY_PROFILE=$(PROFILE) forge test $(FORK_URL) $(MATCH_PATH) $(MATCH_TEST) -vvv # runs single test within file with added verbosity for failing test from a given block: "make test_file_block_debug_test FILE=Minter TEST=testUnwrap" -test_file_block_debug_test :; FOUNDRY_PROFILE=$(PROFILE) forge test $(FORK_URL) $(MATCH_PATH) $(MATCH_TEST) $(FORK_BLOCK) -vvv \ No newline at end of file +test_file_block_debug_test :; FOUNDRY_PROFILE=$(PROFILE) forge test $(FORK_URL) $(MATCH_PATH) $(MATCH_TEST) $(FORK_BLOCK) -vvv + +# | File | % Lines | % Statements | % Branches | % Funcs | +# |----------------------------|--------------------|--------------------|------------------|------------------| +# | src/RewardsDistributor.sol | 90.00% (126/140) | 87.44% (174/199) | 69.70% (46/66) | 80.00% (12/15) | +# | src/Bribe.sol | 92.47% (135/146) | 93.44% (171/183) | 78.38% (58/74) | 78.95% (15/19) | +# | src/VotingEscrow.sol | 93.96% (467/497) | 94.13% (577/613) | 77.65% (205/264) | 90.00% (72/80) | +# | src/Voter.sol | 98.86% (173/175) | 99.01% (200/202) | 83.96% (89/106) | 96.67% (29/30) | +# | src/Minter.sol | 100.00% (48/48) | 100.00% (61/61) | 88.46% (23/26) | 100.00% (9/9) | +# | src/FluxToken.sol | 97.06% (66/68) | 97.40% (75/77) | 90.32% (56/62) | 100.00% (19/19) | +# | src/RevenueHandler.sol | 100.00% (94/94) | 100.00% (115/115) | 93.18% (41/44) | 100.00% (15/15) | +# | src/RewardPoolManager.sol | 100.00% (47/47) | 100.00% (55/55) | 94.12% (32/34) | 100.00% (13/13) | +# | src/AlchemixGovernor.sol | 100.00% (16/16) | 100.00% (17/17) | 100.00% (12/12) | 100.00% (6/6) | +# | src/BaseGauge.sol | 100.00% (13/13) | 100.00% (13/13) | 100.00% (14/14) | 80.00% (4/5) | +# | src/BribeFactory.sol | 100.00% (1/1) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (1/1) | +# | src/GaugeFactory.sol | 100.00% (2/2) | 100.00% (4/4) | 100.00% (0/0) | 100.00% (2/2) | +# | src/PassthroughGauge.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (1/1) | \ No newline at end of file diff --git a/src/BaseGauge.sol b/src/BaseGauge.sol index 6bc7261..dbddef0 100644 --- a/src/BaseGauge.sol +++ b/src/BaseGauge.sol @@ -6,6 +6,7 @@ import "src/interfaces/IBribe.sol"; import "src/interfaces/IBaseGauge.sol"; import "src/interfaces/IVoter.sol"; import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title Base Gauge @@ -13,17 +14,27 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; * @notice Gauges are used to incentivize pools, they emit or passthrough reward tokens */ abstract contract BaseGauge is IBaseGauge { - uint256 internal constant DURATION = 2 weeks; // Rewards released over voting period + using SafeERC20 for IERC20; + + /// @notice Rewards released over voting period + uint256 internal constant DURATION = 2 weeks; uint256 internal constant BRIBE_LAG = 1 days; uint256 internal constant MAX_REWARD_TOKENS = 16; - address public ve; // veALCX token used for gauges - address public bribe; // Address of bribe contract - address public voter; // Address of voter contract + /// @notice veALCX token used for gauges + address public ve; + /// @notice Address of bribe contract + address public bribe; + /// @notice Address of voter contract + address public voter; + /// @notice Address of admin address public admin; + /// @notice Address of pending admin address public pendingAdmin; - address public receiver; // Address that receives the ALCX rewards - address public rewardToken; // Address of the reward token + /// @notice Address that receives the ALCX rewards + address public receiver; + /// @notice Address of the reward token + address public rewardToken; // Re-entrancy check uint256 internal _unlocked = 1; @@ -34,22 +45,6 @@ abstract contract BaseGauge is IBaseGauge { _unlocked = 1; } - /* - View functions - */ - - /// @inheritdoc IBaseGauge - function getVotingStage(uint256 timestamp) external pure returns (VotingStage) { - uint256 modTime = timestamp % (1 weeks); - if (modTime < BRIBE_LAG) { - return VotingStage.BribesPhase; - } else if (modTime >= BRIBE_LAG && modTime < (BRIBE_LAG + DURATION)) { - return VotingStage.VotesPhase; - } else { - return VotingStage.RewardsPhase; - } - } - /* External functions */ @@ -76,7 +71,7 @@ abstract contract BaseGauge is IBaseGauge { function notifyRewardAmount(uint256 _amount) external lock { require(msg.sender == voter, "not voter"); require(_amount > 0, "zero amount"); - _safeTransferFrom(rewardToken, msg.sender, address(this), _amount); + IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), _amount); emit NotifyReward(msg.sender, rewardToken, _amount); @@ -87,20 +82,6 @@ abstract contract BaseGauge is IBaseGauge { Internal functions */ - function _safeTransfer(address token, address to, uint256 value) internal { - require(token.code.length > 0); - (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool)))); - } - - function _safeTransferFrom(address token, address from, address to, uint256 value) internal { - require(token.code.length > 0); - (bool success, bytes memory data) = token.call( - abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value) - ); - require(success && (data.length == 0 || abi.decode(data, (bool)))); - } - /** * @notice Override function to implement passthrough logic * @param _amount Amount of rewards diff --git a/src/Bribe.sol b/src/Bribe.sol index 35fb9cb..09411cb 100644 --- a/src/Bribe.sol +++ b/src/Bribe.sol @@ -1,18 +1,21 @@ // SPDX-License-Identifier: GPL-3 pragma solidity ^0.8.15; - +import "lib/forge-std/src/console2.sol"; import "src/interfaces/IBribe.sol"; import "src/interfaces/IBaseGauge.sol"; import "src/interfaces/IVoter.sol"; import "src/interfaces/IVotingEscrow.sol"; import "src/libraries/Math.sol"; import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title Bribe * @notice Implementation of bribe contract to be used with gauges */ contract Bribe is IBribe { + using SafeERC20 for IERC20; + /// @notice Rewards released over voting period uint256 internal constant DURATION = 2 weeks; /// @notice Duration of time when bribes are accepted @@ -107,7 +110,7 @@ contract Bribe is IBribe { /// @inheritdoc IBribe function notifyRewardAmount(address token, uint256 amount) external lock { - require(amount > 0); + require(amount > 0, "reward amount must be greater than 0"); // If the token has been whitelisted by the voter contract, add it to the rewards list require(IVoter(voter).isWhitelisted(token), "bribe tokens must be whitelisted"); @@ -117,7 +120,8 @@ contract Bribe is IBribe { uint256 adjustedTstamp = getEpochStart(block.timestamp); uint256 epochRewards = tokenRewardsPerEpoch[token][adjustedTstamp]; - _safeTransferFrom(token, msg.sender, address(this), amount); + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount; periodFinish[token] = adjustedTstamp + DURATION; @@ -126,7 +130,7 @@ contract Bribe is IBribe { /// @inheritdoc IBribe function addRewardToken(address token) external { - require(msg.sender == gauge); + require(msg.sender == gauge, "not being set by a gauge"); _addRewardToken(token); } @@ -135,7 +139,6 @@ contract Bribe is IBribe { require(msg.sender == voter, "Only voter can execute"); require(IVoter(voter).isWhitelisted(newToken), "New token must be whitelisted"); require(rewards[oldTokenIndex] == oldToken, "Old token mismatch"); - require(newToken != address(0), "New token cannot be zero address"); // Check that the newToken does not already exist in the rewards array for (uint256 i = 0; i < rewards.length; i++) { @@ -193,7 +196,6 @@ contract Bribe is IBribe { if (votingCheckpoints[nCheckpoints - 1].timestamp < timestamp) { return (nCheckpoints - 1); } - // Check implicit zero balance if (votingCheckpoints[0].timestamp > timestamp) { return 0; @@ -291,7 +293,7 @@ contract Bribe is IBribe { _writeCheckpoint(tokenId, balanceOf[tokenId]); - _safeTransfer(tokens[i], _owner, _reward); + IERC20(tokens[i]).safeTransfer(_owner, _reward); emit ClaimRewards(_owner, tokens[i], _reward); } @@ -384,18 +386,4 @@ contract Bribe is IBribe { function _bribeStart(uint256 timestamp) internal pure returns (uint256) { return timestamp - (timestamp % (DURATION)); } - - function _safeTransfer(address token, address to, uint256 value) internal { - require(token.code.length > 0); - (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool)))); - } - - function _safeTransferFrom(address token, address from, address to, uint256 value) internal { - require(token.code.length > 0); - (bool success, bytes memory data) = token.call( - abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value) - ); - require(success && (data.length == 0 || abi.decode(data, (bool)))); - } } diff --git a/src/FluxToken.sol b/src/FluxToken.sol index efc2b9a..9df10b6 100644 --- a/src/FluxToken.sol +++ b/src/FluxToken.sol @@ -213,7 +213,7 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { // Given an amount of eth, calculate how much FLUX it would earn in a year if it were deposited into veALCX function getClaimableFlux(uint256 _amount, address _nft) public view returns (uint256 claimableFlux) { - uint256 bpt = _calculateBPT(_amount); + uint256 bpt = calculateBPT(_amount); uint256 veMul = IVotingEscrow(veALCX).MULTIPLIER(); uint256 veMax = IVotingEscrow(veALCX).MAXTIME(); @@ -229,7 +229,7 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { } } - function _calculateBPT(uint256 _amount) public view returns (uint256 bptOut) { + function calculateBPT(uint256 _amount) public view returns (uint256 bptOut) { bptOut = _amount * bptMultiplier; } } diff --git a/src/RewardsDistributor.sol b/src/RewardsDistributor.sol index 8861571..8c5d7c0 100644 --- a/src/RewardsDistributor.sol +++ b/src/RewardsDistributor.sol @@ -138,7 +138,7 @@ contract RewardsDistributor is IRewardsDistributor, ReentrancyGuard { /// @inheritdoc IRewardsDistributor function checkpointToken() external { - assert(msg.sender == depositor); + require(msg.sender == depositor, "only depositor"); _checkpointToken(); } @@ -211,7 +211,7 @@ contract RewardsDistributor is IRewardsDistributor, ReentrancyGuard { /// @dev Once off event on contract initialize function setDepositor(address _depositor) external { - require(msg.sender == depositor); + require(msg.sender == depositor, "only depositor"); depositor = _depositor; emit DepositorUpdated(_depositor); } diff --git a/src/Voter.sol b/src/Voter.sol index 60074d7..11dad6f 100644 --- a/src/Voter.sol +++ b/src/Voter.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3 pragma solidity ^0.8.15; - +import "lib/forge-std/src/console2.sol"; import "src/interfaces/IBribeFactory.sol"; import "src/interfaces/IBribe.sol"; import "src/interfaces/IBaseGauge.sol"; @@ -28,7 +28,7 @@ contract Voter is IVoter { address public immutable bribefactory; uint256 internal constant BPS = 10_000; - uint256 internal constant MAX_BOOST = 10000; + uint256 internal constant MAX_BOOST = 10_000; uint256 internal constant MIN_BOOST = 0; /// @notice Rewards are released over this duration (2 weeks) uint256 internal constant DURATION = 2 weeks; @@ -167,7 +167,7 @@ contract Voter is IVoter { /// @inheritdoc IVoter function swapReward(address gaugeAddress, uint256 tokenIndex, address oldToken, address newToken) external { - require(msg.sender == admin); + require(msg.sender == admin, "only admin can swap reward tokens"); IBribe(bribes[gaugeAddress]).swapOutRewardToken(tokenIndex, oldToken, newToken); } @@ -231,8 +231,8 @@ contract Voter is IVoter { uint256[] calldata _weights, uint256 _boost ) external onlyNewEpoch(_tokenId) { - require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId)); - require(_poolVote.length == _weights.length); + require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner"); + require(_poolVote.length == _weights.length, "pool vote and weights mismatch"); require(_poolVote.length > 0, "no pools voted for"); require(_poolVote.length <= pools.length, "invalid pools"); require( @@ -251,6 +251,7 @@ contract Voter is IVoter { /// @inheritdoc IVoter function whitelist(address _token) public { require(msg.sender == admin, "not admin"); + require(_token != address(0), "cannot be zero address"); _whitelist(_token); } @@ -262,8 +263,8 @@ contract Voter is IVoter { /// @inheritdoc IVoter function createGauge(address _pool, GaugeType _gaugeType) external returns (address) { + require(msg.sender == admin, "not admin"); require(gauges[_pool] == address(0x0), "exists"); - require(msg.sender == admin, "only admin creates gauges"); address _bribe = IBribeFactory(bribefactory).createBribe(); @@ -327,23 +328,6 @@ contract Voter is IVoter { } } - /// @inheritdoc IVoter - function updateForRange(uint256 start, uint256 end) public { - for (uint256 i = start; i < end; i++) { - _updateFor(gauges[pools[i]]); - } - } - - /// @inheritdoc IVoter - function updateAll() external { - updateForRange(0, pools.length); - } - - /// @inheritdoc IVoter - function updateGauge(address _gauge) external { - _updateFor(_gauge); - } - /// @inheritdoc IVoter function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external { require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId)); @@ -446,8 +430,8 @@ contract Voter is IVoter { require(isAlive[_gauge], "cannot vote for dead gauge"); uint256 _poolWeight = (_weights[i] * totalPower) / _totalVoteWeight; - require(votes[_tokenId][_pool] == 0); - require(_poolWeight != 0); + require(votes[_tokenId][_pool] == 0, "already voted for pool"); + require(_poolWeight != 0, "cannot vote with zero weight"); _updateFor(_gauge); poolVote[_tokenId].push(_pool); @@ -471,13 +455,13 @@ contract Voter is IVoter { } function _whitelist(address _token) internal { - require(!isWhitelisted[_token]); + require(!isWhitelisted[_token], "token already whitelisted"); isWhitelisted[_token] = true; emit Whitelisted(msg.sender, _token); } function _removeFromWhitelist(address _token) internal { - require(isWhitelisted[_token]); + require(isWhitelisted[_token], "token not whitelisted"); isWhitelisted[_token] = false; emit RemovedFromWhitelist(msg.sender, _token); } diff --git a/src/VotingEscrow.sol b/src/VotingEscrow.sol index 77264b5..0fb6515 100644 --- a/src/VotingEscrow.sol +++ b/src/VotingEscrow.sol @@ -501,13 +501,13 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { function approve(address _approved, uint256 _tokenId) public { address owner = idToOwner[_tokenId]; // Throws if `_tokenId` is not a valid token - require(owner != address(0)); + require(owner != address(0), "owner not found"); // Throws if `_approved` is the current owner require(_approved != owner, "Approved is already owner"); // Check requirements bool senderIsOwner = (owner == msg.sender); bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender]; - require(senderIsOwner || senderIsApprovedForAll); + require(senderIsOwner || senderIsApprovedForAll, "sender is not owner or approved"); // Set the approval idToApprovals[_tokenId] = _approved; emit Approval(owner, _approved, _tokenId); @@ -523,7 +523,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { */ function setApprovalForAll(address _operator, bool _approved) external { // Throws if `_operator` is the `msg.sender` - require(_operator != msg.sender); + require(_operator != msg.sender, "operator cannot be sender"); ownerToOperators[msg.sender][_operator] = _approved; emit ApprovalForAll(msg.sender, _operator, _approved); } @@ -533,7 +533,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { * @param delegatee The address to delegate votes to */ function delegate(address delegatee) public { - if (delegatee == address(0)) delegatee = msg.sender; + require(delegatee != address(0), "cannot delegate to zero address"); return _delegate(msg.sender, delegatee); } @@ -571,13 +571,13 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { /// @inheritdoc IVotingEscrow function voting(uint256 _tokenId) external { - require(msg.sender == voter); + require(msg.sender == voter, "not voter"); voted[_tokenId] = true; } /// @inheritdoc IVotingEscrow function abstain(uint256 _tokenId) external { - require(msg.sender == voter); + require(msg.sender == voter, "not voter"); voted[_tokenId] = false; } @@ -618,8 +618,8 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { function merge(uint256 _from, uint256 _to) external { require(!voted[_from], "voting in progress for token"); require(_from != _to, "must be different tokens"); - require(_isApprovedOrOwner(msg.sender, _from)); - require(_isApprovedOrOwner(msg.sender, _to)); + require(_isApprovedOrOwner(msg.sender, _from), "not approved or owner"); + require(_isApprovedOrOwner(msg.sender, _to), "not approved or owner"); LockedBalance memory _locked0 = locked[_from]; LockedBalance memory _locked1 = locked[_to]; @@ -657,8 +657,8 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { /// @inheritdoc IVotingEscrow function updateLock(uint256 _tokenId) external { - require(isMaxLocked(_tokenId), "not max locked"); require(msg.sender == voter, "not voter"); + require(isMaxLocked(_tokenId), "not max locked"); locked[_tokenId].end = ((block.timestamp + MAXTIME) / WEEK) * WEEK; } @@ -712,7 +712,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { * @param _maxLockEnabled Is max lock being enabled */ function updateUnlockTime(uint256 _tokenId, uint256 _lockDuration, bool _maxLockEnabled) external nonreentrant { - require(_isApprovedOrOwner(msg.sender, _tokenId)); + require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner"); LockedBalance memory _locked = locked[_tokenId]; @@ -739,7 +739,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { * @dev Only possible if the lock has expired */ function withdraw(uint256 _tokenId) public nonreentrant { - require(_isApprovedOrOwner(msg.sender, _tokenId)); + require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner"); require(!voted[_tokenId], "voting in progress for token"); LockedBalance memory _locked = locked[_tokenId]; @@ -757,7 +757,10 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { _checkpoint(_tokenId, _locked, LockedBalance(0, 0, false, 0)); // Withdraws BPT from reward pool - require(IRewardPoolManager(rewardPoolManager).withdrawFromRewardPool(value)); + require( + IRewardPoolManager(rewardPoolManager).withdrawFromRewardPool(value), + "withdraw from reward pool failed" + ); require(IERC20(BPT).transfer(ownerOf(_tokenId), value)); @@ -773,7 +776,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { /// @inheritdoc IVotingEscrow function startCooldown(uint256 _tokenId) external { - require(_isApprovedOrOwner(msg.sender, _tokenId)); + require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner"); LockedBalance memory _locked = locked[_tokenId]; diff --git a/src/gauges/PassthroughGauge.sol b/src/gauges/PassthroughGauge.sol index 77dfeac..7be0eda 100644 --- a/src/gauges/PassthroughGauge.sol +++ b/src/gauges/PassthroughGauge.sol @@ -13,6 +13,8 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; * @dev If custom distribution logic is necessary create additional contract */ contract PassthroughGauge is BaseGauge { + using SafeERC20 for IERC20; + constructor(address _receiver, address _bribe, address _ve, address _voter) { receiver = _receiver; bribe = _bribe; @@ -35,10 +37,9 @@ contract PassthroughGauge is BaseGauge { * @param _amount Amount of rewards */ function _passthroughRewards(uint256 _amount) internal override { - uint256 rewardBalance = IERC20(rewardToken).balanceOf(address(this)); - require(rewardBalance >= _amount, "insufficient rewards"); - - _safeTransfer(rewardToken, receiver, _amount); + // Gauge will always have _amount + // It is transfered in BaseGauge.notifyRewardAmount + IERC20(rewardToken).safeTransfer(receiver, _amount); emit Passthrough(msg.sender, rewardToken, _amount, receiver); } diff --git a/src/interfaces/IBaseGauge.sol b/src/interfaces/IBaseGauge.sol index 21514c0..c8fb687 100644 --- a/src/interfaces/IBaseGauge.sol +++ b/src/interfaces/IBaseGauge.sol @@ -54,12 +54,6 @@ interface IBaseGauge { function acceptAdmin() external; - /** - * @notice Get the current voting stage - * @param timestamp The timestamp to check the voting stage - */ - function getVotingStage(uint256 timestamp) external pure returns (VotingStage); - /** * @notice Distribute the appropriate rewards to a gauge * @dev This function may require different implementation depending on destination diff --git a/src/interfaces/IVoter.sol b/src/interfaces/IVoter.sol index 4be043a..8d05f27 100644 --- a/src/interfaces/IVoter.sol +++ b/src/interfaces/IVoter.sol @@ -156,24 +156,6 @@ interface IVoter { */ function updateFor(address[] memory _gauges) external; - /** - * @notice Update a list of gauges given a range of pool indexes - * @param start start index - * @param end end index - */ - function updateForRange(uint256 start, uint256 end) external; - - /** - * @notice Update all gauges in the pools array - */ - function updateAll() external; - - /** - * @notice Update the gauge for a specific pool - * @param _gauge Address of the gauge to update - */ - function updateGauge(address _gauge) external; - /** * @notice Claim the bribes for a given token id * @param _bribes Array of bribe addresses to claim from diff --git a/src/test/AlchemixGovernor.t.sol b/src/test/AlchemixGovernor.t.sol index 81e2ce9..1a798b5 100644 --- a/src/test/AlchemixGovernor.t.sol +++ b/src/test/AlchemixGovernor.t.sol @@ -381,4 +381,28 @@ contract AlchemixGovernorTest is BaseTest { assertFalse(voter.isWhitelisted(usdc)); } + + function testAdminFunctions() public { + address admin = governor.admin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + governor.setAdmin(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + governor.setVotingDelay(20); + + hevm.expectRevert(abi.encodePacked("not admin")); + governor.setVotingPeriod(20); + + hevm.prank(admin); + governor.setAdmin(devmsig); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("not pending admin")); + governor.acceptAdmin(); + + hevm.prank(devmsig); + governor.acceptAdmin(); + assertEq(governor.admin(), devmsig, "admin should be updated"); + } } diff --git a/src/test/FluxToken.t.sol b/src/test/FluxToken.t.sol index 3e234a6..22876a9 100644 --- a/src/test/FluxToken.t.sol +++ b/src/test/FluxToken.t.sol @@ -8,6 +8,80 @@ contract FluxTokenTest is BaseTest { setupContracts(block.timestamp); } + function testAdminFunctionErrors() external { + address admin = flux.admin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setAdmin(devmsig); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("not pending admin")); + flux.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setVoter(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setVeALCX(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setAlchemechNFT(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setPatronNFT(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setNftMultiplier(1); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setBptMultiplier(1); + } + + function testUpdateToZero() external { + address admin = flux.admin(); + address minter = flux.minter(); + + hevm.prank(admin); + flux.setAdmin(devmsig); + + hevm.startPrank(devmsig); + flux.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("FluxToken: voter cannot be zero address")); + flux.setVoter(address(0)); + + hevm.expectRevert(abi.encodePacked("FluxToken: veALCX cannot be zero address")); + flux.setVeALCX(address(0)); + + hevm.expectRevert(abi.encodePacked("FluxToken: alchemechNFT cannot be zero address")); + flux.setAlchemechNFT(address(0)); + + hevm.expectRevert(abi.encodePacked("FluxToken: patronNFT cannot be zero address")); + flux.setPatronNFT(address(0)); + + hevm.expectRevert(abi.encodePacked("FluxToken: nftMultiplier cannot be zero")); + flux.setNftMultiplier(0); + + hevm.expectRevert(abi.encodePacked("FluxToken: bptMultiplier cannot be zero")); + flux.setBptMultiplier(0); + + hevm.stopPrank(); + + hevm.prank(minter); + hevm.expectRevert(abi.encodePacked("FluxToken: minter cannot be zero address")); + flux.setMinter(address(0)); + } + + function testSetMultipliersLimit() external { + hevm.startPrank(admin); + + hevm.expectRevert(abi.encodePacked("FluxToken: nftMultiplier cannot be greater than BPS")); + flux.setNftMultiplier(BPS + 1); + + hevm.expectRevert(abi.encodePacked("FluxToken: bptMultiplier cannot be greater than BPS")); + flux.setBptMultiplier(BPS + 1); + } + function testMintFluxFromNFT() external { uint256 tokenId = 4; address ownerOfPatronNFT = IAlEthNFT(patronNFT).ownerOf(tokenId); @@ -36,6 +110,13 @@ contract FluxTokenTest is BaseTest { assertEq(flux.balanceOf(ownerOfAlchemechNFT), alchemechTotal, "owner should have alchemech flux"); } + function testCalculateBPT() external { + uint256 amount = 1000; + uint256 bptCalculation = flux.calculateBPT(amount); + + assertEq(amount * flux.bptMultiplier(), bptCalculation, "should calculate BPT correctly"); + } + function testMintFluxFromNFTErrors() external { uint256 tokenId = 4; address ownerOfPatronNFT = IAlEthNFT(patronNFT).ownerOf(tokenId); @@ -107,4 +188,66 @@ contract FluxTokenTest is BaseTest { assertEq(unclaimedFluxEnd, amountToRagequit, "should have all unclaimed flux"); } + + function testFluxActions() external { + address bribeAddress = voter.bribes(address(sushiGauge)); + + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, veALCX.MAXTIME(), false); + uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, veALCX.MAXTIME(), false); + + uint256 amount1 = veALCX.claimableFlux(tokenId1); + uint256 amount2 = veALCX.claimableFlux(tokenId2); + + uint256 totalAmount = amount1 + amount2; + + uint256 unclaimedFlux1Start = flux.getUnclaimedFlux(tokenId1); + uint256 unclaimedFlux2Start = flux.getUnclaimedFlux(tokenId2); + + assertEq(unclaimedFlux1Start, 0, "should start with no unclaimed flux"); + assertEq(unclaimedFlux2Start, 0, "should start with no unclaimed flux"); + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + address[] memory bribes = new address[](1); + bribes[0] = address(bribeAddress); + address[][] memory tokens = new address[][](2); + tokens[0] = new address[](1); + tokens[0][0] = bal; + + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + hevm.prank(beef); + voter.vote(tokenId2, pools, weights, 0); + + // Fast forward epochs + hevm.warp(newEpoch()); + + voter.distribute(); + + hevm.expectRevert(abi.encodePacked("not voter")); + flux.updateFlux(tokenId1, amount1); + + hevm.expectRevert(abi.encodePacked("not voter")); + flux.accrueFlux(tokenId1); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("not enough flux")); + flux.updateFlux(tokenId1, TOKEN_100K); + + hevm.expectRevert(abi.encodePacked("not veALCX")); + flux.mergeFlux(tokenId1, tokenId2); + + hevm.prank(address(veALCX)); + flux.mergeFlux(tokenId1, tokenId2); + + uint256 unclaimedFlux1End = flux.getUnclaimedFlux(tokenId1); + uint256 unclaimedFlux2End = flux.getUnclaimedFlux(tokenId2); + + assertEq(unclaimedFlux1End, 0, "should have no unclaimed flux"); + assertEq(unclaimedFlux2End, totalAmount, "should have all unclaimed flux"); + } } diff --git a/src/test/Minter.t.sol b/src/test/Minter.t.sol index a215740..3162b00 100644 --- a/src/test/Minter.t.sol +++ b/src/test/Minter.t.sol @@ -295,6 +295,11 @@ contract MinterTest is BaseTest { hevm.expectRevert(abi.encodePacked("insufficient balance to compound")); distributor.claim(tokenId, true); + + hevm.stopPrank(); + + hevm.expectRevert(abi.encodePacked("not approved")); + distributor.claim(tokenId, true); } // Compound claiming should revert if user doesn't provide enough weth @@ -320,6 +325,9 @@ contract MinterTest is BaseTest { hevm.expectRevert(abi.encodePacked("insufficient balance to compound")); distributor.claim(tokenId, true); + hevm.expectRevert(abi.encodePacked("Value must be 0 if not compounding")); + distributor.claim{ value: 1 ether }(tokenId, false); + distributor.claim{ value: 100 ether }(tokenId, true); assertGt(admin.balance, 0); assertGt(100 ether, admin.balance); @@ -332,12 +340,19 @@ contract MinterTest is BaseTest { hevm.expectRevert(abi.encodePacked("not initializer")); minter.initialize(); + hevm.prank(address(0)); + hevm.expectRevert(abi.encodePacked("already initialized")); + minter.initialize(); + hevm.expectRevert(abi.encodePacked("not admin")); minter.setAdmin(devmsig); hevm.expectRevert(abi.encodePacked("not admin")); minter.setVeAlcxEmissionsRate(1000); + hevm.expectRevert(abi.encodePacked("not voter")); + minter.updatePeriod(); + hevm.prank(admin); minter.setAdmin(devmsig); @@ -347,6 +362,10 @@ contract MinterTest is BaseTest { hevm.startPrank(devmsig); minter.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("cannot be greater than 100%")); + minter.setVeAlcxEmissionsRate(10_000 * 2); + minter.setVeAlcxEmissionsRate(1000); hevm.stopPrank(); diff --git a/src/test/RevenueHandler.t.sol b/src/test/RevenueHandler.t.sol index 38039b9..7908478 100644 --- a/src/test/RevenueHandler.t.sol +++ b/src/test/RevenueHandler.t.sol @@ -312,6 +312,22 @@ contract RevenueHandlerTest is BaseTest { assertEq(balAfter, claimable, "should be equal to amount claimed"); } + function testNotEnoughRevenueToClaim() external { + uint256 revAmt = 1000e18; + uint256 tokenId = _setupClaimableNonAlchemicRevenue(revAmt, bal); + uint256 balBefore = IERC20(bal).balanceOf(address(this)); + + assertEq(balBefore, 0, "should have no bal before claiming"); + + uint256 claimable = revenueHandler.claimable(tokenId, bal); + + hevm.prank(address(revenueHandler)); + IERC20(bal).transfer(admin, claimable); + + hevm.expectRevert(abi.encodePacked("Not enough revenue to claim")); + revenueHandler.claim(tokenId, bal, address(0), claimable, address(this)); + } + function testClaimNonApprovedRevenue() external { uint256 revAmt = 1000e18; uint256 tokenId = _setupClaimableNonAlchemicRevenue(revAmt, aura); @@ -606,4 +622,26 @@ contract RevenueHandlerTest is BaseTest { assertEq(revenueHandler.treasuryPct(), newPct); } } + + function testEnableRevenueToken() external { + address revenueToken = revenueHandler.revenueTokens(0); + + hevm.expectRevert(abi.encodePacked("Token enabled")); + revenueHandler.enableRevenueToken(revenueToken); + + revenueHandler.disableRevenueToken(revenueToken); + + hevm.expectRevert(abi.encodePacked("Token disabled")); + revenueHandler.disableRevenueToken(revenueToken); + + revenueHandler.enableRevenueToken(revenueToken); + } + + function testSetTreasury() external { + hevm.expectRevert(abi.encodePacked("treasury cannot be 0x0")); + revenueHandler.setTreasury(address(0)); + + revenueHandler.setTreasury(admin); + assertEq(revenueHandler.treasury(), admin, "treasury should be admin"); + } } diff --git a/src/test/RewardPoolManager.t.sol b/src/test/RewardPoolManager.t.sol new file mode 100644 index 0000000..59d4f74 --- /dev/null +++ b/src/test/RewardPoolManager.t.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import "./BaseTest.sol"; + +contract RewardPoolManagerTest is BaseTest { + function setUp() public { + setupContracts(block.timestamp); + } + + function testAdminFunctions() public { + address admin = rewardPoolManager.admin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setAdmin(devmsig); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("not pending admin")); + rewardPoolManager.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setTreasury(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setRewardPool(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setPoolToken(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setVeALCX(devmsig); + + hevm.prank(admin); + rewardPoolManager.setAdmin(devmsig); + + hevm.startPrank(devmsig); + rewardPoolManager.acceptAdmin(); + + rewardPoolManager.setTreasury(devmsig); + + rewardPoolManager.setRewardPool(devmsig); + + rewardPoolManager.setPoolToken(address(usdc)); + + rewardPoolManager.setVeALCX(address(minter)); + + hevm.stopPrank(); + } + + function testDepositIntoRewardPoolError() public { + hevm.expectRevert(abi.encodePacked("must be veALCX")); + rewardPoolManager.depositIntoRewardPool(TOKEN_1); + } + + function testWithdrawFromRewardPool() public { + hevm.expectRevert(abi.encodePacked("must be veALCX")); + rewardPoolManager.withdrawFromRewardPool(1000); + } + + // Test depositing, withdrawing from a rewardPool (Aura pool) + function testRewardPool() public { + // Reward pool should be set + assertEq(rewardPool, rewardPoolManager.rewardPool()); + + deal(bpt, address(rewardPoolManager), TOKEN_1); + + // Initial amount of bal and aura rewards earned + uint256 rewardBalanceBefore1 = IERC20(bal).balanceOf(admin); + uint256 rewardBalanceBefore2 = IERC20(aura).balanceOf(admin); + assertEq(rewardBalanceBefore1, 0, "rewardBalanceBefore1 should be 0"); + assertEq(rewardBalanceBefore2, 0, "rewardBalanceBefore2 should be 0"); + + // Initial BPT balance of rewardPoolManager + uint256 amount = IERC20(bpt).balanceOf(address(rewardPoolManager)); + assertEq(amount, TOKEN_1); + + // Deposit BPT balance into rewardPool + hevm.prank(address(veALCX)); + rewardPoolManager.depositIntoRewardPool(amount); + + uint256 amountAfterDeposit = IERC20(bpt).balanceOf(address(rewardPoolManager)); + assertEq(amountAfterDeposit, 0, "full balance should be deposited"); + + uint256 rewardPoolBalance = IRewardPool4626(rewardPool).balanceOf(address(rewardPoolManager)); + assertEq(rewardPoolBalance, amount, "rewardPool balance should equal amount deposited"); + + // Fast forward to accumulate rewards + hevm.warp(block.timestamp + 2 weeks); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.claimRewardPoolRewards(); + + hevm.prank(admin); + rewardPoolManager.claimRewardPoolRewards(); + uint256 rewardBalanceAfter1 = IERC20(bal).balanceOf(address(admin)); + uint256 rewardBalanceAfter2 = IERC20(aura).balanceOf(address(admin)); + + // After claiming rewards admin bal balance should increase + assertGt(rewardBalanceAfter1, rewardBalanceBefore1, "should accumulate bal rewards"); + assertGt(rewardBalanceAfter2, rewardBalanceBefore2, "should accumulate aura rewards"); + + hevm.prank(address(veALCX)); + rewardPoolManager.withdrawFromRewardPool(amount); + + // veALCX BPT balance should equal original amount after withdrawing from rewardPool + uint256 amountAfterWithdraw = IERC20(bpt).balanceOf(address(veALCX)); + assertEq(amountAfterWithdraw, amount, "should equal original amount"); + + // Only rewardPoolManager admin can update rewardPool + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setRewardPool(sushiPoolAddress); + + hevm.prank(admin); + rewardPoolManager.setRewardPool(sushiPoolAddress); + + // Reward pool should update + assertEq(sushiPoolAddress, rewardPoolManager.rewardPool(), "rewardPool not updated"); + } + + function testUpdatingRewardPoolTokens() public { + address admin = rewardPoolManager.admin(); + + address[] memory tokens = new address[](2); + tokens[0] = dai; + tokens[1] = usdt; + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.swapOutRewardPoolToken(0, bal, usdc); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.addRewardPoolTokens(tokens); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.addRewardPoolToken(dai); + + hevm.startPrank(admin); + + hevm.expectRevert(abi.encodePacked("incorrect token")); + rewardPoolManager.swapOutRewardPoolToken(0, dai, usdc); + + rewardPoolManager.swapOutRewardPoolToken(0, bal, usdc); + assertEq(rewardPoolManager.rewardPoolTokens(0), usdc, "rewardPoolTokens[0] should be usdc"); + + rewardPoolManager.addRewardPoolTokens(tokens); + assertEq(rewardPoolManager.rewardPoolTokens(2), dai, "rewardPoolTokens[2] should be dai"); + assertEq(rewardPoolManager.rewardPoolTokens(3), usdt, "rewardPoolTokens[3] should be usdt"); + } + + function testMaxRewardPoolTokens() public { + address[] memory tokens = new address[](8); + tokens[0] = dai; + tokens[1] = usdt; + tokens[2] = usdc; + tokens[3] = bpt; + tokens[4] = time; + tokens[5] = aleth; + tokens[6] = alusd3crv; + tokens[7] = alusd; + + hevm.prank(admin); + rewardPoolManager.addRewardPoolTokens(tokens); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("too many reward pool tokens")); + rewardPoolManager.addRewardPoolToken(beef); + } +} diff --git a/src/test/Voting.t.sol b/src/test/Voting.t.sol index f3ae124..3f1cc5a 100644 --- a/src/test/Voting.t.sol +++ b/src/test/Voting.t.sol @@ -23,6 +23,18 @@ contract VotingTest is BaseTest { voter.distribute(); + deal(address(alcx), address(voter), TOKEN_100K); + + hevm.expectRevert(abi.encodePacked("not voter")); + sushiGauge.notifyRewardAmount(TOKEN_100K); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("zero amount")); + sushiGauge.notifyRewardAmount(0); + + hevm.prank(address(voter)); + sushiGauge.notifyRewardAmount(TOKEN_100K); + uint256 distributorBal2 = alcx.balanceOf(address(distributor)); uint256 voterBal2 = alcx.balanceOf(address(voter)); @@ -83,6 +95,7 @@ contract VotingTest is BaseTest { hevm.startPrank(admin); hevm.expectRevert(abi.encodePacked("no rewards to claim")); voter.claimBribes(bribes, tokens, tokenId); + distributor.claim(tokenId, false); hevm.stopPrank(); @@ -143,6 +156,10 @@ contract VotingTest is BaseTest { function testInvalidGauge() public { uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false); + hevm.prank(voter.admin()); + hevm.expectRevert(abi.encodePacked("exists")); + voter.createGauge(alUsdPoolAddress, IVoter.GaugeType.Passthrough); + hevm.startPrank(admin); uint256 period = minter.activePeriod(); @@ -167,6 +184,39 @@ contract VotingTest is BaseTest { hevm.stopPrank(); } + function testManageGauge() public { + address emergencyCouncil = voter.emergencyCouncil(); + address gaugeAddress = voter.gauges(alUsdPoolAddress); + + bool isGaugeAlive = voter.isAlive(gaugeAddress); + assertEq(isGaugeAlive, true, "gauge should be alive"); + + hevm.expectRevert(abi.encodePacked("not emergency council")); + voter.killGauge(gaugeAddress); + + hevm.prank(emergencyCouncil); + voter.killGauge(gaugeAddress); + + hevm.prank(emergencyCouncil); + hevm.expectRevert(abi.encodePacked("gauge already dead")); + voter.killGauge(gaugeAddress); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not emergency council")); + voter.reviveGauge(gaugeAddress); + + hevm.prank(emergencyCouncil); + hevm.expectRevert(abi.encodePacked("invalid gauge")); + voter.reviveGauge(beef); + + hevm.prank(emergencyCouncil); + voter.reviveGauge(gaugeAddress); + + hevm.prank(emergencyCouncil); + hevm.expectRevert(abi.encodePacked("gauge already alive")); + voter.reviveGauge(gaugeAddress); + } + function testNextEpochVoteOrReset() public { uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -206,10 +256,16 @@ contract VotingTest is BaseTest { // Next epoch hevm.warp(block.timestamp + nextEpoch); - // Resetting succeeds + hevm.stopPrank(); + + // Resetting fails when not approved or owner + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not approved or owner")); voter.reset(tokenId); - hevm.stopPrank(); + // Resetting succeeds + hevm.prank(admin); + voter.reset(tokenId); } // veALCX holders should be able to accrue their unclaimed flux over epochs @@ -402,6 +458,10 @@ contract VotingTest is BaseTest { hevm.expectRevert(abi.encodePacked("bribe tokens must be whitelisted")); IBribe(bribeAddress).notifyRewardAmount(usdc, TOKEN_100K); + // Bribe amount must be greater than 0 + hevm.expectRevert(abi.encodePacked("reward amount must be greater than 0")); + IBribe(bribeAddress).notifyRewardAmount(usdc, 0); + uint256 rewardApplicable = IBribe(bribeAddress).lastTimeRewardApplicable(bal); assertEq(rewardApplicable, block.timestamp, "reward applicable should be current timestamp"); @@ -507,6 +567,45 @@ contract VotingTest is BaseTest { ); } + function testPriorVotingIndexZero() public { + uint256 initialTimestamp = block.timestamp; + + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); + + address bribeAddress = voter.bribes(address(sushiGauge)); + + // Prior index should be zero when nCheckpoints is zero + uint256 priorIndex = IBribe(bribeAddress).getPriorVotingIndex(initialTimestamp); + assertEq(priorIndex, 0, "prior voting index should be 0"); + + uint256 earned = IBribe(bribeAddress).earned(address(alcx), tokenId1); + assertEq(earned, 0, "earned should be 0 when there are no checkpoints for a token"); + + // Add BAL bribes to sushiGauge + createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + address[] memory bribes = new address[](1); + bribes[0] = address(bribeAddress); + address[][] memory tokens = new address[][](2); + tokens[0] = new address[](1); + tokens[0][0] = bal; + + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + // Fast forward epochs + hevm.warp(block.timestamp + nextEpoch * 5); + + // Prior index should be zero when timestamp is before first checkpoint timestamp + uint256 priorIndexNow = IBribe(bribeAddress).getPriorVotingIndex(initialTimestamp - nextEpoch * 5); + assertEq(priorIndexNow, 0, "prior voting index should be 0"); + } + // Test impact of voting on bribes earned function testBribeAccounting() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -1157,6 +1256,11 @@ contract VotingTest is BaseTest { // Mock poking idle tokens to sync voting hevm.stopPrank(); + + // Only admin can poke tokens + hevm.expectRevert(abi.encodePacked("not admin")); + voter.pokeTokens(tokens); + hevm.prank(voter.admin()); voter.pokeTokens(tokens); hevm.startPrank(admin); @@ -1190,7 +1294,62 @@ contract VotingTest is BaseTest { hevm.stopPrank(); } + function testGaugeAdminFunctions() public { + address gaugeAdmin = sushiGauge.admin(); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not admin")); + sushiGauge.setAdmin(beef); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not admin")); + sushiGauge.updateReceiver(beef); + + hevm.prank(gaugeAdmin); + sushiGauge.setAdmin(beef); + + hevm.prank(gaugeAdmin); + hevm.expectRevert(abi.encodePacked("not pending admin")); + sushiGauge.acceptAdmin(); + + hevm.prank(beef); + sushiGauge.acceptAdmin(); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("cannot be zero address")); + sushiGauge.updateReceiver(address(0)); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("same receiver")); + sushiGauge.updateReceiver(sushiPoolAddress); + + hevm.prank(beef); + sushiGauge.updateReceiver(admin); + + address newReceiver = sushiGauge.receiver(); + assertEq(newReceiver, admin, "receiver should be updated"); + } + function testAdminFunctions() public { + hevm.expectRevert(abi.encodePacked("not voter")); + veALCX.updateLock(1); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("not max locked")); + veALCX.updateLock(1); + + hevm.expectRevert(abi.encodePacked("not emergency council")); + voter.setEmergencyCouncil(devmsig); + + address emergencyCouncil = voter.emergencyCouncil(); + + hevm.prank(emergencyCouncil); + hevm.expectRevert(abi.encodePacked("cannot be zero address")); + voter.setEmergencyCouncil(address(0)); + + hevm.prank(emergencyCouncil); + voter.setEmergencyCouncil(devmsig); + hevm.expectRevert(abi.encodePacked("not admin")); voter.setMinter(admin); @@ -1200,6 +1359,15 @@ contract VotingTest is BaseTest { hevm.expectRevert(abi.encodePacked("not admin")); voter.setBoostMultiplier(1000); + hevm.expectRevert(abi.encodePacked("not admin")); + voter.whitelist(dai); + + hevm.expectRevert(abi.encodePacked("not admin")); + voter.removeFromWhitelist(dai); + + hevm.expectRevert(abi.encodePacked("not admin")); + voter.createGauge(alUsdPoolAddress, IVoter.GaugeType.Passthrough); + hevm.prank(address(timelockExecutor)); voter.setAdmin(devmsig); @@ -1209,12 +1377,189 @@ contract VotingTest is BaseTest { hevm.startPrank(devmsig); voter.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("Boost multiplier is out of bounds")); + voter.setBoostMultiplier(10_000 + 1); + voter.setBoostMultiplier(1000); + hevm.expectRevert(abi.encodePacked("cannot be zero address")); + voter.whitelist(address(0)); + + assertEq(voter.isWhitelisted(address(0)), false, "zero address should not be whitelisted"); + + hevm.expectRevert(abi.encodePacked("token not whitelisted")); + voter.removeFromWhitelist(dai); + + voter.whitelist(dai); + + hevm.expectRevert(abi.encodePacked("token already whitelisted")); voter.whitelist(dai); assertEq(voter.isWhitelisted(dai), true, "whitelisting failed"); + voter.removeFromWhitelist(dai); + + assertEq(voter.isWhitelisted(dai), false, "remove whitelisting failed"); + hevm.stopPrank(); } + + function testAddingRewardTokenErrors() public { + address bribeAddress = voter.bribes(address(sushiGauge)); + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("not being set by a gauge")); + IBribe(bribeAddress).addRewardToken(usdt); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("New token must be whitelisted")); + IBribe(bribeAddress).swapOutRewardToken(0, dai, usdt); + + hevm.prank(address(timelockExecutor)); + voter.whitelist(usdt); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("Old token mismatch")); + IBribe(bribeAddress).swapOutRewardToken(0, usdc, usdt); + + hevm.prank(address(sushiGauge)); + IBribe(bribeAddress).addRewardToken(usdt); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("New token already exists")); + IBribe(bribeAddress).swapOutRewardToken(1, usdt, usdt); + } + + function testSwapOutRewardToken() public { + address bribeAddress = voter.bribes(address(sushiGauge)); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("only admin can swap reward tokens")); + voter.swapReward(address(sushiGauge), 0, dai, usdt); + + hevm.prank(address(timelockExecutor)); + voter.whitelist(usdt); + + hevm.prank(address(sushiGauge)); + IBribe(bribeAddress).addRewardToken(usdt); + + assertEq(IBribe(bribeAddress).rewards(0), address(alcx), "reward token should be alcx"); + assertEq(IBribe(bribeAddress).rewards(1), usdt, "reward token should be usdt"); + + hevm.prank(address(timelockExecutor)); + voter.whitelist(dai); + + hevm.prank(address(voter)); + IBribe(bribeAddress).swapOutRewardToken(1, usdt, dai); + + assertEq(IBribe(bribeAddress).rewards(1), dai, "reward token should have updated to be dai"); + } + + function testNotifyRewardAmountNoVotes() public { + deal(address(alcx), address(minter), TOKEN_100K); + + uint256 minterAlcxBalance = IERC20(alcx).balanceOf(address(minter)); + assertEq(minterAlcxBalance, TOKEN_100K, "minter should have balance"); + + uint256 voterAlcxBalance = IERC20(alcx).balanceOf(address(voter)); + assertEq(voterAlcxBalance, 0, "voter should have no balance"); + + hevm.expectRevert(abi.encodePacked("only minter can send rewards")); + voter.notifyRewardAmount(TOKEN_100K); + + hevm.prank(address(minter)); + // Distributing rewards without any votes should revert + hevm.expectRevert(abi.encodePacked("no votes")); + voter.notifyRewardAmount(TOKEN_100K); + + uint256 minterAlcxBalanceAfter = IERC20(alcx).balanceOf(address(minter)); + assertEq(minterAlcxBalanceAfter, TOKEN_100K, "minter should have the same balance"); + } + + function testNotifyRewardAmount() public { + // Create a token and vote to create votes + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, 3 weeks, false); + + uint256[] memory tokens = new uint256[](2); + tokens[0] = tokenId1; + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + deal(address(alcx), address(minter), TOKEN_100K); + + uint256 minterAlcxBalance = IERC20(alcx).balanceOf(address(minter)); + assertEq(minterAlcxBalance, TOKEN_100K, "minter should have balance"); + + uint256 voterAlcxBalance = IERC20(alcx).balanceOf(address(voter)); + assertEq(voterAlcxBalance, 0, "voter should have no balance"); + + hevm.startPrank(address(minter)); + alcx.approve(address(voter), TOKEN_100K); + voter.notifyRewardAmount(TOKEN_100K); + hevm.stopPrank(); + + uint256 minterAlcxBalanceAfter = IERC20(alcx).balanceOf(address(minter)); + assertEq(minterAlcxBalanceAfter, 0, "minter should have distributed its balance"); + + uint256 voterAlcxBalanceAfter = IERC20(alcx).balanceOf(address(voter)); + assertEq(voterAlcxBalanceAfter, TOKEN_100K, "voter should have received balance"); + } + + function testSettingGauge() public { + address bribeAddress = voter.bribes(address(sushiGauge)); + + hevm.expectRevert(abi.encodePacked("gauge already set")); + IBribe(bribeAddress).setGauge(devmsig); + } + + function testVotingErrors() public { + uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false); + + address[] memory pools = new address[](0); + + uint256[] memory weights = new uint256[](2); + weights[0] = 5000; + weights[1] = 5000; + + uint256[] memory weights2 = new uint256[](0); + + address[] memory pools2 = new address[](6); + pools2[0] = sushiPoolAddress; + pools2[1] = sushiPoolAddress; + pools2[2] = sushiPoolAddress; + pools2[3] = sushiPoolAddress; + pools2[4] = sushiPoolAddress; + pools2[5] = sushiPoolAddress; + + uint256[] memory weights3 = new uint256[](6); + weights3[0] = 5000; + weights3[1] = 5000; + weights3[2] = 5000; + weights3[3] = 5000; + weights3[4] = 5000; + weights3[5] = 5000; + + address[] memory gauges = new address[](1); + gauges[0] = address(sushiGauge); + + hevm.expectRevert(abi.encodePacked("not approved or owner")); + voter.vote(tokenId, pools, weights, 0); + + hevm.startPrank(admin); + + hevm.expectRevert(abi.encodePacked("pool vote and weights mismatch")); + voter.vote(tokenId, pools, weights, 0); + + hevm.expectRevert(abi.encodePacked("no pools voted for")); + voter.vote(tokenId, pools, weights2, 0); + + hevm.expectRevert(abi.encodePacked("invalid pools")); + voter.vote(tokenId, pools2, weights3, 0); + } } diff --git a/src/test/VotingEscrow.t.sol b/src/test/VotingEscrow.t.sol index ce39526..6e828a3 100644 --- a/src/test/VotingEscrow.t.sol +++ b/src/test/VotingEscrow.t.sol @@ -45,63 +45,6 @@ contract VotingEscrowTest is BaseTest { hevm.stopPrank(); } - // Test depositing, withdrawing from a rewardPool (Aura pool) - function testRewardPool() public { - // Reward pool should be set - assertEq(rewardPool, rewardPoolManager.rewardPool()); - - deal(bpt, address(rewardPoolManager), TOKEN_1); - - // Initial amount of bal and aura rewards earned - uint256 rewardBalanceBefore1 = IERC20(bal).balanceOf(admin); - uint256 rewardBalanceBefore2 = IERC20(aura).balanceOf(admin); - assertEq(rewardBalanceBefore1, 0, "rewardBalanceBefore1 should be 0"); - assertEq(rewardBalanceBefore2, 0, "rewardBalanceBefore2 should be 0"); - - // Initial BPT balance of rewardPoolManager - uint256 amount = IERC20(bpt).balanceOf(address(rewardPoolManager)); - assertEq(amount, TOKEN_1); - - // Deposit BPT balance into rewardPool - hevm.prank(address(veALCX)); - rewardPoolManager.depositIntoRewardPool(amount); - - uint256 amountAfterDeposit = IERC20(bpt).balanceOf(address(rewardPoolManager)); - assertEq(amountAfterDeposit, 0, "full balance should be deposited"); - - uint256 rewardPoolBalance = IRewardPool4626(rewardPool).balanceOf(address(rewardPoolManager)); - assertEq(rewardPoolBalance, amount, "rewardPool balance should equal amount deposited"); - - // Fast forward to accumulate rewards - hevm.warp(block.timestamp + 2 weeks); - - hevm.prank(admin); - rewardPoolManager.claimRewardPoolRewards(); - uint256 rewardBalanceAfter1 = IERC20(bal).balanceOf(address(admin)); - uint256 rewardBalanceAfter2 = IERC20(aura).balanceOf(address(admin)); - - // After claiming rewards admin bal balance should increase - assertGt(rewardBalanceAfter1, rewardBalanceBefore1, "should accumulate bal rewards"); - assertGt(rewardBalanceAfter2, rewardBalanceBefore2, "should accumulate aura rewards"); - - hevm.prank(address(veALCX)); - rewardPoolManager.withdrawFromRewardPool(amount); - - // veALCX BPT balance should equal original amount after withdrawing from rewardPool - uint256 amountAfterWithdraw = IERC20(bpt).balanceOf(address(veALCX)); - assertEq(amountAfterWithdraw, amount, "should equal original amount"); - - // Only rewardPoolManager admin can update rewardPool - hevm.expectRevert(abi.encodePacked("not admin")); - rewardPoolManager.setRewardPool(sushiPoolAddress); - - hevm.prank(admin); - rewardPoolManager.setRewardPool(sushiPoolAddress); - - // Reward pool should update - assertEq(sushiPoolAddress, rewardPoolManager.rewardPool(), "rewardPool not updated"); - } - function testUpdateLockDuration() public { hevm.startPrank(admin); @@ -376,6 +319,15 @@ contract VotingEscrowTest is BaseTest { assertGt(unclaimedFlux3, unclaimedFlux2, "unclaimed flux should be greater for active voter"); } + function testOnlyDepositorFunctions() public { + // Distributor should be set + hevm.expectRevert(abi.encodePacked("only depositor")); + distributor.setDepositor(beef); + + hevm.expectRevert(abi.encodePacked("only depositor")); + distributor.checkpointToken(); + } + // Voting should not impact amount of ALCX rewards earned function testRewardsClaiming() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -468,6 +420,11 @@ contract VotingEscrowTest is BaseTest { function testMergeTokens() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); uint256 tokenId2 = createVeAlcx(admin, TOKEN_100K, MAXTIME / 2, false); + uint256 tokenId3 = createVeAlcx(beef, TOKEN_100K, MAXTIME / 2, false); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.merge(tokenId1, tokenId2); hevm.startPrank(admin); @@ -500,6 +457,9 @@ contract VotingEscrowTest is BaseTest { hevm.expectRevert(abi.encodePacked("must be different tokens")); veALCX.merge(tokenId1, tokenId1); + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.merge(tokenId1, tokenId3); + veALCX.merge(tokenId1, tokenId2); uint256 unclaimedFluxAfter1 = flux.getUnclaimedFlux(tokenId1); @@ -552,6 +512,12 @@ contract VotingEscrowTest is BaseTest { // Fast forward to lock end of tokenId2 hevm.warp(block.timestamp + THREE_WEEKS); + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.withdraw(tokenId1); + + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.startCooldown(tokenId1); + hevm.startPrank(admin); // Should not be able to withdraw BPT @@ -655,6 +621,9 @@ contract VotingEscrowTest is BaseTest { hevm.prank(address(veALCX)); flux.mint(admin, ragequitAmount); + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.updateUnlockTime(tokenId, 1 days, true); + hevm.startPrank(admin); flux.approve(address(veALCX), ragequitAmount); veALCX.startCooldown(tokenId); @@ -977,4 +946,73 @@ contract VotingEscrowTest is BaseTest { uint256 newVotingPowerBeef = veALCX.getVotes(beef); assertEq(newVotingPowerBeef, balanceOfTokens, "incorrect voting power"); } + + function testAdminFunctions() public { + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setTreasury(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setVoter(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setRewardsDistributor(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setfluxMultiplier(0); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setRewardPoolManager(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setAdmin(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setfluxPerVeALCX(0); + + hevm.expectRevert(abi.encodePacked("not voter")); + veALCX.voting(0); + + hevm.expectRevert(abi.encodePacked("not voter")); + veALCX.abstain(0); + + hevm.prank(admin); + veALCX.setAdmin(beef); + + hevm.expectRevert(abi.encodePacked("not pending admin")); + veALCX.acceptAdmin(); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("treasury cannot be 0x0")); + veALCX.setTreasury(address(0)); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("fluxMultiplier must be greater than 0")); + veALCX.setfluxMultiplier(0); + + hevm.prank(admin); + veALCX.setTreasury(beef); + assertEq(veALCX.treasury(), beef, "incorrect treasury"); + + hevm.expectRevert(abi.encodePacked("owner not found")); + veALCX.approve(admin, 100); + + hevm.expectRevert(abi.encodePacked("cannot delegate to zero address")); + veALCX.delegate(address(0)); + + hevm.prank(beef); + veALCX.acceptAdmin(); + + hevm.startPrank(beef); + + veALCX.setfluxPerVeALCX(10); + assertEq(veALCX.fluxPerVeALCX(), 10, "incorrect flux per veALCX"); + + veALCX.setfluxMultiplier(10); + assertEq(veALCX.fluxMultiplier(), 10, "incorrect flux multiplier"); + + veALCX.setClaimFee(1000); + assertEq(veALCX.claimFeeBps(), 1000, "incorrect claim fee"); + + hevm.stopPrank(); + } }