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: Implement active stake checkpointing #614

Merged
merged 17 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
131 changes: 93 additions & 38 deletions contracts/bonding/BondingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import "../token/ILivepeerToken.sol";
import "../token/IMinter.sol";
import "../rounds/IRoundsManager.sol";
import "../snapshots/IMerkleSnapshot.sol";
import "./IBondingVotes.sol";

import "@openzeppelin/contracts/utils/math/SafeMath.sol";

Expand Down Expand Up @@ -123,6 +124,11 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
_;
}

modifier autoCheckpoint(address _account) {
_;
_checkpointBondingState(_account, delegators[_account], transcoders[_account]);
}

/**
* @notice BondingManager constructor. Only invokes constructor of base Manager contract with provided Controller address
* @dev This constructor will not initialize any state variables besides `controller`. The following setter functions
Expand Down Expand Up @@ -198,6 +204,15 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
rebondFromUnbondedWithHint(_to, _unbondingLockId, address(0), address(0));
}

/**
* @notice Checkpoints the bonding state for a given account.
* @dev This is to allow checkpointing an account that has an inconsistent checkpoint with its current state.
* @param _account The account to make the checkpoint for
*/
function checkpointBondingState(address _account) external {
_checkpointBondingState(_account, delegators[_account], transcoders[_account]);
}

