-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: ensure that bribes can always be claimed
Even from epochs where the voter stayed inactive (but had a previous LQTY allocation). Currently failing due to #101.
- Loading branch information
1 parent
5ec3e29
commit 00b827b
Showing
4 changed files
with
503 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.24; | ||
|
||
import {console2 as console} from "forge-std/console2.sol"; | ||
import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; | ||
import {Math} from "openzeppelin/contracts/utils/math/Math.sol"; | ||
import {IBribeInitiative} from "../src/interfaces/IBribeInitiative.sol"; | ||
import {IGovernance} from "../src/interfaces/IGovernance.sol"; | ||
import {BribeInitiative} from "../src/BribeInitiative.sol"; | ||
import {Governance} from "../src/Governance.sol"; | ||
import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; | ||
import {MockStakingV1} from "./mocks/MockStakingV1.sol"; | ||
import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; | ||
import {Random} from "./util/Random.sol"; | ||
import {UintArray} from "./util/UintArray.sol"; | ||
import {StringFormatting} from "./util/StringFormatting.sol"; | ||
|
||
contract BribeInitiativeFireAndForgetTest is MockStakingV1Deployer { | ||
using Random for Random.Context; | ||
using UintArray for uint256[]; | ||
using Strings for *; | ||
using StringFormatting for *; | ||
|
||
uint32 constant START_TIME = 1732873631; | ||
uint32 constant EPOCH_DURATION = 7 days; | ||
uint32 constant EPOCH_VOTING_CUTOFF = 6 days; | ||
|
||
uint16 constant MAX_NUM_EPOCHS = 100; | ||
uint88 constant MAX_VOTE = 1e6 ether; | ||
uint128 constant MAX_BRIBE = 1e6 ether; | ||
uint256 constant MAX_CLAIMS_PER_CALL = 10; | ||
uint256 constant MEAN_TIME_BETWEEN_VOTES = 2 * EPOCH_DURATION; | ||
uint256 constant VOTER_PROBABILITY = type(uint256).max / 10; | ||
|
||
address constant voter = address(uint160(uint256(keccak256("voter")))); | ||
address constant other = address(uint160(uint256(keccak256("other")))); | ||
address constant briber = address(uint160(uint256(keccak256("briber")))); | ||
|
||
IGovernance.Configuration config = IGovernance.Configuration({ | ||
registrationFee: 0, | ||
registrationThresholdFactor: 0, | ||
unregistrationThresholdFactor: 4 ether, | ||
unregistrationAfterEpochs: 4, | ||
votingThresholdFactor: 1e4, // min value that doesn't result in division by zero | ||
minClaim: 0, | ||
minAccrual: 0, | ||
epochStart: START_TIME - EPOCH_DURATION, | ||
epochDuration: EPOCH_DURATION, | ||
epochVotingCutoff: EPOCH_VOTING_CUTOFF | ||
}); | ||
|
||
struct Vote { | ||
uint16 epoch; | ||
uint256 amount; | ||
} | ||
|
||
MockStakingV1 stakingV1; | ||
MockERC20Tester lqty; | ||
MockERC20Tester lusd; | ||
MockERC20Tester bold; | ||
MockERC20Tester bryb; | ||
Governance governance; | ||
BribeInitiative bribeInitiative; | ||
|
||
mapping(address who => address[]) initiativesToReset; | ||
mapping(address who => Vote) latestVote; | ||
mapping(uint256 epoch => uint256) boldAtEpoch; | ||
mapping(uint256 epoch => uint256) brybAtEpoch; | ||
mapping(uint256 epoch => uint256) voteAtEpoch; // number of LQTY allocated by "voter" | ||
mapping(uint256 epoch => uint256) toteAtEpoch; // number of LQTY allocated in total ("voter" + "other") | ||
mapping(uint256 epoch => IBribeInitiative.ClaimData) claimDataAtEpoch; | ||
IBribeInitiative.ClaimData[] claimData; | ||
|
||
function setUp() external { | ||
vm.warp(START_TIME); | ||
|
||
vm.label(voter, "voter"); | ||
vm.label(other, "other"); | ||
vm.label(briber, "briber"); | ||
|
||
(stakingV1, lqty, lusd) = deployMockStakingV1(); | ||
|
||
bold = new MockERC20Tester("BOLD Stablecoin", "BOLD"); | ||
vm.label(address(bold), "BOLD"); | ||
|
||
bryb = new MockERC20Tester("Bribe Token", "BRYB"); | ||
vm.label(address(bryb), "BRYB"); | ||
|
||
governance = new Governance({ | ||
_lqty: address(lqty), | ||
_lusd: address(lusd), | ||
_stakingV1: address(stakingV1), | ||
_bold: address(bold), | ||
_config: config, | ||
_owner: address(this), | ||
_initiatives: new address[](0) | ||
}); | ||
|
||
bribeInitiative = | ||
new BribeInitiative({_governance: address(governance), _bold: address(bold), _bribeToken: address(bryb)}); | ||
|
||
address[] memory initiatives = new address[](1); | ||
initiatives[0] = address(bribeInitiative); | ||
governance.registerInitialInitiatives(initiatives); | ||
|
||
address voterProxy = governance.deriveUserProxyAddress(voter); | ||
vm.label(voterProxy, "voterProxy"); | ||
|
||
address otherProxy = governance.deriveUserProxyAddress(other); | ||
vm.label(otherProxy, "otherProxy"); | ||
|
||
lqty.mint(voter, MAX_VOTE); | ||
lqty.mint(other, MAX_VOTE); | ||
|
||
vm.startPrank(voter); | ||
lqty.approve(voterProxy, MAX_VOTE); | ||
governance.depositLQTY(MAX_VOTE); | ||
vm.stopPrank(); | ||
|
||
vm.startPrank(other); | ||
lqty.approve(otherProxy, MAX_VOTE); | ||
governance.depositLQTY(MAX_VOTE); | ||
vm.stopPrank(); | ||
|
||
vm.startPrank(briber); | ||
bold.approve(address(bribeInitiative), type(uint256).max); | ||
bryb.approve(address(bribeInitiative), type(uint256).max); | ||
vm.stopPrank(); | ||
} | ||
|
||
function test_AbleToClaimBribesInAnyOrder_EvenFromEpochsWhereVoterStayedInactive(bytes32 seed) external { | ||
Random.Context memory random = Random.init(seed); | ||
uint16 startingEpoch = governance.epoch(); | ||
uint16 lastEpoch = startingEpoch; | ||
|
||
for (uint16 i = 0; i < MAX_NUM_EPOCHS; ++i) { | ||
uint128 boldAmount = uint128(random.generate(MAX_BRIBE)); | ||
uint128 brybAmount = uint128(random.generate(MAX_BRIBE)); | ||
|
||
bold.mint(briber, boldAmount); | ||
bryb.mint(briber, brybAmount); | ||
|
||
vm.prank(briber); | ||
bribeInitiative.depositBribe(boldAmount, brybAmount, startingEpoch + i); | ||
} | ||
|
||
for (;;) { | ||
vm.warp(block.timestamp + random.generate(2 * MEAN_TIME_BETWEEN_VOTES)); | ||
uint16 epoch = governance.epoch(); | ||
|
||
for (uint16 i = lastEpoch; i < epoch; ++i) { | ||
voteAtEpoch[i] = latestVote[voter].amount; | ||
toteAtEpoch[i] = latestVote[voter].amount + latestVote[other].amount; | ||
claimDataAtEpoch[i].epoch = i; | ||
claimDataAtEpoch[i].prevLQTYAllocationEpoch = latestVote[voter].epoch; | ||
claimDataAtEpoch[i].prevTotalLQTYAllocationEpoch = | ||
uint16(Math.max(latestVote[voter].epoch, latestVote[other].epoch)); | ||
|
||
console.log( | ||
string.concat( | ||
"epoch #", | ||
i.toString(), | ||
": vote = ", | ||
voteAtEpoch[i].decimal(), | ||
", tote = ", | ||
toteAtEpoch[i].decimal() | ||
) | ||
); | ||
} | ||
|
||
lastEpoch = epoch; | ||
if (epoch >= startingEpoch + MAX_NUM_EPOCHS) break; | ||
|
||
(IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(address(bribeInitiative)); | ||
|
||
if (status == IGovernance.InitiativeStatus.CLAIMABLE) { | ||
governance.claimForInitiative(address(bribeInitiative)); | ||
} | ||
|
||
if (status == IGovernance.InitiativeStatus.UNREGISTERABLE) { | ||
governance.unregisterInitiative(address(bribeInitiative)); | ||
break; | ||
} | ||
|
||
address who = random.generate() < VOTER_PROBABILITY ? voter : other; | ||
uint256 vote = governance.secondsWithinEpoch() <= EPOCH_VOTING_CUTOFF ? random.generate(MAX_VOTE) : 0; | ||
|
||
if (vote > 0 || latestVote[who].amount > 0) { | ||
// can't reset when already reset | ||
latestVote[who].epoch = epoch; | ||
latestVote[who].amount = vote; | ||
_vote(who, address(bribeInitiative), latestVote[who].amount); | ||
} | ||
} | ||
|
||
uint256[] memory epochPermutation = UintArray.seq(startingEpoch, lastEpoch + 1).permute(random); | ||
uint256 start = 0; | ||
uint256 expectedBold = 0; | ||
uint256 expectedBryb = 0; | ||
|
||
while (start < epochPermutation.length) { | ||
uint256 end = Math.min(start + random.generate(MAX_CLAIMS_PER_CALL), epochPermutation.length); | ||
|
||
for (uint256 i = start; i < end; ++i) { | ||
if ( | ||
voteAtEpoch[epochPermutation[i]] > 0 | ||
&& (boldAtEpoch[epochPermutation[i]] > 0 || brybAtEpoch[epochPermutation[i]] > 0) | ||
) { | ||
claimData.push(claimDataAtEpoch[epochPermutation[i]]); | ||
expectedBold += boldAtEpoch[epochPermutation[i]] * voteAtEpoch[epochPermutation[i]] | ||
/ toteAtEpoch[epochPermutation[i]]; | ||
expectedBryb += brybAtEpoch[epochPermutation[i]] * voteAtEpoch[epochPermutation[i]] | ||
/ toteAtEpoch[epochPermutation[i]]; | ||
} | ||
} | ||
|
||
vm.prank(voter); | ||
bribeInitiative.claimBribes(claimData); | ||
delete claimData; | ||
|
||
assertEqDecimal(bold.balanceOf(voter), expectedBold, 18, "bold.balanceOf(voter) != expectedBold"); | ||
assertEqDecimal(bryb.balanceOf(voter), expectedBryb, 18, "bryb.balanceOf(voter) != expectedBryb"); | ||
|
||
start = end; | ||
} | ||
} | ||
|
||
///////////// | ||
// Helpers // | ||
///////////// | ||
|
||
function _vote(address who, address initiative, uint256 vote) internal { | ||
assertLeDecimal(vote, uint256(int256(type(int88).max)), 18, "vote > type(uint88).max"); | ||
vm.startPrank(who); | ||
|
||
if (vote > 0) { | ||
address[] memory initiatives = new address[](1); | ||
int88[] memory votes = new int88[](1); | ||
int88[] memory vetos = new int88[](1); | ||
|
||
initiatives[0] = initiative; | ||
votes[0] = int88(uint88(vote)); | ||
governance.allocateLQTY(initiativesToReset[who], initiatives, votes, vetos); | ||
|
||
if (initiativesToReset[who].length != 0) initiativesToReset[who].pop(); | ||
initiativesToReset[who].push(initiative); | ||
} else { | ||
if (initiativesToReset[who].length != 0) { | ||
governance.resetAllocations(initiativesToReset[who], true); | ||
initiativesToReset[who].pop(); | ||
} | ||
} | ||
|
||
vm.stopPrank(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.24; | ||
|
||
function bound(uint256 x, uint256 min, uint256 max) pure returns (uint256) { | ||
require(min <= max, "min > max"); | ||
return min == 0 && max == type(uint256).max ? x : min + x % (max - min + 1); | ||
} | ||
|
||
library Random { | ||
struct Context { | ||
bytes32 seed; | ||
} | ||
|
||
function init(bytes32 seed) internal pure returns (Random.Context memory c) { | ||
init(c, seed); | ||
} | ||
|
||
function init(Context memory c, bytes32 seed) internal pure { | ||
c.seed = seed; | ||
} | ||
|
||
function generate(Context memory c) internal pure returns (uint256) { | ||
return generate(c, 0, type(uint256).max); | ||
} | ||
|
||
function generate(Context memory c, uint256 max) internal pure returns (uint256) { | ||
return generate(c, 0, max); | ||
} | ||
|
||
function generate(Context memory c, uint256 min, uint256 max) internal pure returns (uint256) { | ||
c.seed = keccak256(abi.encode(c.seed)); | ||
return bound(uint256(c.seed), min, max); | ||
} | ||
} |
Oops, something went wrong.