Skip to content

Commit

Permalink
bonding: Improve documentation around checkpoints storage
Browse files Browse the repository at this point in the history
  • Loading branch information
victorges committed Jun 20, 2023
1 parent 8cc579c commit 3b49303
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 27 deletions.
68 changes: 55 additions & 13 deletions contracts/bonding/BondingCheckpoints.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,67 @@ import "../IController.sol";
import "../rounds/IRoundsManager.sol";
import "./BondingManager.sol";

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

constructor(address _controller) Manager(_controller) {}

struct BondingCheckpoint {
uint256 bondedAmount; // The amount of bonded tokens to another delegate as per the lastClaimRound
address delegateAddress; // The address delegated to
uint256 delegatedAmount; // The amount of tokens delegated to the account (only set for transcoders)
uint256 lastClaimRound; // The last round during which the delegator claimed its earnings. Pegs the value of bondedAmount for rewards calculation
uint256 lastRewardRound; // The last round during which the transcoder called rewards. This is useful to find the reward pool when calculating historical rewards. Notice that this actually comes from the Transcoder struct in bonding manager, not Delegator.
/**
* @dev The amount of bonded tokens to another delegate as per the lastClaimRound.
*/
uint256 bondedAmount;
/**
* @dev The address of the delegate the account is bonded to. In case of transcoders this is their own address.
*/
address delegateAddress;
/**
* @dev The amount of tokens delegated from delegators to this account. This is only set for transcoders, which
* have to self-delegate first and then have tokens bonded from other delegators.
*/
uint256 delegatedAmount;
/**
* @dev The last round during which the delegator claimed its earnings. This pegs the value of bondedAmount for
* rewards calculation in {BondingManager-delegatorCumulativeStakeAndFees}.
*/
uint256 lastClaimRound;
/**
* @dev The last round during which the transcoder called {BondingManager-reward}. This is needed to find a
* reward pool for any round when calculating historical rewards.
*
* Notice that this is the only field that comes from the Transcoder struct in BondingManager, not Delegator.
*/
uint256 lastRewardRound;
}

/**
* @dev Stores a list of checkpoints for an account, queryable and mapped by start round. To access the checkpoint
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 {
uint256[] startRounds;
mapping(uint256 => BondingCheckpoint) data;
}

/**
* @dev Checkpoints by account (delegators and transcoders).
*/
mapping(address => BondingCheckpointsByRound) private bondingCheckpoints;

/**
* @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.
*/
uint256[] totalStakeCheckpointRounds;
/**
* @dev See {totalStakeCheckpointRounds} above.
*/
mapping(uint256 => uint256) private totalActiveStakeCheckpoints;

