Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bonding: Treasury rewards contribution #616

Merged
merged 41 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
cc315d1
package.json: Update @openzeppelin libraries
victorges Jul 7, 2023
d42e92c
bonding: Create SortedArrays library
victorges Jul 7, 2023
2abf207
bonding: Create BondingCheckpoints contract
victorges Jul 7, 2023
a9dde9d
bonding: Checkpoint bonding state on changes
victorges Jul 7, 2023
078f348
test/bonding: Test BondingManager and Checkpoints
victorges Jul 7, 2023
22ee2db
bonding: Migrate to custom error types
victorges Jul 14, 2023
2e4157b
bonding: Allow querying unbonded+uncheckpointed accounts
victorges Jul 17, 2023
06b7100
treasury: Create treasury governance contracts
victorges Jul 17, 2023
0aa6cd0
test/treasury: Add unit test fro BondingCheckpointsVotes
victorges Jul 17, 2023
9f007ba
test/treasury: Test GovernorCountingOverridable
victorges Jul 18, 2023
2fd7abb
test/treasury: Test LivepeerGovernor
victorges Jul 20, 2023
34cb455
test/treasury: A couple additional Governor tests
victorges Jul 20, 2023
e970472
test/treasury: Rename Counting unit test mock
victorges Jul 20, 2023
a6db562
Apply suggestions from code review
victorges Jul 26, 2023
0ad3a8f
treasury: Fix storage layout situation
victorges Jul 27, 2023
bb9ff45
treasury: Move governor initial params to configs
victorges Aug 4, 2023
6984ede
bonding: Implement treasury contribution
victorges Jul 20, 2023
9aad9e2
test/bonding: Add tests for treasury contribution
victorges Jul 21, 2023
cd6d147
bonding: Update reward cut logic to match LIP
victorges Jul 21, 2023
0c41f78
bonding: Make sure we checkpoint up to once per op
victorges Aug 12, 2023
d518cd6
bonding: Make bonding checkpoints implement IVotes
victorges Aug 13, 2023
ccc3ddf
bonding: Read votes from the end of the round
victorges Aug 15, 2023
d5062fe
bonding: Make checkpoint reading revert-free
victorges Aug 17, 2023
292f954
bonding: Address minor code review comments
victorges Aug 16, 2023
4f12ce9
Merge branch 'vg/feat/bonding-checkpoints' into vg/feat/onchain-governor
victorges Aug 17, 2023
e9d3339
treasury: Migrate to the new BondingVotes contract
victorges Aug 17, 2023
f0fb8ee
treasury: Address PR comments
victorges Aug 17, 2023
9dbb0ae
bonding: Move constructor to after modifiers
victorges Aug 17, 2023
cf12cb4
Merge branch 'vg/feat/onchain-governor' into vg/feat/treasury-contrib…
victorges Aug 18, 2023
27f8930
bonding: Avoid returning payable treasury address
victorges Aug 18, 2023
ead1edb
bonding: Update treasury cut rate only on the next round
victorges Aug 18, 2023
391e444
test: Fix TicketBroker tests flakiness!
victorges Aug 18, 2023
b59a538
test/mocks: Remove mock functions that moved to other mock
victorges Aug 18, 2023
1692ec1
bonding: Implement ERC20 metadata on votes
victorges Aug 21, 2023
fecf5a2
bonding: Address PR comments
victorges Aug 22, 2023
4246474
bonding: Address BondingVotes review comments
victorges Aug 23, 2023
63d1d19
Merge branch 'vg/feat/bonding-checkpoints' into vg/feat/onchain-governor
victorges Aug 23, 2023
8c57a4d
treasury: Merge BondingCheckpoints and nits
victorges Aug 23, 2023
e2a2ee7
Merge branch 'vg/feat/onchain-governor' into vg/feat/treasury-contrib…
victorges Aug 23, 2023
a65a5b0
bonding: Move internal func to the right section
victorges Aug 23, 2023
48a9230
Merge branch 'delta' into vg/feat/treasury-contribution
victorges Aug 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 167 additions & 41 deletions contracts/bonding/BondingManager.sol

Large diffs are not rendered by default.

501 changes: 501 additions & 0 deletions contracts/bonding/BondingVotes.sol