/**
* @notice Withdraws tokens for an unbonding lock that has existed through an unbonding period
* @param _unbondingLockId ID of unbonding lock to withdraw with
Expand Down Expand Up @@ -347,7 +362,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
address _finder,
uint256 _slashAmount,
uint256 _finderFee
) external whenSystemNotPaused onlyVerifier {
) external whenSystemNotPaused onlyVerifier autoClaimEarnings(_transcoder) autoCheckpoint(_transcoder) {
Delegator storage del = delegators[_transcoder];

if (del.bondedAmount > 0) {
Expand Down Expand Up @@ -395,7 +410,12 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
* @notice Claim token pools shares for a delegator from its lastClaimRound through the end round
* @param _endRound The last round for which to claim token pools shares for a delegator
*/
function claimEarnings(uint256 _endRound) external whenSystemNotPaused currentRoundInitialized {
function claimEarnings(uint256 _endRound)
external
whenSystemNotPaused
currentRoundInitialized
autoCheckpoint(msg.sender)
{
// Silence unused param compiler warning
_endRound;

Expand All @@ -407,6 +427,8 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
*/
function setCurrentRoundTotalActiveStake() external onlyRoundsManager {
currentRoundTotalActiveStake = nextRoundTotalActiveStake;

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

/**
Expand Down Expand Up @@ -546,6 +568,9 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
}

emit Bond(_to, currentDelegate, _owner, _amount, del.bondedAmount);

// the `autoCheckpoint` modifier has been replaced with its internal function as a `Stack too deep` error work-around
_checkpointBondingState(_owner, del, transcoders[_owner]);
}

/**
Expand Down Expand Up @@ -669,7 +694,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
uint256 _amount,
address _newPosPrev,
address _newPosNext
) public whenSystemNotPaused currentRoundInitialized autoClaimEarnings(msg.sender) {
) public whenSystemNotPaused currentRoundInitialized autoClaimEarnings(msg.sender) autoCheckpoint(msg.sender) {
require(delegatorStatus(msg.sender) == DelegatorStatus.Bonded, "caller must be bonded");

Delegator storage del = delegators[msg.sender];
Expand Down Expand Up @@ -766,6 +791,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
public
whenSystemNotPaused
currentRoundInitialized
autoCheckpoint(msg.sender)
{
uint256 currentRound = roundsManager().currentRound();

Expand Down Expand Up @@ -1120,7 +1146,8 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
* @notice Return a delegator's cumulative stake and fees using the LIP-36 earnings claiming algorithm
* @param _transcoder Storage pointer to a transcoder struct for a delegator's delegate
* @param _startRound The round for the start cumulative factors
* @param _endRound The round for the end cumulative factors
* @param _endRound The round for the end cumulative factors. Normally this is the current round as historical
* lookup is only supported through BondingVotes
* @param _stake The delegator's initial stake before including earned rewards
* @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
Expand All @@ -1134,31 +1161,10 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
) internal view returns (uint256 cStake, uint256 cFees) {
// Fetch start cumulative factors
EarningsPool.Data memory startPool = cumulativeFactorsPool(_transcoder, _startRound);

// 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);
}

// Fetch end cumulative factors
EarningsPool.Data memory endPool = latestCumulativeFactorsPool(_transcoder, _endRound);

// 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);
return EarningsPoolLIP36.delegatorCumulativeStakeAndFees(startPool, endPool, _stake, _fees);
}

/**
Expand Down Expand Up @@ -1207,18 +1213,33 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
uint256 _amount,
address _newPosPrev,
address _newPosNext
) internal autoCheckpoint(_delegate) {
return increaseTotalStakeUncheckpointed(_delegate, _amount, _newPosPrev, _newPosNext);
}

/**
* @dev Implementation of increaseTotalStake that does not checkpoint the caller, to be used by functions that
* guarantee the checkpointing themselves.
*/
function increaseTotalStakeUncheckpointed(
address _delegate,
uint256 _amount,
address _newPosPrev,
address _newPosNext
) internal {
Transcoder storage t = transcoders[_delegate];

uint256 currStake = transcoderTotalStake(_delegate);
uint256 newStake = currStake.add(_amount);

if (isRegisteredTranscoder(_delegate)) {
uint256 currStake = transcoderTotalStake(_delegate);
uint256 newStake = currStake.add(_amount);
uint256 currRound = roundsManager().currentRound();
uint256 nextRound = currRound.add(1);

// If the transcoder is already in the active set update its stake and return
if (transcoderPool.contains(_delegate)) {
transcoderPool.updateKey(_delegate, newStake, _newPosPrev, _newPosNext);
nextRoundTotalActiveStake = nextRoundTotalActiveStake.add(_amount);
Transcoder storage t = transcoders[_delegate];

// currStake (the transcoder's delegatedAmount field) will reflect the transcoder's stake from lastActiveStakeUpdateRound
// because it is updated every time lastActiveStakeUpdateRound is updated
Expand All @@ -1237,7 +1258,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
}

// Increase delegate's delegated amount
delegators[_delegate].delegatedAmount = delegators[_delegate].delegatedAmount.add(_amount);
delegators[_delegate].delegatedAmount = newStake;
}

/**
Expand All @@ -1250,16 +1271,18 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
uint256 _amount,
address _newPosPrev,
address _newPosNext
) internal {
) internal autoCheckpoint(_delegate) {
Transcoder storage t = transcoders[_delegate];

uint256 currStake = transcoderTotalStake(_delegate);
uint256 newStake = currStake.sub(_amount);

if (transcoderPool.contains(_delegate)) {
uint256 currStake = transcoderTotalStake(_delegate);
uint256 newStake = currStake.sub(_amount);
uint256 currRound = roundsManager().currentRound();
uint256 nextRound = currRound.add(1);

transcoderPool.updateKey(_delegate, newStake, _newPosPrev, _newPosNext);
nextRoundTotalActiveStake = nextRoundTotalActiveStake.sub(_amount);
Transcoder storage t = transcoders[_delegate];

// currStake (the transcoder's delegatedAmount field) will reflect the transcoder's stake from lastActiveStakeUpdateRound
// because it is updated every time lastActiveStakeUpdateRound is updated
Expand All @@ -1274,7 +1297,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
}

// Decrease old delegate's delegated amount
delegators[_delegate].delegatedAmount = delegators[_delegate].delegatedAmount.sub(_amount);
delegators[_delegate].delegatedAmount = newStake;
}

/**
Expand Down Expand Up @@ -1342,7 +1365,8 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {

/**
* @dev Update a transcoder with rewards and update the transcoder pool with an optional list hint if needed.
* See SortedDoublyLL.sol for details on list hints
* See SortedDoublyLL.sol for details on list hints. This function updates the transcoder state but does not
* checkpoint it as it assumes the caller will ensure that.
* @param _transcoder Address of transcoder
* @param _rewards Amount of rewards
* @param _round Round that transcoder is updated
Expand Down Expand Up @@ -1378,11 +1402,14 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
// the earnings claiming algorithm and instead that amount is accounted for in the transcoder's cumulativeRewards field
earningsPool.updateCumulativeRewardFactor(prevEarningsPool, delegatorsRewards);
// Update transcoder's total stake with rewards
increaseTotalStake(_transcoder, _rewards, _newPosPrev, _newPosNext);
increaseTotalStakeUncheckpointed(_transcoder, _rewards, _newPosPrev, _newPosNext);
}

/**
* @dev Update a delegator with token pools shares from its lastClaimRound through a given round
*
* Notice that this function updates the delegator storage but does not checkpoint its state. Since it is internal
* it assumes the top-level caller will checkpoint it instead.
* @param _delegator Delegator address
* @param _endRound The last round for which to update a delegator's stake with earnings pool shares
* @param _lastClaimRound The round for which a delegator has last claimed earnings
Expand Down Expand Up @@ -1456,7 +1483,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
uint256 _unbondingLockId,
address _newPosPrev,
address _newPosNext
) internal {
) internal autoCheckpoint(_delegator) {
Delegator storage del = delegators[_delegator];
UnbondingLock storage lock = del.unbondingLocks[_unbondingLockId];

Expand All @@ -1474,6 +1501,30 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
emit Rebond(del.delegateAddress, _delegator, _unbondingLockId, amount);
}

/**
* @notice Checkpoints a delegator state after changes, to be used for historical voting power calculations in
* on-chain governor logic.
*/
function _checkpointBondingState(
address _owner,
Delegator storage _delegator,
Transcoder storage _transcoder
) internal {
// start round refers to the round where the checkpointed stake will be active. The actual `startRound` value
// in the delegators doesn't get updated on bond or claim earnings though, so we use currentRound() + 1
// which is the only guaranteed round where the currently stored stake will be active.
uint256 startRound = roundsManager().currentRound() + 1;
bondingVotes().checkpointBondingState(
_owner,
startRound,
_delegator.bondedAmount,
_delegator.delegateAddress,
_delegator.delegatedAmount,
_delegator.lastClaimRound,
_transcoder.lastRewardRound
);
}

/**
* @dev Return LivepeerToken interface
* @return Livepeer token contract registered with Controller
Expand Down Expand Up @@ -1506,6 +1557,10 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
return IRoundsManager(controller.getContract(keccak256("RoundsManager")));
}

function bondingVotes() internal view returns (IBondingVotes) {
return IBondingVotes(controller.getContract(keccak256("BondingVotes")));
}

function _onlyTicketBroker() internal view {
require(msg.sender == controller.getContract(keccak256("TicketBroker")), "caller must be TicketBroker");
}
Expand Down
Loading
Loading