Skip to content

Commit

Permalink
bonding: Ensure pool factors init in rebondFromUnbonded()
Browse files Browse the repository at this point in the history
  • Loading branch information
yondonfu committed Nov 6, 2023
1 parent 459c487 commit 2229821
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 28 deletions.
58 changes: 30 additions & 28 deletions contracts/bonding/BondingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -600,18 +600,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
_checkpointBondingState(currentDelegate, delegators[currentDelegate], transcoders[currentDelegate]);
}

{
Transcoder storage newDelegate = transcoders[_to];
EarningsPool.Data storage currPool = newDelegate.earningsPoolPerRound[currentRound];
if (currPool.cumulativeRewardFactor == 0) {
currPool.cumulativeRewardFactor = cumulativeFactorsPool(newDelegate, newDelegate.lastRewardRound)
.cumulativeRewardFactor;
}
if (currPool.cumulativeFeeFactor == 0) {
currPool.cumulativeFeeFactor = cumulativeFactorsPool(newDelegate, newDelegate.lastFeeRound)
.cumulativeFeeFactor;
}
}
ensureInitializedCumulativeFactorsPool(_to, currentRound);

// cannot delegate to someone without having bonded stake
require(delegationAmount > 0, "delegation amount must be greater than 0");
Expand Down Expand Up @@ -738,6 +727,10 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
// Does not transfer bond to the zero address
require(address(0) != _delegator, "INVALID_DELEGATOR");

// We do not need to call ensureInitializedCumulativeFactorsPool() here for oldDelDelegate because
// the _autoClaimEarnings() call at the top of this function will already include a sub-call to
// ensureInitializedCumulativeFactorsPool() for oldDelDelegate and the current round.

newDel.delegateAddress = oldDelDelegate;
}

