Skip to content

Commit

Permalink
[WIP] bonding: Make bonding checkpoints implement Votes
Browse files Browse the repository at this point in the history
  • Loading branch information
victorges committed Aug 13, 2023
1 parent 8b78ba7 commit 226921d
Show file tree
Hide file tree
Showing 19 changed files with 435 additions and 339 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,32 @@ 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

/**
* @notice Clock is set to match the current round, which is the checkpointing
Expand All @@ -94,6 +98,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 +187,65 @@ 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) {
// TODO: maybe check if this is the mysterious revert?
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);

emitVotesEvents(_account, previous, bond);
}

function emitVotesEvents(
address _account,
BondingCheckpoint memory previous,
BondingCheckpoint memory current
) internal {
bool wasTranscoder = previous.delegateAddress == previous.delegateAddress;
uint256 previousVotes = wasTranscoder ? previous.delegatedAmount : previous.bondedAmount;
address previousDelegate = previous.delegateAddress;

bool isTranscoder = current.delegateAddress == current.delegateAddress;
uint256 newVotes = isTranscoder ? current.delegatedAmount : current.bondedAmount;
address newDelegate = current.delegateAddress;

if (previousDelegate != newDelegate) {
emit DelegateChanged(_account, previousDelegate, newDelegate);
}
if (isTranscoder) {
emit DelegateVotesChanged(_account, previousVotes, newVotes);
} else {
emit DelegatorVotesChanged(_account, previousVotes, newVotes);
}
}

/**
* @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 +258,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 +276,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 +336,11 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
view
returns (BondingCheckpoint storage)
{
if (_round > clock()) {
if (_round > clock() + 1) {
revert FutureLookup(_round, clock());
}

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,15 +3,20 @@ 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);
// Indicates that the called function is not supported in this contract and should be performed through the
// BondingManager instead. This is mostly used for delegation methods, which must be bonds instead.
error MustCallBondingManager(string bondingManagerFunction);

// BondingManager hooks

function checkpointBondingState(
address _account,
Expand Down
2 changes: 1 addition & 1 deletion contracts/test/mocks/BondingCheckpointsMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity 0.8.9;

import "./GenericMock.sol";

contract BondingCheckpointsMock is GenericMock {
contract BondingVotesMock is GenericMock {
event CheckpointBondingState(
address account,
uint256 startRound,
Expand Down
9 changes: 3 additions & 6 deletions contracts/treasury/GovernorCountingOverridable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import "../bonding/libraries/EarningsPoolLIP36.sol";
import "../Manager.sol";
import "../IController.sol";
import "../rounds/IRoundsManager.sol";
import "../bonding/IBondingCheckpoints.sol";

interface IVotes is IERC5805Upgradeable {
function delegatedAt(address account, uint256 timepoint) external returns (address);
}
import "./IVotes.sol";

/**
* @title GovernorCountingOverridable
Expand Down Expand Up @@ -71,6 +67,7 @@ abstract contract GovernorCountingOverridable is Initializable, GovernorUpgradea
*/
// solhint-disable-next-line func-name-mixedcase
function COUNTING_MODE() public pure virtual override returns (string memory) {
// TODO: Consider a different counting mode
return "support=bravo&quorum=for,abstain,against";
}

Expand Down Expand Up @@ -225,5 +222,5 @@ abstract contract GovernorCountingOverridable is Initializable, GovernorUpgradea
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[50] private __gap;
uint256[49] private __gap;
}
14 changes: 14 additions & 0 deletions contracts/treasury/IVotes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

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

interface IVotes is IERC5805Upgradeable {
/**
* @dev Emitted when bonding change results in changes to a delegate's number of votes. This complements the events
* from IERC5805 by also supporting voting power to the delegators themselves.
*/
event DelegatorVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);

function delegatedAt(address account, uint256 timepoint) external returns (address);
}
Loading

0 comments on commit 226921d

Please sign in to comment.