Skip to content

Commit 33ece7f

Browse files
Merge pull request #67 from liquity/fix-temp-invariant-align-bribes
Fix temp invariant align bribes
2 parents 78688ab + 3f7705a commit 33ece7f

13 files changed

+615
-78
lines changed

Invariants.MD

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Snapshot Solvency
2+
3+
uint256 claim = _votesForInitiativeSnapshot.votes * boldAccrued / _votesSnapshot.votes;
4+
For each initiative this is what the value is
5+
If the initiative is "Claimable" this is what it receives
6+
The call never reverts
7+
The sum of claims is less than the boldAccrued
8+
9+
Veto consistency
10+

echidna.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
testMode: "assertion"
2-
prefix: "crytic_"
1+
testMode: "optimization"
2+
prefix: "optimize_"
33
coverage: true
44
corpusDir: "echidna"
55
balanceAddr: 0x1043561a8829300000

src/BribeInitiative.sol

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,27 @@ contract BribeInitiative is IInitiative, IBribeInitiative {
9999
);
100100

101101
(uint88 totalLQTY, uint32 totalAverageTimestamp) = _decodeLQTYAllocation(totalLQTYAllocation.value);
102-
uint240 totalVotes = governance.lqtyToVotes(totalLQTY, block.timestamp, totalAverageTimestamp);
102+
// WHAT HAPPENS IF WE ENFORCE EPOCH AT END?
103+
// THEN WE LINEARLY TRANSLATE TO EPOCH END?
104+
// EPOCH_START + epoch * EPOCH_DURATION is the time to claim (I think)
105+
106+
// Since epoch 1 starts at Epoch Start, epoch * Duration goes to the end of
107+
// But is this safe vs last second of the epoch?
108+
// I recall having a similar issue already with Velodrome
109+
uint32 epochEnd = uint32(governance.EPOCH_START()) + uint32(_epoch) * uint32(governance.EPOCH_DURATION());
110+
111+
/// @audit User Invariant
112+
assert(totalAverageTimestamp <= epochEnd); /// NOTE: Tests break because they are not realistic
113+
114+
115+
uint240 totalVotes = governance.lqtyToVotes(totalLQTY, epochEnd, totalAverageTimestamp);
103116
if (totalVotes != 0) {
104117
(uint88 lqty, uint32 averageTimestamp) = _decodeLQTYAllocation(lqtyAllocation.value);
105-
uint240 votes = governance.lqtyToVotes(lqty, block.timestamp, averageTimestamp);
118+
119+
/// @audit Governance Invariant
120+
assert(averageTimestamp <= epochEnd); /// NOTE: Tests break because they are not realistic
121+
122+
uint240 votes = governance.lqtyToVotes(lqty, epochEnd, averageTimestamp);
106123
boldAmount = uint256(bribe.boldAmount) * uint256(votes) / uint256(totalVotes);
107124
bribeTokenAmount = uint256(bribe.bribeTokenAmount) * uint256(votes) / uint256(totalVotes);
108125
}

src/Governance.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,10 +843,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance
843843

844844
/// @inheritdoc IGovernance
845845
function claimForInitiative(address _initiative) external nonReentrant returns (uint256) {
846+
// Accrue and update state
846847
(VoteSnapshot memory votesSnapshot_,) = _snapshotVotes();
847848
(InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) =
848849
_snapshotVotesForInitiative(_initiative);
849850

851+
// Compute values on accrued state
850852
(InitiativeStatus status,, uint256 claimableAmount) =
851853
getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState);
852854

@@ -862,6 +864,14 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance
862864
/// If `lastEpochClaim` is older than epoch() - 1 it means the initiative couldn't claim any rewards this epoch
863865
initiativeStates[_initiative].lastEpochClaim = epoch() - 1;
864866

867+
// @audit INVARIANT, because of rounding errors the system can overpay
868+
/// We upscale the timestamp to reduce the impact of the loss
869+
/// However this is still possible
870+
uint256 available = bold.balanceOf(address(this));
871+
if(claimableAmount > available) {
872+
claimableAmount = available;
873+
}
874+
865875
bold.safeTransfer(_initiative, claimableAmount);
866876

867877
emit ClaimForInitiative(_initiative, claimableAmount, votesSnapshot_.forEpoch);

test/BribeInitiativeAllocate.t.sol

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,12 @@ contract BribeInitiativeAllocateTest is Test {
159159

160160
function test_onAfterAllocateLQTY_newEpoch_NoVetoToVeto() public {
161161
governance.setEpoch(1);
162+
vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch
162163

163164
vm.startPrank(address(governance));
164165

166+
// set user2 allocations like governance would using onAfterAllocateLQTY at epoch 1
167+
// sets avgTimestamp to current block.timestamp
165168
{
166169
IGovernance.UserState memory userState =
167170
IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)});
@@ -184,6 +187,8 @@ contract BribeInitiativeAllocateTest is Test {
184187
assertEq(userAverageTimestamp, uint32(block.timestamp));
185188
}
186189