Large diffs are not rendered by default.

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
48 changes: 48 additions & 0 deletions contracts/bonding/IBondingVotes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts-upgradeable/interfaces/IERC6372Upgradeable.sol";

import "../treasury/IVotes.sol";

/**
* @title Interface for BondingVotes
*/
interface IBondingVotes is IERC6372Upgradeable, IVotes {
error InvalidCaller(address caller, address required);
error InvalidStartRound(uint256 checkpointRound, uint256 requiredRound);
error FutureLastClaimRound(uint256 lastClaimRound, uint256 maxAllowed);
error FutureTotalStakeCheckpoint(uint256 checkpointRound, uint256 maxAllowedRound);

error FutureLookup(uint256 queryRound, uint256 maxAllowed);
error MissingEarningsPool(address transcoder, uint256 round);

// Indicates that the called function is not supported in this contract and should be performed through the
// BondingManager instead. This is mostly used for IVotes delegation methods which must be bonds instead.
error MustCallBondingManager(string bondingManagerFunction);

// BondingManager hooks

function checkpointBondingState(
address _account,
uint256 _startRound,
uint256 _bondedAmount,
address _delegateAddress,
uint256 _delegatedAmount,
uint256 _lastClaimRound,
uint256 _lastRewardRound
) external;

function checkpointTotalActiveStake(uint256 _totalStake, uint256 _round) external;

// Historical stake access functions

function hasCheckpoint(address _account) external view returns (bool);

function getTotalActiveStakeAt(uint256 _round) external view returns (uint256);

function getBondingStateAt(address _account, uint256 _round)
external
view
returns (uint256 amount, address delegateAddress);
}
39 changes: 39 additions & 0 deletions contracts/bonding/libraries/EarningsPoolLIP36.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,43 @@ library EarningsPoolLIP36 {
PreciseMathUtils.percOf(prevCumulativeRewardFactor, _rewards, earningsPool.totalStake)
);
}

/**
* @notice Calculates a delegator's cumulative stake and fees using the LIP-36 earnings claiming algorithm.
* @param _startPool The earning pool from the start round for the start cumulative factors. Normally this is the
* earning pool from the {Delegator-lastclaimRound}+1 round, as the round where `bondedAmount` was measured.
* @param _endPool The earning pool from the end round for the end cumulative factors
* @param _stake The delegator initial stake before including earned rewards. Normally the {Delegator-bondedAmount}
* @param _fees The delegator's initial fees before including earned fees
* @return cStake , cFees where cStake is the delegator's cumulative stake including earned rewards and cFees is the
* delegator's cumulative fees including earned fees
*/
function delegatorCumulativeStakeAndFees(
EarningsPool.Data memory _startPool,
EarningsPool.Data memory _endPool,
uint256 _stake,
uint256 _fees
) internal pure returns (uint256 cStake, uint256 cFees) {
// If the start cumulativeRewardFactor is 0 set the default value to PreciseMathUtils.percPoints(1, 1)
if (_startPool.cumulativeRewardFactor == 0) {
_startPool.cumulativeRewardFactor = PreciseMathUtils.percPoints(1, 1);
}

// If the end cumulativeRewardFactor is 0 set the default value to PreciseMathUtils.percPoints(1, 1)
if (_endPool.cumulativeRewardFactor == 0) {
_endPool.cumulativeRewardFactor = PreciseMathUtils.percPoints(1, 1);
}

cFees = _fees.add(
PreciseMathUtils.percOf(
_stake,
_endPool.cumulativeFeeFactor.sub(_startPool.cumulativeFeeFactor),
_startPool.cumulativeRewardFactor
)
);

cStake = PreciseMathUtils.percOf(_stake, _endPool.cumulativeRewardFactor, _startPool.cumulativeRewardFactor);

return (cStake, cFees);
}
}
81 changes: 81 additions & 0 deletions contracts/bonding/libraries/SortedArrays.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "../../libraries/MathUtils.sol";

import "@openzeppelin/contracts/utils/Arrays.sol";

