Skip to content

Commit

Permalink
Merge pull request #123 from liquity/bribe-dust
Browse files Browse the repository at this point in the history
fix: dust left after claiming all bribes
  • Loading branch information
danielattilasimon authored Jan 2, 2025
2 parents 9efb1d1 + 8db397c commit f6839c9
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 199 deletions.
78 changes: 32 additions & 46 deletions src/BribeInitiative.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import {IInitiative} from "./interfaces/IInitiative.sol";
import {IBribeInitiative} from "./interfaces/IBribeInitiative.sol";

import {DoubleLinkedList} from "./utils/DoubleLinkedList.sol";
import {_lqtyToVotes} from "./utils/VotingPower.sol";

contract BribeInitiative is IInitiative, IBribeInitiative {
using SafeERC20 for IERC20;
using DoubleLinkedList for DoubleLinkedList.List;

uint256 internal immutable EPOCH_START;
uint256 internal immutable EPOCH_DURATION;

/// @inheritdoc IBribeInitiative
IGovernance public immutable governance;
/// @inheritdoc IBribeInitiative
Expand All @@ -37,6 +41,9 @@ contract BribeInitiative is IInitiative, IBribeInitiative {
governance = IGovernance(_governance);
bold = IERC20(_bold);
bribeToken = IERC20(_bribeToken);

EPOCH_START = governance.EPOCH_START();
EPOCH_DURATION = governance.EPOCH_DURATION();
}

modifier onlyGovernance() {
Expand All @@ -46,23 +53,24 @@ contract BribeInitiative is IInitiative, IBribeInitiative {

/// @inheritdoc IBribeInitiative
function totalLQTYAllocatedByEpoch(uint256 _epoch) external view returns (uint256, uint256) {
return _loadTotalLQTYAllocation(_epoch);
return (totalLQTYAllocationByEpoch.items[_epoch].lqty, totalLQTYAllocationByEpoch.items[_epoch].offset);
}

/// @inheritdoc IBribeInitiative
function lqtyAllocatedByUserAtEpoch(address _user, uint256 _epoch) external view returns (uint256, uint256) {
return _loadLQTYAllocation(_user, _epoch);
return (
lqtyAllocationByUserAtEpoch[_user].items[_epoch].lqty,
lqtyAllocationByUserAtEpoch[_user].items[_epoch].offset
);
}

/// @inheritdoc IBribeInitiative
function depositBribe(uint256 _boldAmount, uint256 _bribeTokenAmount, uint256 _epoch) external {
uint256 epoch = governance.epoch();
require(_epoch >= epoch, "BribeInitiative: now-or-future-epochs");

Bribe memory bribe = bribeByEpoch[_epoch];
bribe.boldAmount += _boldAmount;
bribe.bribeTokenAmount += _bribeTokenAmount;
bribeByEpoch[_epoch] = bribe;
bribeByEpoch[_epoch].remainingBoldAmount += _boldAmount;
bribeByEpoch[_epoch].remainingBribeTokenAmount += _bribeTokenAmount;

emit DepositBribe(msg.sender, _boldAmount, _bribeTokenAmount, _epoch);

Expand All @@ -80,7 +88,7 @@ contract BribeInitiative is IInitiative, IBribeInitiative {
require(!claimedBribeAtEpoch[_user][_epoch], "BribeInitiative: already-claimed");

Bribe memory bribe = bribeByEpoch[_epoch];
require(bribe.boldAmount != 0 || bribe.bribeTokenAmount != 0, "BribeInitiative: no-bribe");
require(bribe.remainingBoldAmount != 0 || bribe.remainingBribeTokenAmount != 0, "BribeInitiative: no-bribe");

DoubleLinkedList.Item memory lqtyAllocation =
lqtyAllocationByUserAtEpoch[_user].getItem(_prevLQTYAllocationEpoch);
Expand All @@ -98,18 +106,25 @@ contract BribeInitiative is IInitiative, IBribeInitiative {
);

require(totalLQTYAllocation.lqty > 0, "BribeInitiative: total-lqty-allocation-zero");
require(lqtyAllocation.lqty > 0, "BribeInitiative: lqty-allocation-zero");

uint256 epochEnd = governance.EPOCH_START() + _epoch * governance.EPOCH_DURATION();
// `Governance` guarantees that `votes` evaluates to 0 or greater for each initiative at the time of allocation.
// Since the last possible moment to allocate within this epoch is 1 second before `epochEnd`, we have that:
// - `lqtyAllocation.lqty > 0` implies `votes > 0`
// - `totalLQTYAllocation.lqty > 0` implies `totalVotes > 0`

uint256 totalVotes = governance.lqtyToVotes(totalLQTYAllocation.lqty, epochEnd, totalLQTYAllocation.offset);
if (totalVotes != 0) {
require(lqtyAllocation.lqty > 0, "BribeInitiative: lqty-allocation-zero");
uint256 epochEnd = EPOCH_START + _epoch * EPOCH_DURATION;
uint256 totalVotes = _lqtyToVotes(totalLQTYAllocation.lqty, epochEnd, totalLQTYAllocation.offset);
uint256 votes = _lqtyToVotes(lqtyAllocation.lqty, epochEnd, lqtyAllocation.offset);
uint256 remainingVotes = totalVotes - bribe.claimedVotes;

uint256 votes = governance.lqtyToVotes(lqtyAllocation.lqty, epochEnd, lqtyAllocation.offset);
boldAmount = bribe.boldAmount * votes / totalVotes;
bribeTokenAmount = bribe.bribeTokenAmount * votes / totalVotes;
}
boldAmount = bribe.remainingBoldAmount * votes / remainingVotes;
bribeTokenAmount = bribe.remainingBribeTokenAmount * votes / remainingVotes;
bribe.remainingBoldAmount -= boldAmount;
bribe.remainingBribeTokenAmount -= bribeTokenAmount;
bribe.claimedVotes += votes;

bribeByEpoch[_epoch] = bribe;
claimedBribeAtEpoch[_user][_epoch] = true;

emit ClaimBribe(_user, _epoch, boldAmount, bribeTokenAmount);
Expand All @@ -129,23 +144,8 @@ contract BribeInitiative is IInitiative, IBribeInitiative {
bribeTokenAmount += bribeTokenAmount_;
}

// NOTE: Due to rounding errors, bribes may slightly overpay compared to what they have allocated
// We cap to the available amount for this reason
if (boldAmount != 0) {
uint256 max = bold.balanceOf(address(this));
if (boldAmount > max) {
boldAmount = max;
}
bold.safeTransfer(msg.sender, boldAmount);
}

if (bribeTokenAmount != 0) {
uint256 max = bribeToken.balanceOf(address(this));
if (bribeTokenAmount > max) {
bribeTokenAmount = max;
}
bribeToken.safeTransfer(msg.sender, bribeTokenAmount);
}
if (boldAmount != 0) bold.safeTransfer(msg.sender, boldAmount);
if (bribeTokenAmount != 0) bribeToken.safeTransfer(msg.sender, bribeTokenAmount);
}

/// @inheritdoc IInitiative
Expand Down Expand Up @@ -180,20 +180,6 @@ contract BribeInitiative is IInitiative, IBribeInitiative {
emit ModifyLQTYAllocation(_user, _epoch, _lqty, _offset);
}

function _loadTotalLQTYAllocation(uint256 _epoch) private view returns (uint256, uint256) {
require(_epoch <= governance.epoch(), "No future Lookup");
DoubleLinkedList.Item memory totalLqtyAllocation = totalLQTYAllocationByEpoch.items[_epoch];

return (totalLqtyAllocation.lqty, totalLqtyAllocation.offset);
}

function _loadLQTYAllocation(address _user, uint256 _epoch) private view returns (uint256, uint256) {
require(_epoch <= governance.epoch(), "No future Lookup");
DoubleLinkedList.Item memory lqtyAllocation = lqtyAllocationByUserAtEpoch[_user].items[_epoch];

return (lqtyAllocation.lqty, lqtyAllocation.offset);
}

/// @inheritdoc IBribeInitiative
function getMostRecentUserEpoch(address _user) external view returns (uint256) {
uint256 mostRecentUserEpoch = lqtyAllocationByUserAtEpoch[_user].getHead();
Expand Down
4 changes: 2 additions & 2 deletions src/Governance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {MultiDelegateCall} from "./utils/MultiDelegateCall.sol";
import {WAD, PermitParams} from "./utils/Types.sol";
import {safeCallWithMinGas} from "./utils/SafeCallMinGas.sol";
import {Ownable} from "./utils/Ownable.sol";
import {_lqtyToVotes} from "./utils/VotingPower.sol";

/// @title Governance: Modular Initiative based Governance
contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Ownable, IGovernance {
Expand Down Expand Up @@ -266,8 +267,7 @@ contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Own

/// @inheritdoc IGovernance
function lqtyToVotes(uint256 _lqtyAmount, uint256 _timestamp, uint256 _offset) public pure returns (uint256) {
uint256 prod = _lqtyAmount * _timestamp;
return prod > _offset ? prod - _offset : 0;
return _lqtyToVotes(_lqtyAmount, _timestamp, _offset);
}

/*//////////////////////////////////////////////////////////////
Expand Down
15 changes: 10 additions & 5 deletions src/interfaces/IBribeInitiative.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ interface IBribeInitiative {
function bribeToken() external view returns (IERC20 bribeToken);

struct Bribe {
uint256 boldAmount;
uint256 bribeTokenAmount; // [scaled as 10 ** bribeToken.decimals()]
uint256 remainingBoldAmount;
uint256 remainingBribeTokenAmount; // [scaled as 10 ** bribeToken.decimals()]
uint256 claimedVotes;
}

/// @notice Amount of bribe tokens deposited for a given epoch
/// @param _epoch Epoch at which the bribe was deposited
/// @return boldAmount Amount of BOLD tokens deposited
/// @return bribeTokenAmount Amount of bribe tokens deposited
function bribeByEpoch(uint256 _epoch) external view returns (uint256 boldAmount, uint256 bribeTokenAmount);
/// @return remainingBoldAmount Amount of BOLD tokens that haven't been claimed yet
/// @return remainingBribeTokenAmount Amount of bribe tokens that haven't been claimed yet
/// @return claimedVotes Sum of voting power of users who have already claimed their bribes
function bribeByEpoch(uint256 _epoch)
external
view
returns (uint256 remainingBoldAmount, uint256 remainingBribeTokenAmount, uint256 claimedVotes);
/// @notice Check if a user has claimed bribes for a given epoch
/// @param _user Address of the user
/// @param _epoch Epoch at which the bribe may have been claimed by the user
Expand Down
7 changes: 7 additions & 0 deletions src/utils/VotingPower.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

function _lqtyToVotes(uint256 _lqtyAmount, uint256 _timestamp, uint256 _offset) pure returns (uint256) {
uint256 prod = _lqtyAmount * _timestamp;
return prod > _offset ? prod - _offset : 0;
}
Loading

0 comments on commit f6839c9

Please sign in to comment.