Skip to content

Commit

Permalink
bonding: Treasury rewards contribution (#616)
Browse files Browse the repository at this point in the history
* package.json: Update @OpenZeppelin libraries

Will be required for new checkpoints code and later governor
implementation.

* bonding: Create SortedArrays library

Used for checkpointing logic in bonding state checkpoints

* bonding: Create BondingCheckpoints contract

Handles historic checkpointing ("snapshotting") and lookup
of the bonding state.

* bonding: Checkpoint bonding state on changes

* test/bonding: Test BondingManager and Checkpoints

 - unit tests
 - integration tests
 - gas-report
 - fork test for upgrade

* bonding: Migrate to custom error types

* bonding: Allow querying unbonded+uncheckpointed accounts

This will provide a cleaner experience using governance
tools, since users that don't have any stake won't get
errors when querying their voting power. For users that
do have stake, we will make sure to checkpoint them on
first deploy.

* treasury: Create treasury governance contracts

* test/treasury: Add unit test fro BondingCheckpointsVotes

* test/treasury: Test GovernorCountingOverridable

* test/treasury: Test LivepeerGovernor

* test/treasury: A couple additional Governor tests

100% coverage 😎

* test/treasury: Rename Counting unit test mock

"Harness" seems to make more sense, I could only think
of that now.

* Apply suggestions from code review

Co-authored-by: Chase Adams <[email protected]>

* treasury: Fix storage layout situation

* treasury: Move governor initial params to configs

* bonding: Implement treasury contribution

* test/bonding: Add tests for treasury contribution

* bonding: Update reward cut logic to match LIP

It is a little less exact (might overmint on the
last reward call), but the simpler logic might be
just worth it.

* bonding: Make sure we checkpoint up to once per op

* bonding: Make bonding checkpoints implement IVotes

* bonding: Read votes from the end of the round

Meaning the start of next round instead of the
current round. This is more compatible with the
way OZ expects the timepoints to work in the clock
and snapshots on the Governor framework.

* bonding: Make checkpoint reading revert-free

This changes the BondingVotes implementation to
stop having so many reverts on supposedly invalid states.

Instead, redefine the voting power (and checkpointed stake)
as being zero before the first round to be checkpointed.

This had other implications in the code like removing
changes in BondingManager to make the state complete
(always having an earnings pool on lastClaimRound).
Instead, the BondingVotes implementaiton is resilient
to that as well and just assumes reward() had never
been called.

This also fixed the redundant reverts between BondingVotes
and SortedArrays, as now there are almost no reverts.

* bonding: Address minor code review comments

* treasury: Migrate to the new BondingVotes contract

* treasury: Address PR comments

* bonding: Move constructor to after modifiers

Just for consistency with other contracts.
Some docs as a bonus

* bonding: Avoid returning payable treasury address

* bonding: Update treasury cut rate only on the next round

* test: Fix TicketBroker tests flakiness!

* test/mocks: Remove mock functions that moved to other mock

* bonding: Implement ERC20 metadata on votes

This is to increase compatibility with some tools
out there like Tally, which require only these
functions from the ERC20 spec, not the full implementation.

So we can have these from the get-go to make things
easier if we want to make something with them.

* bonding: Address PR comments

* bonding: Address BondingVotes review comments

* treasury: Merge BondingCheckpoints and nits

* bonding: Move internal func to the right section

Also add docs

---------

Co-authored-by: Chase Adams <[email protected]>
  • Loading branch information
victorges and 0xcadams committed Oct 11, 2023
1 parent ce28eaa commit 62b3e19
Show file tree
Hide file tree
Showing 6 changed files with 414 additions and 5 deletions.
81 changes: 78 additions & 3 deletions contracts/bonding/BondingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
// in the pool are locked into the active set for round N + 1
SortedDoublyLL.Data private transcoderPool;

// The % of newly minted rewards to be routed to the treasury. Represented as a PreciseMathUtils percPoint value.
uint256 public treasuryRewardCutRate;
// The value for `treasuryRewardCutRate` to be set on the next round initialization.
uint256 public nextRoundTreasuryRewardCutRate;

// If the balance of the treasury in LPT is above this value, automatic treasury contributions will halt.
uint256 public treasuryBalanceCeiling;

// Check if sender is TicketBroker
modifier onlyTicketBroker() {
_onlyTicketBroker();
Expand Down Expand Up @@ -150,6 +158,27 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
emit ParameterUpdate("unbondingPeriod");
}

/**
* @notice Set treasury reward cut rate. Only callable by Controller owner. Notice that the change will only be
* effective on the next round.
* @param _cutRate Percentage of newly minted rewards to route to the treasury. Must be a valid PreciseMathUtils
* percentage (<100% specified with 27-digits precision).
*/
function setTreasuryRewardCutRate(uint256 _cutRate) external onlyControllerOwner {
_setTreasuryRewardCutRate(_cutRate);
}

/**
* @notice Set treasury balance ceiling. Only callable by Controller owner
* @param _ceiling Balance at which treasury reward contributions should halt. Specified in LPT fractional units
* (18-digit precision).
*/
function setTreasuryBalanceCeiling(uint256 _ceiling) external onlyControllerOwner {
treasuryBalanceCeiling = _ceiling;

emit ParameterUpdate("treasuryBalanceCeiling");
}

/**
* @notice Set maximum number of active transcoders. Only callable by Controller owner
* @param _numActiveTranscoders Number of active transcoders
Expand Down Expand Up @@ -321,6 +350,11 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
totalStake,
currentRoundTotalActiveStake
);

// Deduct what would have been the treasury rewards
uint256 treasuryRewards = MathUtils.percOf(rewards, treasuryRewardCutRate);
rewards = rewards.sub(treasuryRewards);

uint256 transcoderCommissionRewards = MathUtils.percOf(rewards, earningsPool.transcoderRewardCut);
uint256 delegatorsRewards = rewards.sub(transcoderCommissionRewards);

Expand Down Expand Up @@ -428,6 +462,12 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
function setCurrentRoundTotalActiveStake() external onlyRoundsManager {
currentRoundTotalActiveStake = nextRoundTotalActiveStake;

if (nextRoundTreasuryRewardCutRate != treasuryRewardCutRate) {
treasuryRewardCutRate = nextRoundTreasuryRewardCutRate;
// The treasury cut rate changes in a delayed fashion so we want to emit the parameter update event here
emit ParameterUpdate("treasuryRewardCutRate");
}

bondingVotes().checkpointTotalActiveStake(currentRoundTotalActiveStake, roundsManager().currentRound());
}

Expand Down Expand Up @@ -828,16 +868,35 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
earningsPool.setStake(t.earningsPoolPerRound[lastUpdateRound].totalStake);
}

if (treasuryBalanceCeiling > 0) {
uint256 treasuryBalance = livepeerToken().balanceOf(treasury());
if (treasuryBalance >= treasuryBalanceCeiling && nextRoundTreasuryRewardCutRate > 0) {
// halt treasury contributions until the cut rate param is updated again
_setTreasuryRewardCutRate(0);
}
}

// Create reward based on active transcoder's stake relative to the total active stake
// rewardTokens = (current mintable tokens for the round * active transcoder stake) / total active stake
uint256 rewardTokens = minter().createReward(earningsPool.totalStake, currentRoundTotalActiveStake);
IMinter mtr = minter();
uint256 totalRewardTokens = mtr.createReward(earningsPool.totalStake, currentRoundTotalActiveStake);
uint256 treasuryRewards = PreciseMathUtils.percOf(totalRewardTokens, treasuryRewardCutRate);
if (treasuryRewards > 0) {
address trsry = treasury();

mtr.trustedTransferTokens(trsry, treasuryRewards);

updateTranscoderWithRewards(msg.sender, rewardTokens, currentRound, _newPosPrev, _newPosNext);
emit TreasuryReward(msg.sender, trsry, treasuryRewards);
}

uint256 transcoderRewards = totalRewardTokens.sub(treasuryRewards);

updateTranscoderWithRewards(msg.sender, transcoderRewards, currentRound, _newPosPrev, _newPosNext);

// Set last round that transcoder called reward
t.lastRewardRound = currentRound;

emit Reward(msg.sender, rewardTokens);
emit Reward(msg.sender, transcoderRewards);
}

/**
Expand Down Expand Up @@ -1110,6 +1169,18 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
return delegators[_delegator].unbondingLocks[_unbondingLockId].withdrawRound > 0;
}

/**
* @dev Internal version of setTreasuryRewardCutRate. Sets the treasury reward cut rate for the next round and emits
* corresponding event.
*/
function _setTreasuryRewardCutRate(uint256 _cutRate) internal {
require(PreciseMathUtils.validPerc(_cutRate), "_cutRate is invalid precise percentage");

nextRoundTreasuryRewardCutRate = _cutRate;

emit ParameterUpdate("nextRoundTreasuryRewardCutRate");
}

/**
* @notice Return an EarningsPool.Data struct with cumulative factors for a given round that are rescaled if needed
* @param _transcoder Storage pointer to a transcoder struct
Expand Down Expand Up @@ -1569,6 +1640,10 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
return IRoundsManager(controller.getContract(keccak256("RoundsManager")));
}

function treasury() internal view returns (address) {
return controller.getContract(keccak256("Treasury"));
}

function bondingVotes() internal view returns (IBondingVotes) {
return IBondingVotes(controller.getContract(keccak256("BondingVotes")));
}
Expand Down
1 change: 1 addition & 0 deletions contracts/bonding/IBondingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface IBondingManager {
event TranscoderDeactivated(address indexed transcoder, uint256 deactivationRound);
event TranscoderSlashed(address indexed transcoder, address finder, uint256 penalty, uint256 finderReward);
event Reward(address indexed transcoder, uint256 amount);
event TreasuryReward(address indexed transcoder, address treasury, uint256 amount);
event Bond(
address indexed newDelegate,
address indexed oldDelegate,
Expand Down
5 changes: 5 additions & 0 deletions contracts/test/mocks/MinterMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import "./GenericMock.sol";

contract MinterMock is GenericMock {
event TrustedWithdrawETH(address to, uint256 amount);
event TrustedTransferTokens(address to, uint256 amount);

function trustedWithdrawETH(address _to, uint256 _amount) external {
emit TrustedWithdrawETH(_to, _amount);
}

function trustedTransferTokens(address _to, uint256 _amount) external {
emit TrustedTransferTokens(_to, _amount);
}
}
Loading

0 comments on commit 62b3e19

Please sign in to comment.