Skip to content

Commit

Permalink
bonding: Make bonding checkpoints implement IVotes
Browse files Browse the repository at this point in the history
  • Loading branch information
victorges committed Aug 15, 2023
1 parent c262d6a commit 8b927b1
Show file tree
Hide file tree
Showing 20 changed files with 821 additions and 684 deletions.
14 changes: 7 additions & 7 deletions contracts/bonding/BondingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import "../token/ILivepeerToken.sol";
import "../token/IMinter.sol";
import "../rounds/IRoundsManager.sol";
import "../snapshots/IMerkleSnapshot.sol";
import "./IBondingCheckpoints.sol";
import "./IBondingVotes.sol";

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

Expand Down Expand Up @@ -453,7 +453,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
function setCurrentRoundTotalActiveStake() external onlyRoundsManager {
currentRoundTotalActiveStake = nextRoundTotalActiveStake;

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

/**
Expand Down Expand Up @@ -614,7 +614,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
// 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;
bondingCheckpoints().checkpointBondingState(
bondingVotes().checkpointBondingState(
_owner,
startRound,
_delegator.bondedAmount,
Expand All @@ -628,7 +628,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
/**
* @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.
* Implemented as a deploy utility to checkpoint the existing state when deploying the BondingCheckpoints contract.
* Implemented as a deploy utility to checkpoint the existing state when deploying the BondingVotes contract.
* @param _account The account to initialize the bonding checkpoint for
*/
function checkpointBondingState(address _account) public {
Expand Down Expand Up @@ -1228,7 +1228,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
* @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. Normally this is the current round as historical
* lookup is only supported through BondingCheckpoints
* 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 Down Expand Up @@ -1618,8 +1618,8 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
return payable(controller.getContract(keccak256("Treasury")));
}

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

function _onlyTicketBroker() internal view {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import "../rounds/IRoundsManager.sol";
import "./BondingManager.sol";

/**
* @title BondingCheckpoints
* @title BondingVotes
* @dev Checkpointing logic for BondingManager state for historical stake calculations.
*/
contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
contract BondingVotes is ManagerProxyTarget, IBondingVotes {
using SortedArrays for uint256[];

constructor(address _controller) Manager(_controller) {}
Expand Down Expand Up @@ -55,28 +55,33 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
* for a given round, find the checkpoint with the highest start round that is lower or equal to the queried round
* ({SortedArrays-findLowerBound}) and then fetch the specific checkpoint on the data mapping.
*/
struct BondingCheckpointsByRound {
struct BondingVotesByRound {
uint256[] startRounds;
mapping(uint256 => BondingCheckpoint) data;
}

/**
* @dev Checkpoints by account (delegators and transcoders).
* @dev Stores a list of checkpoints for the total active stake, queryable and mapped by round. Notce that
* differently from bonding checkpoints, it's only accessible on the specific round. To access the checkpoint for a
* given round, look for the checkpoint in the {data}} and if it's zero ensure the round was actually checkpointed on
* the {rounds} array ({SortedArrays-findLowerBound}).
*/
mapping(address => BondingCheckpointsByRound) private bondingCheckpoints;
struct TotalActiveStakeByRound {
uint256[] rounds;
mapping(uint256 => uint256) data;
}

/**
* @dev Rounds in which we have checkpoints for the total active stake. This and {totalActiveStakeCheckpoints} are
* handled in the same wat that {BondingCheckpointsByRound}, with rounds stored and queried on this array and
* checkpointed value stored and retrieved from the mapping.
* @dev Checkpoints by account (delegators and transcoders).
*/
uint256[] totalStakeCheckpointRounds;
mapping(address => BondingVotesByRound) private bondingCheckpoints;
/**
* @dev See {totalStakeCheckpointRounds} above.
* @dev Total active stake checkpoints.
*/
mapping(uint256 => uint256) private totalActiveStakeCheckpoints;
TotalActiveStakeByRound private totalStakeCheckpoints;

// IERC6372 interface implementation
// IVotes interface implementation.
// These should not access any storage directly but proxy to the bonding state functions.

/**
* @notice Clock is set to match the current round, which is the checkpointing
Expand All @@ -94,6 +99,72 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
return "mode=livepeer_round";
}

/**
* @notice Returns the current amount of votes that `_account` has.
*/
function getVotes(address _account) external view returns (uint256) {
return getPastVotes(_account, clock());
}

/**
* @notice Returns the amount of votes that `_account` had at a specific moment in the past. If the `clock()` is
* configured to use block numbers, this will return the value at the end of the corresponding block.
*/
function getPastVotes(address _account, uint256 _round) public view returns (uint256) {
(uint256 amount, ) = getBondingStateAt(_account, _round);
return amount;
}

/**
* @notice Returns the total supply of votes available at a specific round in the past.
* @dev This value is the sum of all *active* stake, which is not necessarily the sum of all voting power.
* Bonded stake that is not part of the top 100 active transcoder set is still given voting power, but is not
* considered here.
*/
function getPastTotalSupply(uint256 _round) external view returns (uint256) {
return getTotalActiveStakeAt(_round);
}

/**
* @notice Returns the delegate that _account has chosen. This means the delegated transcoder address in case of
* delegators, and the account's own address for transcoders (self-delegated).
*/
function delegates(address _account) external view returns (address) {
return delegatedAt(_account, clock());
}

/**
* @notice Returns the delegate that _account had chosen in a specific round in the past. See `delegates()` above
* for more details.
* @dev This is an addition to the IERC5805 interface to support our custom vote counting logic that allows
* delegators to override their transcoders votes. See {GovernorVotesBondingVotes-_handleVoteOverrides}.
*/
function delegatedAt(address _account, uint256 _round) public view returns (address) {
(, address delegateAddress) = getBondingStateAt(_account, _round);
return delegateAddress;
}

/**
* @notice Delegation through BondingVotes is not supported.
*/
function delegate(address) external pure {
revert MustCallBondingManager("bond");
}

/**
* @notice Delegation through BondingVotes is not supported.
*/
function delegateBySig(
address,
uint256,
uint256,
uint8,
bytes32,
bytes32
) external pure {
revert MustCallBondingManager("bondFor");
}

// BondingManager checkpointing hooks

/**
Expand All @@ -117,33 +188,68 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
uint256 _lastClaimRound,
uint256 _lastRewardRound
) public virtual onlyBondingManager {
if (_startRound > clock() + 1) {
revert FutureCheckpoint(_startRound, clock() + 1);
if (_startRound != clock() + 1) {
revert InvalidCheckpoint(_startRound, clock() + 1);
} else if (_lastClaimRound >= _startRound) {
revert FutureLastClaimRound(_lastClaimRound, _startRound - 1);
}

BondingCheckpointsByRound storage checkpoints = bondingCheckpoints[_account];
BondingCheckpoint memory previous;
if (hasCheckpoint(_account)) {
previous = getBondingCheckpointAt(_account, _startRound);
}

checkpoints.data[_startRound] = BondingCheckpoint({
BondingVotesByRound storage checkpoints = bondingCheckpoints[_account];

BondingCheckpoint memory bond = BondingCheckpoint({
bondedAmount: _bondedAmount,
delegateAddress: _delegateAddress,
delegatedAmount: _delegatedAmount,
lastClaimRound: _lastClaimRound,
lastRewardRound: _lastRewardRound
});
checkpoints.data[_startRound] = bond;

// now store the startRound itself in the startRounds array to allow us
// to find it and lookup in the above mapping
checkpoints.startRounds.pushSorted(_startRound);

onCheckpointChanged(_account, previous, bond);
}

function onCheckpointChanged(
address _account,
BondingCheckpoint memory previous,
BondingCheckpoint memory current
) internal {
address previousDelegate = previous.delegateAddress;
address newDelegate = current.delegateAddress;
if (previousDelegate != newDelegate) {
emit DelegateChanged(_account, previousDelegate, newDelegate);
}

bool isTranscoder = newDelegate == _account;
bool wasTranscoder = previousDelegate == _account;
if (isTranscoder) {
emit DelegateVotesChanged(_account, previous.delegatedAmount, current.delegatedAmount);
} else if (wasTranscoder) {
// if the account stopped being a transcoder, we want to emit an event zeroing its "delegate votes"
emit DelegateVotesChanged(_account, previous.delegatedAmount, 0);
}

// Always send a delegator events since transcoders are underlying delegators to themselves. Notice that by the
// nature of this event, delegators will only have their claimed stake checkpointed, without pending rewards.
if (previous.bondedAmount != current.bondedAmount) {
emit DelegatorVotesChanged(_account, previous.bondedAmount, current.bondedAmount);
}
}

/**
* @notice Returns whether an account already has any checkpoint.
* @dev This is meant to be called by a checkpoint initialization script once we deploy the checkpointing logic for
* the first time, so we can efficiently initialize the checkpoint state for all accounts in the system.
*/
function hasCheckpoint(address _account) external view returns (bool) {
function hasCheckpoint(address _account) public view returns (bool) {
return bondingCheckpoints[_account].startRounds.length > 0;
}

Expand All @@ -156,12 +262,11 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
*/
function checkpointTotalActiveStake(uint256 _totalStake, uint256 _round) public virtual onlyBondingManager {
if (_round > clock()) {
revert FutureCheckpoint(_round, clock());
revert InvalidCheckpoint(_round, clock());
}

totalActiveStakeCheckpoints[_round] = _totalStake;

totalStakeCheckpointRounds.pushSorted(_round);
totalStakeCheckpoints.data[_round] = _totalStake;
totalStakeCheckpoints.rounds.pushSorted(_round);
}

// Historical stake access functions
Expand All @@ -175,10 +280,10 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
revert FutureLookup(_round, clock());
}

uint256 activeStake = totalActiveStakeCheckpoints[_round];
uint256 activeStake = totalStakeCheckpoints.data[_round];

if (activeStake == 0) {
uint256 lastInitialized = checkedFindLowerBound(totalStakeCheckpointRounds, _round);
uint256 lastInitialized = checkedFindLowerBound(totalStakeCheckpoints.rounds, _round);

// Check that the round was in fact initialized so we don't return a 0 value accidentally.
if (lastInitialized != _round) {
Expand Down Expand Up @@ -235,11 +340,11 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
view
returns (BondingCheckpoint storage)
{
if (_round > clock()) {
revert FutureLookup(_round, clock());
if (_round > clock() + 1) {
revert FutureLookup(_round, clock() + 1);
}

BondingCheckpointsByRound storage checkpoints = bondingCheckpoints[_account];
BondingVotesByRound storage checkpoints = bondingCheckpoints[_account];

// Most of the time we will be calling this for a transcoder which checkpoints on every round through reward().
// On those cases we will have a checkpoint for exactly the round we want, so optimize for that.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,28 @@ pragma solidity ^0.8.9;

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

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

/**
* @title Interface for BondingCheckpoints
* @title Interface for BondingVotes
*/
interface IBondingCheckpoints is IERC6372Upgradeable {
// BondingManager hooks

interface IBondingVotes is IERC6372Upgradeable, IVotes {
error InvalidCaller(address caller, address required);
error FutureCheckpoint(uint256 checkpointRound, uint256 maxAllowed);
error InvalidCheckpoint(uint256 checkpointRound, uint256 requiredRound);
error FutureLastClaimRound(uint256 lastClaimRound, uint256 maxAllowed);

error FutureLookup(uint256 queryRound, uint256 maxAllowed);
error MissingRoundCheckpoint(uint256 round);
error NoRecordedCheckpoints();
error PastLookup(uint256 queryRound, uint256 firstCheckpointRound);
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,
Expand All @@ -27,12 +39,6 @@ interface IBondingCheckpoints is IERC6372Upgradeable {

// Historical stake access functions

error FutureLookup(uint256 queryRound, uint256 currentRound);
error MissingRoundCheckpoint(uint256 round);
error NoRecordedCheckpoints();
error PastLookup(uint256 queryRound, uint256 firstCheckpointRound);
error MissingEarningsPool(address transcoder, uint256 round);

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

function getTotalActiveStakeAt(uint256 _round) external view returns (uint256);
Expand Down
34 changes: 34 additions & 0 deletions contracts/test/mocks/BondingVotesERC5805Harness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "../../bonding/BondingVotes.sol";
import "./GenericMock.sol";

/**
* @dev This is a tets utility for unit tests on the ERC5805 functions of the BondingVotes contract. It overrides the
* functions that should be used to derive the values returned by the ERC5805 functions and checks against those.
*/
contract BondingVotesERC5805Harness is BondingVotes {
constructor(address _controller) BondingVotes(_controller) {}

/**
* @dev Mocked version that returns transformed version of the input for testing.
* @return amount lowest 4 bytes of address + _round
* @return delegateAddress (_account << 4) | _round.
*/
function getBondingStateAt(address _account, uint256 _round)
public
pure
override
returns (uint256 amount, address delegateAddress)
{
uint160 intAddr = uint160(_account);

amount = (intAddr & 0xffffffff) + _round;
delegateAddress = address((intAddr << 4) | uint160(_round));
}

function getTotalActiveStakeAt(uint256 _round) public pure override returns (uint256) {
return 4 * _round;
}
}
Loading

0 comments on commit 8b927b1

Please sign in to comment.