Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into fix-offset-dust
Browse files Browse the repository at this point in the history
  • Loading branch information
danielattilasimon committed Dec 30, 2024
2 parents ba0f44c + ffbf480 commit 9093f62
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 41 deletions.
4 changes: 2 additions & 2 deletions src/BribeInitiative.sol
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ contract BribeInitiative is IInitiative, IBribeInitiative {
function onAfterAllocateLQTY(
uint256 _currentEpoch,
address _user,
IGovernance.UserState calldata _userState,
IGovernance.UserState calldata,
IGovernance.Allocation calldata _allocation,
IGovernance.InitiativeState calldata _initiativeState
) external virtual onlyGovernance {
Expand All @@ -229,7 +229,7 @@ contract BribeInitiative is IInitiative, IBribeInitiative {
_user,
_currentEpoch,
_allocation.voteLQTY,
_userState.allocatedOffset,
_allocation.voteOffset,
mostRecentUserEpoch != _currentEpoch // Insert if user current > recent
);
}
Expand Down
49 changes: 20 additions & 29 deletions src/Governance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,6 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
function _increaseUserVoteTrackers(uint256 _lqtyAmount) private returns (UserProxy) {
require(_lqtyAmount > 0, "Governance: zero-lqty-amount");

// Assert that we have resetted here
// TODO: Remove, as now unecessary
UserState memory userState = userStates[msg.sender];
require(userState.allocatedLQTY == 0, "Governance: must-be-zero-allocation");

address userProxyAddress = deriveUserProxyAddress(msg.sender);

if (userProxyAddress.code.length == 0) {
Expand All @@ -154,10 +149,8 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
UserProxy userProxy = UserProxy(payable(userProxyAddress));

// update the vote power trackers
userState.unallocatedLQTY += _lqtyAmount;
userState.unallocatedOffset += block.timestamp * _lqtyAmount;

userStates[msg.sender] = userState;
userStates[msg.sender].unallocatedLQTY += _lqtyAmount;
userStates[msg.sender].unallocatedOffset += block.timestamp * _lqtyAmount;

return userProxy;
}
Expand Down Expand Up @@ -201,13 +194,14 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
}

function withdrawLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) public nonReentrant {
// check that user has reset before changing lqty balance
UserState storage userState = userStates[msg.sender];
require(userState.allocatedLQTY == 0, "Governance: must-allocate-zero");

UserProxy userProxy = UserProxy(payable(deriveUserProxyAddress(msg.sender)));
require(address(userProxy).code.length != 0, "Governance: user-proxy-not-deployed");

// check if user has enough unallocated lqty
require(_lqtyAmount <= userState.unallocatedLQTY, "Governance: insufficient-unallocated-lqty");

// Update the offset tracker
if (_lqtyAmount < userState.unallocatedLQTY) {
// The offset decrease is proportional to the partial lqty decrease
Expand All @@ -221,8 +215,6 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
// Update the user's LQTY tracker
userState.unallocatedLQTY -= _lqtyAmount;

userStates[msg.sender] = userState;

(
uint256 lqtyReceived,
uint256 lqtySent,
Expand Down Expand Up @@ -467,7 +459,10 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
}

// == Unregister Condition == //
// e.g. if `UNREGISTRATION_AFTER_EPOCHS` is 4, the 4th epoch flip that would result in SKIP, will result in the initiative being `UNREGISTERABLE`
// e.g. if `UNREGISTRATION_AFTER_EPOCHS` is 4, the initiative will become unregisterable after spending 4 epochs
// while being in one of the following conditions:
// - in `SKIP` state (not having received enough votes to cross the voting threshold)
// - in `CLAIMABLE` state (having received enough votes to cross the voting threshold) but never being claimed
if (
(_initiativeState.lastEpochClaim + UNREGISTRATION_AFTER_EPOCHS < currentEpoch - 1)
|| _votesForInitiativeSnapshot.vetos > _votesForInitiativeSnapshot.votes
Expand All @@ -485,8 +480,6 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
uint256 currentEpoch = epoch();
require(currentEpoch > 2, "Governance: registration-not-yet-enabled");

bold.safeTransferFrom(msg.sender, address(this), REGISTRATION_FEE);

require(_initiative != address(0), "Governance: zero-address");
(InitiativeStatus status,,) = getInitiativeState(_initiative);
require(status == InitiativeStatus.NONEXISTENT, "Governance: initiative-already-registered");
Expand All @@ -495,6 +488,8 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
(VoteSnapshot memory snapshot,) = _snapshotVotes();
UserState memory userState = userStates[msg.sender];

bold.safeTransferFrom(msg.sender, address(this), REGISTRATION_FEE);

// an initiative can be registered if the registrant has more voting power (LQTY * age)
// than the registration threshold derived from the previous epoch's total global votes

Expand Down Expand Up @@ -570,9 +565,7 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
return cachedData;
}

/// @notice Reset the allocations for the initiatives being passed, must pass all initiatives else it will revert
/// NOTE: If you reset at the last day of the epoch, you won't be able to vote again
/// Use `allocateLQTY` to reset and vote
/// @inheritdoc IGovernance
function resetAllocations(address[] calldata _initiativesToReset, bool checkAll) external nonReentrant {
_requireNoDuplicates(_initiativesToReset);
_resetInitiatives(_initiativesToReset);
Expand All @@ -594,8 +587,10 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
int256[] calldata _absoluteLQTYVotes,
int256[] calldata _absoluteLQTYVetos
) external nonReentrant {
require(_initiatives.length == _absoluteLQTYVotes.length, "Length");
require(_absoluteLQTYVetos.length == _absoluteLQTYVotes.length, "Length");
require(
_initiatives.length == _absoluteLQTYVotes.length && _absoluteLQTYVotes.length == _absoluteLQTYVetos.length,
"Governance: array-length-mismatch"
);

// To ensure the change is safe, enforce uniqueness
_requireNoDuplicates(_initiativesToReset);
Expand Down Expand Up @@ -685,20 +680,16 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own
}

/// @dev For each given initiative applies relative changes to the allocation
/// NOTE: Given the current usage the function either: Resets the value to 0, or sets the value to a new value
/// Review the flows as the function could be used in many ways, but it ends up being used in just those 2 ways
/// @dev Assumes that all the input arrays are of equal length
/// @dev NOTE: Given the current usage the function either: Resets the value to 0, or sets the value to a new value
/// Review the flows as the function could be used in many ways, but it ends up being used in just those 2 ways
function _allocateLQTY(
address[] memory _initiatives,
int256[] memory _deltaLQTYVotes,
int256[] memory _deltaLQTYVetos,
int256[] memory _deltaOffsetVotes,
int256[] memory _deltaOffsetVetos
) internal {
require(
_initiatives.length == _deltaLQTYVotes.length && _initiatives.length == _deltaLQTYVetos.length,
"Governance: array-length-mismatch"
);

AllocateLQTYMemory memory vars;
(vars.votesSnapshot_, vars.state) = _snapshotVotes();
vars.currentEpoch = epoch();
Expand Down Expand Up @@ -855,7 +846,7 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own

globalState = state;

/// weeks * 2^16 > u32 so the contract will stop working before this is an issue
/// Epoch will never reach 2^256 - 1
registeredInitiatives[_initiative] = UNREGISTERED_INITIATIVE;

// Replaces try / catch | Enforces sufficient gas is passed
Expand Down
15 changes: 10 additions & 5 deletions src/interfaces/IGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -375,16 +375,21 @@ interface IGovernance {
/// @notice Allocates the user's LQTY to initiatives
/// @dev The user can only allocate to active initiatives (older than 1 epoch) and has to have enough unallocated
/// LQTY available, the initiatives listed must be unique, and towards the end of the epoch a user can only maintain or reduce their votes
/// @param _resetInitiatives Addresses of the initiatives the caller was previously allocated to, must be reset to prevent desynch of voting power
/// @param _initiativesToReset Addresses of the initiatives the caller was previously allocated to, must be reset to prevent desynch of voting power
/// @param _initiatives Addresses of the initiatives to allocate to, can match or be different from `_resetInitiatives`
/// @param _absoluteLQTYVotes Delta LQTY to allocate to the initiatives as votes
/// @param absoluteLQTYVetos Delta LQTY to allocate to the initiatives as vetos
/// @param _absoluteLQTYVotes LQTY to allocate to the initiatives as votes
/// @param _absoluteLQTYVetos LQTY to allocate to the initiatives as vetos
function allocateLQTY(
address[] calldata _resetInitiatives,
address[] calldata _initiativesToReset,
address[] memory _initiatives,
int256[] memory _absoluteLQTYVotes,
int256[] memory absoluteLQTYVetos
int256[] memory _absoluteLQTYVetos
) external;
/// @notice Deallocates the user's LQTY from initiatives
/// @param _initiativesToReset Addresses of initiatives to deallocate LQTY from
/// @param _checkAll When true, the call will revert if there is still some allocated LQTY left after deallocating
/// from all the addresses in `_initiativesToReset`
function resetAllocations(address[] calldata _initiativesToReset, bool _checkAll) external;

/// @notice Splits accrued funds according to votes received between all initiatives
/// @param _initiative Addresse of the initiative
Expand Down
79 changes: 76 additions & 3 deletions test/BribeInitiative.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ contract BribeInitiativeTest is Test, MockStakingV1Deployer {
address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038);
address private user3 = makeAddr("user3");
address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c);
address private constant initiative = address(0x1);
address private constant initiative2 = address(0x2);
address private constant initiative3 = address(0x3);

uint256 private constant REGISTRATION_FEE = 1e18;
uint256 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18;
Expand Down Expand Up @@ -892,6 +889,82 @@ contract BribeInitiativeTest is Test, MockStakingV1Deployer {
assertEq(bribeTokenAmount, 0);
}

// See https://github.com/liquity/V2-gov/issues/106
function test_VoterGetsTheirFairShareOfBribes() external {
uint256 bribeAmount = 10_000 ether;
uint256 voteAmount = 100_000 ether;
address otherInitiative = makeAddr("otherInitiative");

// Fast-forward to enable registration
vm.warp(block.timestamp + 2 * EPOCH_DURATION);

vm.startPrank(lusdHolder);
{
// Register otherInitiative, so user1 has something else to vote on
lusd.approve(address(governance), REGISTRATION_FEE);
governance.registerInitiative(otherInitiative);

// Deposit some bribes into bribeInitiative in next epoch
lusd.approve(address(bribeInitiative), bribeAmount);
lqty.approve(address(bribeInitiative), bribeAmount);
bribeInitiative.depositBribe(bribeAmount, bribeAmount, governance.epoch() + 1);
}
vm.stopPrank();

// Ensure otherInitiative can be voted on
vm.warp(block.timestamp + EPOCH_DURATION);

address[] memory initiativesToReset = new address[](0);
address[] memory initiatives;
int256[] memory votes;
int256[] memory vetos;

vm.startPrank(user1);
{
initiatives = new address[](2);
votes = new int256[](2);
vetos = new int256[](2);

initiatives[0] = otherInitiative;
initiatives[1] = address(bribeInitiative);
votes[0] = int256(voteAmount);
votes[1] = int256(voteAmount);

lqty.approve(governance.deriveUserProxyAddress(user1), 2 * voteAmount);
governance.depositLQTY(2 * voteAmount);
governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos);
}
vm.stopPrank();

vm.startPrank(user2);
{
initiatives = new address[](1);
votes = new int256[](1);
vetos = new int256[](1);

initiatives[0] = address(bribeInitiative);
votes[0] = int256(voteAmount);

lqty.approve(governance.deriveUserProxyAddress(user2), voteAmount);
governance.depositLQTY(voteAmount);
governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos);
}
vm.stopPrank();

// Fast-forward to next epoch, so previous epoch's bribes can be claimed
vm.warp(block.timestamp + EPOCH_DURATION);

IBribeInitiative.ClaimData[] memory claimData = new IBribeInitiative.ClaimData[](1);
claimData[0].epoch = governance.epoch() - 1;
claimData[0].prevLQTYAllocationEpoch = governance.epoch() - 1;
claimData[0].prevTotalLQTYAllocationEpoch = governance.epoch() - 1;

vm.prank(user1);
(uint256 lusdBribe, uint256 lqtyBribe) = bribeInitiative.claimBribes(claimData);
assertEqDecimal(lusdBribe, bribeAmount / 2, 18, "user1 didn't get their fair share of LUSD");
assertEqDecimal(lqtyBribe, bribeAmount / 2, 18, "user1 didn't get their fair share of LQTY");
}

/**
* Helpers
*/
Expand Down
Loading

0 comments on commit 9093f62

Please sign in to comment.