190+
// set user2 allocations like governance would using onAfterAllocateLQTY at epoch 1
191+
// sets avgTimestamp to current block.timestamp
187192
{
188193
IGovernance.UserState memory userState =
189194
IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)});
@@ -196,6 +201,7 @@ contract BribeInitiativeAllocateTest is Test {
196201
lastEpochClaim: 0
197202
});
198203
bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState);
204+
199205
(uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) =
200206
bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch());
201207
assertEq(totalLQTYAllocated, 1001e18);
@@ -206,16 +212,20 @@ contract BribeInitiativeAllocateTest is Test {
206212
assertEq(userAverageTimestamp, uint32(block.timestamp));
207213
}
208214

215+
// lusdHolder deposits bribes into the initiative
209216
vm.startPrank(lusdHolder);
210217
lqty.approve(address(bribeInitiative), 1000e18);
211218
lusd.approve(address(bribeInitiative), 1000e18);
212219
bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1);
213220
vm.stopPrank();
214221

215222
governance.setEpoch(2);
223+
vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts
216224

217225
vm.startPrank(address(governance));
218226

227+
// set allocation in initiative for user in epoch 1
228+
// sets avgTimestamp to current block.timestamp
219229
{
220230
IGovernance.UserState memory userState =
221231
IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)});
@@ -238,6 +248,8 @@ contract BribeInitiativeAllocateTest is Test {
238248
assertEq(userAverageTimestamp, uint32(block.timestamp));
239249
}
240250

251+
// set allocation in initiative for user2 in epoch 1
252+
// sets avgTimestamp to current block.timestamp
241253
{
242254
IGovernance.UserState memory userState =
243255
IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)});
@@ -260,7 +272,8 @@ contract BribeInitiativeAllocateTest is Test {
260272
assertEq(userAverageTimestamp, uint32(block.timestamp));
261273
}
262274

263-
governance.setEpoch(3);
275+
governance.setEpoch(3);
276+
vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to third epoch ts
264277

265278
vm.startPrank(address(user));
266279

@@ -269,12 +282,13 @@ contract BribeInitiativeAllocateTest is Test {
269282
claimData[0].prevLQTYAllocationEpoch = 2;
270283
claimData[0].prevTotalLQTYAllocationEpoch = 2;
271284
(uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(claimData);
272-
assertEq(boldAmount, 0);
273-
assertEq(bribeTokenAmount, 0);
285+
assertEq(boldAmount, 0, "boldAmount nonzero");
286+
assertEq(bribeTokenAmount, 0, "bribeTokenAmount nonzero");
274287
}
275288

276289
function test_onAfterAllocateLQTY_newEpoch_VetoToNoVeto() public {
277290
governance.setEpoch(1);
291+
vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch
278292

279293
vm.startPrank(address(governance));
280294

@@ -325,6 +339,7 @@ contract BribeInitiativeAllocateTest is Test {
325339
assertEq(userAverageTimestampAfterVeto, uint32(block.timestamp));
326340

327341
governance.setEpoch(2);
342+
vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts
328343

329344
IGovernance.UserState memory userStateNewEpoch =
330345
IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint32(block.timestamp)});
@@ -359,6 +374,7 @@ contract BribeInitiativeAllocateTest is Test {
359374
vm.startPrank(address(governance));
360375

361376
governance.setEpoch(3);
377+
vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to third epoch ts
362378

363379
IGovernance.UserState memory userStateNewEpoch3 =
364380
IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint32(block.timestamp)});
@@ -385,6 +401,7 @@ contract BribeInitiativeAllocateTest is Test {
385401
assertEq(userAverageTimestampNewEpoch3, uint32(block.timestamp));
386402

387403
governance.setEpoch(4);
404+
vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to fourth epoch ts
388405

389406
vm.startPrank(address(user));
390407

@@ -509,6 +526,7 @@ contract BribeInitiativeAllocateTest is Test {
509526
vm.stopPrank();
510527

511528
governance.setEpoch(1);
529+
vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch
512530

513531
vm.startPrank(address(governance));
514532

@@ -585,6 +603,7 @@ contract BribeInitiativeAllocateTest is Test {
585603
}
586604

587605
governance.setEpoch(2);
606+
vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts
588607

589608
vm.startPrank(address(user));
590609

@@ -603,6 +622,7 @@ contract BribeInitiativeAllocateTest is Test {
603622
vm.stopPrank();
604623

605624
governance.setEpoch(1);
625+
vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch
606626

607627
vm.startPrank(address(governance));
608628

@@ -679,6 +699,7 @@ contract BribeInitiativeAllocateTest is Test {
679699
}
680700

681701
governance.setEpoch(2);
702+
vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts
682703

683704
vm.startPrank(address(user));
684705

@@ -699,6 +720,7 @@ contract BribeInitiativeAllocateTest is Test {
699720
vm.stopPrank();
700721

701722
governance.setEpoch(1);
723+
vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch
702724

703725
vm.startPrank(address(governance));
704726

@@ -799,6 +821,7 @@ contract BribeInitiativeAllocateTest is Test {
799821
}
800822

801823
governance.setEpoch(2);
824+
vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts
802825

803826
vm.startPrank(address(user));
804827

test/mocks/MockGovernance.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ pragma solidity ^0.8.24;
44
contract MockGovernance {
55
uint16 private __epoch;
66

7+
uint32 constant public EPOCH_START = 0;
8+
uint32 constant public EPOCH_DURATION = 7 days;
9+
710
function claimForInitiative(address) external pure returns (uint256) {
811
return 1000e18;
912
}

0 commit comments

Comments
 (0)