/**
* @title SortedArrays
* @dev Handles maintaining and looking up on sorted uint256 arrays.
*/
library SortedArrays {
using Arrays for uint256[];

error DecreasingValues(uint256 newValue, uint256 lastValue);

/**
* @notice Searches a sorted _array and returns the last element to be lower or equal to _val. If there is no such
* element (all elements in array are higher than the searched element), the array length is returned.
*
* @dev This basically converts OpenZeppelin's {Arrays-findUpperBound} into findLowerBound, meaning it also uses a
* binary search in the worst case after trying some shortcuts. Worst case time complexity is O(log n). The only
* change being that the returned index points to the element lower or equal to _val, instead of higher or equal.
* @param _array Array to search in
* @param _val Value to search for
* @return lower Index of the lower bound found in array
*/
function findLowerBound(uint256[] storage _array, uint256 _val) internal view returns (uint256) {
uint256 len = _array.length;
if (len == 0) {
return 0;
}

if (_array[len - 1] <= _val) {
return len - 1;
}

uint256 upperIdx = _array.findUpperBound(_val);

// we already checked the last element above so the upper will always be inside the array
assert(upperIdx < len);

// the exact value we were searching is in the array
if (_array[upperIdx] == _val) {
return upperIdx;
}

// a 0 idx means that the first elem is already higher than the searched value (and not equal, checked above)
if (upperIdx == 0) {
return len;
}

// the element at upperIdx is the first element higher than the value we want, so return the previous element
return upperIdx - 1;
}

/**
* @notice Pushes a value into an already sorted array.
* @dev Values must be pushed in increasing order as to avoid shifting values in the array. This function only
* guarantees that the pushed value will not create duplicates nor make the array out of order.
* @param array Array to push the value into
* @param val Value to push into array. Must be greater than or equal to the highest (last) element.
*/
function pushSorted(uint256[] storage array, uint256 val) internal {
if (array.length == 0) {
array.push(val);
} else {
uint256 last = array[array.length - 1];

// values must be pushed in order
if (val < last) {
revert DecreasingValues(val, last);
}

// don't push duplicate values
if (val != last) {
array.push(val);
}
}
}
}
87 changes: 87 additions & 0 deletions contracts/test/TestSortedArrays.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "./helpers/truffle/Assert.sol";
import "./helpers/RevertProxy.sol";
import "./mocks/SortedArraysFixture.sol";