// IERC6372 interface implementation
Expand Down Expand Up @@ -147,7 +187,7 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
* @param _round The round for which we want to get the bonding state. Normally a proposal's vote start round.
* @return The active stake of the account at the given round including any accrued rewards.
*/
function getAccountActiveStakeAt(address _account, uint256 _round) public view virtual returns (uint256) {
function getAccountStakeAt(address _account, uint256 _round) public view virtual returns (uint256) {
require(_round <= clock(), "getStakeAt: future lookup");

BondingCheckpoint storage bond = getBondingCheckpointAt(_account, _round);
Expand Down Expand Up @@ -184,6 +224,14 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
returns (BondingCheckpoint storage)
{
BondingCheckpointsByRound 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.
BondingCheckpoint storage bond = checkpoints.data[_round];
if (bond.bondedAmount > 0) {
return bond;
}

uint256 startRound = checkpoints.startRounds.findLowerBound(_round);
return checkpoints.data[startRound];
}
Expand Down Expand Up @@ -248,13 +296,7 @@ contract BondingCheckpoints is ManagerProxyTarget, IBondingCheckpoints {
view
returns (uint256 rewardRound, EarningsPool.Data memory pool)
{
// Most of the time we will already have the checkpoint from exactly the round we want
BondingCheckpoint storage bond = bondingCheckpoints[_transcoder].data[_round];

if (bond.lastRewardRound == 0) {
bond = getBondingCheckpointAt(_transcoder, _round);
}

BondingCheckpoint storage bond = getBondingCheckpointAt(_transcoder, _round);
rewardRound = bond.lastRewardRound;
pool = getTranscoderEarningPoolForRound(_transcoder, rewardRound);
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/bonding/IBondingCheckpoints.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface IBondingCheckpoints is IERC6372Upgradeable {

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

function getAccountActiveStakeAt(address _account, uint256 _round) external view returns (uint256);
function getAccountStakeAt(address _account, uint256 _round) external view returns (uint256);

function getDelegateAddressAt(address _account, uint256 _round) external view returns (address);
}
23 changes: 10 additions & 13 deletions test/unit/BondingCheckpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe("BondingCheckpoints", () => {

const stakeAt = (account, round) =>
bondingCheckpoints
.getAccountActiveStakeAt(account.address, round)
.getAccountStakeAt(account.address, round)
.then(n => n.toString())

it("should revert if bonding checkpoint is not registered", async () => {
Expand Down Expand Up @@ -296,7 +296,7 @@ describe("BondingCheckpoints", () => {
await setRound(currentRound + 2)
})

describe("getAccountActiveStakeAt", () => {
describe("getAccountStakeAt", () => {
it("should return partial rewards for any rounds since bonding", async () => {
const pendingRewards0 = 250
const pendingRewards1 = Math.floor(
Expand All @@ -305,7 +305,7 @@ describe("BondingCheckpoints", () => {

const stakeAt = round =>
bondingCheckpoints
.getAccountActiveStakeAt(delegator.address, round)
.getAccountStakeAt(delegator.address, round)
.then(n => n.toString())

assert.equal(await stakeAt(1), 0)
Expand All @@ -325,7 +325,7 @@ describe("BondingCheckpoints", () => {
it("should return partial rewards for all transcoder stake", async () => {
const stakeAt = round =>
bondingCheckpoints
.getAccountActiveStakeAt(transcoder.address, round)
.getAccountStakeAt(transcoder.address, round)
.then(n => n.toString())

assert.equal(await stakeAt(1), 0)
Expand Down Expand Up @@ -509,11 +509,11 @@ describe("BondingCheckpoints", () => {
assert.isFalse(await isActive(inactiveTranscoder.address))
})

describe("getAccountActiveStakeAt", () => {
describe("getAccountStakeAt", () => {
const stakeAt = (signer, round) =>
bondingCheckpoints
.connect(signer)
.getAccountActiveStakeAt(signer.address, round)
.getAccountStakeAt(signer.address, round)
.then(n => n.toString())

it("should allow votes from active and inactive stake delegators", async () => {
Expand Down Expand Up @@ -565,10 +565,7 @@ describe("BondingCheckpoints", () => {
let activeStake = 0
for (const transcoder of activeTranscoders) {
activeStake += await bondingCheckpoints
.getAccountActiveStakeAt(
transcoder.address,
round
)
.getAccountStakeAt(transcoder.address, round)
.then(n => parseInt(n.toString()))
}
assert.equal(
Expand Down Expand Up @@ -673,10 +670,10 @@ describe("BondingCheckpoints", () => {
await setRound(currentRound + 3)
})

describe("getAccountActiveStakeAt", () => {
describe("getAccountStakeAt", () => {
const stakeAt = (account, round) =>
bondingCheckpoints
.getAccountActiveStakeAt(account.address, round)
.getAccountStakeAt(account.address, round)
.then(n => n.toString())
const expectStakeAt = async (account, round, expected) => {
assert.equal(
Expand Down Expand Up @@ -826,7 +823,7 @@ describe("BondingCheckpoints", () => {

const stakeAt = (account, round) =>
bondingCheckpoints
.getAccountActiveStakeAt(account.address, round)
.getAccountStakeAt(account.address, round)
.then(n => n.toString())
const delegateAt = (account, round) =>
bondingCheckpoints
Expand Down

0 comments on commit 3b49303

Please sign in to comment.