Expand Down Expand Up @@ -844,8 +837,12 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
) public whenSystemNotPaused currentRoundInitialized autoClaimEarnings(msg.sender) {
require(delegatorStatus(msg.sender) == DelegatorStatus.Unbonded, "caller must be unbonded");

uint256 currentRound = roundsManager().currentRound();

ensureInitializedCumulativeFactorsPool(_to, currentRound);

// Set delegator's start round and transition into Pending state
delegators[msg.sender].startRound = roundsManager().currentRound().add(1);
delegators[msg.sender].startRound = currentRound.add(1);
// Set delegator's delegate
delegators[msg.sender].delegateAddress = _to;
// Process rebond using unbonding lock
Expand Down Expand Up @@ -1526,23 +1523,11 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
// Check whether the endEarningsPool is initialised
// If it is not initialised set it's cumulative factors so that they can be used when a delegator
// next claims earnings as the start cumulative factors (see delegatorCumulativeStakeAndFees())
Transcoder storage t = transcoders[del.delegateAddress];
EarningsPool.Data storage endEarningsPool = t.earningsPoolPerRound[_endRound];
if (endEarningsPool.cumulativeRewardFactor == 0) {
uint256 lastRewardRound = t.lastRewardRound;
if (lastRewardRound < _endRound) {
endEarningsPool.cumulativeRewardFactor = cumulativeFactorsPool(t, lastRewardRound)
.cumulativeRewardFactor;
}
}
if (endEarningsPool.cumulativeFeeFactor == 0) {
uint256 lastFeeRound = t.lastFeeRound;
if (lastFeeRound < _endRound) {
endEarningsPool.cumulativeFeeFactor = cumulativeFactorsPool(t, lastFeeRound).cumulativeFeeFactor;
}
}
address delegate = del.delegateAddress;
ensureInitializedCumulativeFactorsPool(delegate, _endRound);

if (del.delegateAddress == _delegator) {
Transcoder storage t = transcoders[delegate];
t.cumulativeFees = 0;
t.cumulativeRewards = 0;
// activeCumulativeRewards is not cleared here because the next reward() call will set it to cumulativeRewards
Expand Down Expand Up @@ -1601,6 +1586,23 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
emit Rebond(delegate, _delegator, _unbondingLockId, amount);
}

function ensureInitializedCumulativeFactorsPool(address _transcoder, uint256 _round) internal {
Transcoder storage t = transcoders[_transcoder];
EarningsPool.Data storage pool = t.earningsPoolPerRound[_round];
if (pool.cumulativeRewardFactor == 0) {
uint256 lastRewardRound = t.lastRewardRound;
if (lastRewardRound < _round) {
pool.cumulativeRewardFactor = cumulativeFactorsPool(t, lastRewardRound).cumulativeRewardFactor;
}
}
if (pool.cumulativeFeeFactor == 0) {
uint256 lastFeeRound = t.lastFeeRound;
if (lastFeeRound < _round) {
pool.cumulativeFeeFactor = cumulativeFactorsPool(t, lastFeeRound).cumulativeFeeFactor;
}
}
}

/**
* @notice Checkpoints a delegator state after changes, to be used for historical voting power calculations in
* on-chain governor logic.
Expand Down
107 changes: 107 additions & 0 deletions src/test/BondingManagerRebondUninitializedFactorsFix.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
pragma solidity ^0.8.9;

import "ds-test/test.sol";
import "forge-std/console.sol";
import "./base/GovernorBaseTest.sol";
import "contracts/bonding/BondingManager.sol";
import "contracts/rounds/RoundsManager.sol";
import "contracts/token/LivepeerToken.sol";

// forge test -vvv --fork-url <ARB_MAINNET_RPC_URL> --match-contract BondingManagerRebondUninitializedFactorsFix --fork-block-number 145827633
contract BondingManagerRebondUninitializedFactorsFix is GovernorBaseTest {
BondingManager public constant BONDING_MANAGER = BondingManager(0x35Bcf3c30594191d53231E4FF333E8A770453e40);
RoundsManager public constant ROUNDS_MANAGER = RoundsManager(0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f);
LivepeerToken public constant TOKEN = LivepeerToken(0x289ba1701C2F088cf0faf8B3705246331cB8A839);

address public constant TREASURY = 0xf82C1FF415F1fCf582554fDba790E27019c8E8C4;
bytes32 public constant BONDING_MANAGER_TARGET_ID = keccak256("BondingManagerTarget");

address public attacker;
// Active and lastRewardRound = 3160
// address public transcoder = 0x525419FF5707190389bfb5C87c375D710F5fCb0E;
// Active and lastRewardRound = 3160
// address public transcoder = 0x6CB1Ce2516FB7d211038420a8Cf9a843c7bD3B08;
// Inactive and lastRewardRound = 3108
address public transcoder = 0x76A65814b6e0fa5a3598Ef6503FA1D990ec0E61A;

uint256 public initialBondedAmount = 20_000 ether;

BondingManager public newBondingManagerTarget;

function setUp() public {
newBondingManagerTarget = new BondingManager(address(CONTROLLER));

(, gitCommitHash) = CONTROLLER.getContractInfo(BONDING_MANAGER_TARGET_ID);

stageAndExecuteOne(
address(CONTROLLER),
0,
abi.encodeWithSelector(
CONTROLLER.setContractInfo.selector,
BONDING_MANAGER_TARGET_ID,
address(newBondingManagerTarget),
gitCommitHash
)
);

// Setup accounts
attacker = newAddr();

CHEATS.prank(TREASURY);
TOKEN.transfer(attacker, initialBondedAmount);

CHEATS.prank(attacker);
TOKEN.approve(address(BONDING_MANAGER), initialBondedAmount);
}

function testUpgrade() public {
// Check that new BondingManagerTarget is registered
(address infoAddr, bytes20 infoGitCommitHash) = fetchContractInfo(BONDING_MANAGER_TARGET_ID);
assertEq(infoAddr, address(newBondingManagerTarget));
assertEq(infoGitCommitHash, gitCommitHash);
}

function testRebondFromUnbonded() public {
CHEATS.prank(attacker);
BONDING_MANAGER.bond(initialBondedAmount, attacker);

nextRound();

console.log(
"Attacker pending stake (before unbond + rebondFromUnbonded): ",
BONDING_MANAGER.pendingStake(attacker, 0)
);

CHEATS.startPrank(attacker);
BONDING_MANAGER.unbond(initialBondedAmount);
BONDING_MANAGER.rebondFromUnbonded(transcoder, 0);
CHEATS.stopPrank();

nextRound();

console.log(
"Attacker pending stake (after unbond + rebondFromUnbonded and new round): ",
BONDING_MANAGER.pendingStake(attacker, 0)
);

CHEATS.prank(attacker);
BONDING_MANAGER.claimEarnings(0);

(uint256 endBondedAmount, , , , , , ) = BONDING_MANAGER.getDelegator(attacker);

assertEq(endBondedAmount, initialBondedAmount);
console.log("Attacker end bonded amount: ", endBondedAmount);
}

function nextRound() public {
console.log("Current round (before roll): ", ROUNDS_MANAGER.currentRound());

uint256 currentRoundStartBlock = ROUNDS_MANAGER.currentRoundStartBlock();
uint256 roundLength = ROUNDS_MANAGER.roundLength();
CHEATS.roll(currentRoundStartBlock + roundLength);

ROUNDS_MANAGER.initializeRound();

console.log("Current round (after roll): ", ROUNDS_MANAGER.currentRound());
}
}
107 changes: 107 additions & 0 deletions test/unit/BondingManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3742,6 +3742,113 @@ describe("BondingManager", () => {
})
})

describe("set delegate earnings pool factors if not initialized", () => {
beforeEach(async () => {
// Delegator unbonds rest of tokens transitioning to the Unbonded state
await bondingManager.connect(delegator).unbond(500)
})

it("sets cumulativeRewardFactor if value is zero", async () => {
let round = currentRound + 1

await bondingManager.connect(transcoder).reward()
const epPrevRound =
await bondingManager.getTranscoderEarningsPoolForRound(
transcoder.address,
round
)

assert.notEqual(
epPrevRound.cumulativeRewardFactor.toString(),
"0"
)

round++

await fixture.roundsManager.setMockUint256(
functionSig("currentRound()"),
round
)
const epCurrentRoundPreRebond =
await bondingManager.getTranscoderEarningsPoolForRound(
transcoder.address,
round
)

assert.equal(
epCurrentRoundPreRebond.cumulativeRewardFactor.toString(),
"0"
)

await bondingManager
.connect(delegator)
.rebondFromUnbonded(transcoder.address, unbondingLockID)

const epCurrentRoundPostRebond =
await bondingManager.getTranscoderEarningsPoolForRound(
transcoder.address,
round
)

assert.equal(
epCurrentRoundPostRebond.cumulativeRewardFactor.toString(),
epPrevRound.cumulativeRewardFactor.toString()
)
})

it("sets cumulativeFeeFactor if value is zero", async () => {
let round = currentRound + 1

await fixture.ticketBroker.execute(
bondingManager.address,
functionEncodedABI(
"updateTranscoderWithFees(address,uint256,uint256)",
["address", "uint256", "uint256"],
[transcoder.address, "1000000000000000000", round]
)
)
const epPrevRound =
await bondingManager.getTranscoderEarningsPoolForRound(
transcoder.address,
round
)

assert.notEqual(epPrevRound.cumulativeFeeFactor.toString(), "0")

round++

await fixture.roundsManager.setMockUint256(
functionSig("currentRound()"),
round
)
const epCurrentRoundPreRebond =
await bondingManager.getTranscoderEarningsPoolForRound(
transcoder.address,
round
)

assert.equal(
epCurrentRoundPreRebond.cumulativeFeeFactor.toString(),
"0"
)

await bondingManager
.connect(delegator)
.rebondFromUnbonded(transcoder.address, unbondingLockID)

const epCurrentRoundPostRebond =
await bondingManager.getTranscoderEarningsPoolForRound(
transcoder.address,
round
)

assert.equal(
epCurrentRoundPostRebond.cumulativeFeeFactor.toString(),
epPrevRound.cumulativeFeeFactor.toString()
)
})
})

it("should create a Rebond event", async () => {
// Delegator unbonds rest of tokens transitioning to the Unbonded state
await bondingManager.connect(delegator).unbond(500)
Expand Down

0 comments on commit 2229821

Please sign in to comment.