contract TestSortedArrays {
RevertProxy proxy;
SortedArraysFixture fixture;

function beforeAll() public {
proxy = new RevertProxy();
}

function beforeEach() public {
fixture = new SortedArraysFixture();
}

function test_pushSorted_addsElements() public {
fixture.pushSorted(3);
Assert.equal(fixture.length(), 1, "incorrect array length");
Assert.equal(fixture.array(0), 3, "incorrect value in array");

fixture.pushSorted(4);
Assert.equal(fixture.length(), 2, "incorrect array length");
Assert.equal(fixture.array(0), 3, "incorrect value in array");
Assert.equal(fixture.array(1), 4, "incorrect value in array");
}

function test_pushSorted_avoidsDuplicates() public {
fixture.pushSorted(4);
Assert.equal(fixture.length(), 1, "incorrect array length");
Assert.equal(fixture.array(0), 4, "incorrect value in array");

fixture.pushSorted(4);
Assert.equal(fixture.length(), 1, "incorrect array length");
}

function test_pushSorted_revertsOnDecreasing() public {
fixture.pushSorted(4);
Assert.equal(fixture.length(), 1, "incorrect array length");
Assert.equal(fixture.array(0), 4, "incorrect value in array");

SortedArraysFixture(address(proxy)).pushSorted(3);
bool ok = proxy.execute(address(fixture));
Assert.isFalse(ok, "did not revert");
}

function test_findLowerBound_lowerThanElement() public {
fixture.pushSorted(2);
fixture.pushSorted(4);
fixture.pushSorted(7);
fixture.pushSorted(11);

Assert.equal(fixture.findLowerBound(3), 0, "found incorrect element");
Assert.equal(fixture.findLowerBound(6), 1, "found incorrect element");
Assert.equal(fixture.findLowerBound(10), 2, "found incorrect element");
Assert.equal(fixture.findLowerBound(15), 3, "found incorrect element");
}

function test_findLowerBound_exactElement() public {
fixture.pushSorted(3);
fixture.pushSorted(5);
fixture.pushSorted(8);
fixture.pushSorted(13);

Assert.equal(fixture.findLowerBound(3), 0, "found incorrect element");
Assert.equal(fixture.findLowerBound(5), 1, "found incorrect element");
Assert.equal(fixture.findLowerBound(8), 2, "found incorrect element");
Assert.equal(fixture.findLowerBound(13), 3, "found incorrect element");
}

function test_findLowerBound_returnsLengthOnEmpty() public {
Assert.equal(fixture.length(), 0, "incorrect array length");
Assert.equal(fixture.findLowerBound(3), 0, "incorrect return on empty array");
}

function test_findLowerBound_returnsLengthOnNotFound() public {
fixture.pushSorted(8);
fixture.pushSorted(13);

Assert.equal(fixture.findLowerBound(22), 1, "found incorrect element");
// looking for a value lower than min should return the array length
Assert.equal(fixture.findLowerBound(5), 2, "incorrect return on not found");
}
}
105 changes: 105 additions & 0 deletions contracts/test/mocks/BondingManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,116 @@ import "./GenericMock.sol";
contract BondingManagerMock is GenericMock {
event UpdateTranscoderWithFees(address transcoder, uint256 fees, uint256 round);

struct EarningsPoolMock {
uint256 totalStake;
uint256 transcoderRewardCut;
uint256 transcoderFeeShare;
uint256 cumulativeRewardFactor;
uint256 cumulativeFeeFactor;
}

struct DelegatorMock {
uint256 bondedAmount;
uint256 fees;
address delegateAddress;
uint256 delegatedAmount;
uint256 startRound;
uint256 lastClaimRound;
uint256 nextUnbondingLockId;
}

mapping(address => mapping(uint256 => EarningsPoolMock)) private earningPoolMocks;

mapping(address => DelegatorMock) private delegatorMocks;

function updateTranscoderWithFees(
address _transcoder,
uint256 _fees,
uint256 _round
) external {
emit UpdateTranscoderWithFees(_transcoder, _fees, _round);
}

function getTranscoderEarningsPoolForRound(address _transcoder, uint256 _round)
public
view
returns (
uint256 totalStake,
uint256 transcoderRewardCut,
uint256 transcoderFeeShare,
uint256 cumulativeRewardFactor,
uint256 cumulativeFeeFactor
)
{
EarningsPoolMock storage pool = earningPoolMocks[_transcoder][_round];

totalStake = pool.totalStake;
transcoderRewardCut = pool.transcoderRewardCut;
transcoderFeeShare = pool.transcoderFeeShare;
cumulativeRewardFactor = pool.cumulativeRewardFactor;
cumulativeFeeFactor = pool.cumulativeFeeFactor;
}

function setMockTranscoderEarningsPoolForRound(
address _transcoder,
uint256 _round,
uint256 _totalStake,
uint256 _transcoderRewardCut,
uint256 _transcoderFeeShare,
uint256 _cumulativeRewardFactor,
uint256 _cumulativeFeeFactor
) external {
earningPoolMocks[_transcoder][_round] = EarningsPoolMock({
totalStake: _totalStake,
transcoderRewardCut: _transcoderRewardCut,
transcoderFeeShare: _transcoderFeeShare,
cumulativeRewardFactor: _cumulativeRewardFactor,
cumulativeFeeFactor: _cumulativeFeeFactor
});
}

function setMockDelegator(
address _delegator,
uint256 _bondedAmount,
uint256 _fees,
address _delegateAddress,
uint256 _delegatedAmount,
uint256 _startRound,
uint256 _lastClaimRound,
uint256 _nextUnbondingLockId
) external {
delegatorMocks[_delegator] = DelegatorMock({
bondedAmount: _bondedAmount,
fees: _fees,
delegateAddress: _delegateAddress,
delegatedAmount: _delegatedAmount,
startRound: _startRound,
lastClaimRound: _lastClaimRound,
nextUnbondingLockId: _nextUnbondingLockId
});
}

function getDelegator(address _delegator)
public
view
returns (
uint256 bondedAmount,
uint256 fees,
address delegateAddress,
uint256 delegatedAmount,
uint256 startRound,
uint256 lastClaimRound,
uint256 nextUnbondingLockId
)
{
DelegatorMock storage del = delegatorMocks[_delegator];

bondedAmount = del.bondedAmount;
fees = del.fees;
delegateAddress = del.delegateAddress;
delegatedAmount = del.delegatedAmount;
startRound = del.startRound;
lastClaimRound = del.lastClaimRound;
nextUnbondingLockId = del.nextUnbondingLockId;
}
}
Loading