Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
name: ${{ github.event.repository.name }}
token: ${{ secrets.CI_CODECOV_TOKEN }}

- name: Enforce test coverage threshold
run: yarn test:coverage:check

editorconfig:
name: Run editorconfig checker
runs-on: ubuntu-latest
Expand Down
43 changes: 36 additions & 7 deletions contracts/bonding/BondingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
// Constants
// Occurances are replaced at compile time
// and computed to a single value if possible by the optimizer
uint256 constant MAX_FUTURE_ROUND = 2**256 - 1;

Check warning on line 32 in contracts/bonding/BondingManager.sol

View workflow job for this annotation

GitHub Actions / Test with coverage

Explicitly mark visibility of state

// Time between unbonding and possible withdrawl in rounds
uint64 public unbondingPeriod;
Expand Down Expand Up @@ -102,6 +102,9 @@
// If the balance of the treasury in LPT is above this value, automatic treasury contributions will halt.
uint256 public treasuryBalanceCeiling;

// Allow reward() calls by pre-defined set of addresses
mapping(address => address) private rewardCallerToTranscoder;

// Check if sender is TicketBroker
modifier onlyTicketBroker() {
_onlyTicketBroker();
Expand Down Expand Up @@ -188,6 +191,26 @@
emit ParameterUpdate("numActiveTranscoders");
}

/**
* @notice Set a reward caller for a transcoder
* @param _rewardCaller Address of the new reward caller
*/
function setRewardCaller(address _rewardCaller) external whenSystemNotPaused {
require(rewardCallerToTranscoder[_rewardCaller] == address(0), "reward caller is already set");
rewardCallerToTranscoder[_rewardCaller] = msg.sender;
emit RewardCallerSet(msg.sender, _rewardCaller);
}

/**
* @notice Unset a reward caller for a transcoder
* @param _rewardCaller Address of the existing reward caller
*/
function unsetRewardCaller(address _rewardCaller) external whenSystemNotPaused {
require(rewardCallerToTranscoder[_rewardCaller] == msg.sender, "only relevant transcoder can unset");
rewardCallerToTranscoder[_rewardCaller] = address(0);
emit RewardCallerUnset(msg.sender, _rewardCaller);
}

/**
* @notice Sets commission rates as a transcoder and if the caller is not in the transcoder pool tries to add it
* @dev Percentages are represented as numerators of fractions over MathUtils.PERC_DIVISOR
Expand Down Expand Up @@ -865,17 +888,20 @@
public
whenSystemNotPaused
currentRoundInitialized
autoCheckpoint(msg.sender)
{
uint256 currentRound = roundsManager().currentRound();

require(isActiveTranscoder(msg.sender), "caller must be an active transcoder");
address transcoderAddress = msg.sender;
if (!isActiveTranscoder(transcoderAddress)) {
transcoderAddress = rewardCallerToTranscoder[msg.sender];
require(isActiveTranscoder(transcoderAddress), "caller must be an active transcoder or rewardCaller");
}
require(
transcoders[msg.sender].lastRewardRound != currentRound,
transcoders[transcoderAddress].lastRewardRound != currentRound,
"caller has already called reward for the current round"
);

Transcoder storage t = transcoders[msg.sender];
Transcoder storage t = transcoders[transcoderAddress];
EarningsPool.Data storage earningsPool = t.earningsPoolPerRound[currentRound];

// Set last round that transcoder called reward
Expand Down Expand Up @@ -908,17 +934,20 @@

mtr.trustedTransferTokens(trsry, treasuryRewards);

emit TreasuryReward(msg.sender, trsry, treasuryRewards);
emit TreasuryReward(transcoderAddress, trsry, treasuryRewards);
}

uint256 transcoderRewards = totalRewardTokens.sub(treasuryRewards);

updateTranscoderWithRewards(msg.sender, transcoderRewards, currentRound, _newPosPrev, _newPosNext);
updateTranscoderWithRewards(transcoderAddress, transcoderRewards, currentRound, _newPosPrev, _newPosNext);

// Set last round that transcoder called reward
t.lastRewardRound = currentRound;

emit Reward(msg.sender, transcoderRewards);
emit Reward(transcoderAddress, transcoderRewards);

// Manual execution of the `autoCheckpoint` modifier due to conditional nature of `transcoderAddress`
_checkpointBondingState(transcoderAddress, delegators[transcoderAddress], transcoders[transcoderAddress]);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions contracts/bonding/IBondingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ interface IBondingManager {
uint256 startRound,
uint256 endRound
);
event RewardCallerSet(address indexed transcoder, address indexed rewardCaller);
event RewardCallerUnset(address indexed transcoder, address indexed rewardCaller);

// Deprecated events
// These event signatures can be used to construct the appropriate topic hashes to filter for past logs corresponding
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"clean": "rm -rf cache artifacts typechain",
"compile": "npx hardhat compile",
"test:coverage": "npx hardhat coverage",
"test:coverage:check": "npx istanbul check-coverage ./coverage.json --statements 100 --branches 100 --functions 100 --lines 100",
"test": "npx hardhat test",
"test:unit": "npx hardhat test test/unit/*.*",
"test:integration": "npx hardhat test test/integration/**",
Expand Down
152 changes: 150 additions & 2 deletions test/unit/BondingManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -5474,7 +5474,9 @@ describe("BondingManager", () => {
it("should fail if caller is not a transcoder", async () => {
await expect(
bondingManager.connect(nonTranscoder).reward()
).to.be.revertedWith("caller must be an active transcoder")
).to.be.revertedWith(
"caller must be an active transcoder or rewardCaller"
)
})

it("should fail if caller is registered but not an active transcoder yet in the current round", async () => {
Expand All @@ -5484,7 +5486,9 @@ describe("BondingManager", () => {
)
await expect(
bondingManager.connect(transcoder).reward()
).to.be.revertedWith("caller must be an active transcoder")
).to.be.revertedWith(
"caller must be an active transcoder or rewardCaller"
)
})

it("should fail if caller already called reward during the current round", async () => {
Expand Down Expand Up @@ -6100,6 +6104,150 @@ describe("BondingManager", () => {
atCeilingTest("when above limit", 1500)
})
})

describe("reward delegation", () => {
const transcoderRewards = 1000

it("should only allow active transcoders to set a RewardCaller", async () => {
const registeredTranscoder = signers[2]
await bondingManager
.connect(registeredTranscoder)
.bond(1000, registeredTranscoder.address)
await bondingManager
.connect(registeredTranscoder)
.transcoder(5, 10)

const registeredTranscoderTx = bondingManager
.connect(registeredTranscoder)
.setRewardCaller(nonTranscoder.address)

await expect(registeredTranscoderTx).to.be.revertedWith(
"caller must be an active transcoder"
)

const activeTranscoderTx = bondingManager
.connect(transcoder)
.setRewardCaller(nonTranscoder.address)

await expect(activeTranscoderTx)
.to.emit(bondingManager, "RewardCallerSet")
.withArgs(transcoder.address, nonTranscoder.address)
})

it("should allow a transcoder to call reward even if RewardCaller is set", async () => {
const setRewardCallerTx = bondingManager
.connect(transcoder)
.setRewardCaller(nonTranscoder.address)
await expect(setRewardCallerTx)
.to.emit(bondingManager, "RewardCallerSet")
.withArgs(transcoder.address, nonTranscoder.address)

const rewardTx = bondingManager.connect(transcoder).reward()
await expect(rewardTx)
.to.emit(bondingManager, "Reward")
.withArgs(transcoder.address, transcoderRewards)

await fixture.roundsManager.setMockUint256(
functionSig("currentRound()"),
currentRound + 3
)

const unsetRewardCallerTx = bondingManager
.connect(transcoder)
.unsetRewardCaller(nonTranscoder.address)
await expect(unsetRewardCallerTx)
.to.emit(bondingManager, "RewardCallerUnset")
.withArgs(transcoder.address, nonTranscoder.address)

const rewardTx2 = bondingManager.connect(transcoder).reward()
await expect(rewardTx2)
.to.emit(bondingManager, "Reward")
.withArgs(transcoder.address, transcoderRewards)
})

it("should allow a RewardCaller to call reward", async () => {
const setRewardCallerTx = bondingManager
.connect(transcoder)
.setRewardCaller(nonTranscoder.address)
await expect(setRewardCallerTx)
.to.emit(bondingManager, "RewardCallerSet")
.withArgs(transcoder.address, nonTranscoder.address)

const rewardTx = bondingManager.connect(nonTranscoder).reward()
await expect(rewardTx)
.to.emit(bondingManager, "Reward")
.withArgs(transcoder.address, transcoderRewards)

await fixture.roundsManager.setMockUint256(
functionSig("currentRound()"),
currentRound + 3
)

const unsetRewardCallerTx = bondingManager
.connect(transcoder)
.unsetRewardCaller(nonTranscoder.address)
await expect(unsetRewardCallerTx)
.to.emit(bondingManager, "RewardCallerUnset")
.withArgs(transcoder.address, nonTranscoder.address)

const rewardTx2 = bondingManager.connect(nonTranscoder).reward()
await expect(rewardTx2).to.be.revertedWith(
"caller must be an active transcoder or rewardCaller"
)
})

it("impossible to set the same RewardCaller twice", async () => {
const setRewardCallerTx = bondingManager
.connect(transcoder)
.setRewardCaller(nonTranscoder.address)
await expect(setRewardCallerTx)
.to.emit(bondingManager, "RewardCallerSet")
.withArgs(transcoder.address, nonTranscoder.address)

const setRewardCallerTx2 = bondingManager
.connect(transcoder)
.setRewardCaller(nonTranscoder.address)
await expect(setRewardCallerTx2).to.be.revertedWith(
"reward caller is already set"
)
})

it("impossible to unset the RewardCaller for another transcoder", async () => {
const setRewardCallerTx = bondingManager
.connect(transcoder)
.setRewardCaller(nonTranscoder.address)
await expect(setRewardCallerTx)
.to.emit(bondingManager, "RewardCallerSet")
.withArgs(transcoder.address, nonTranscoder.address)

const unsetRewardCallerTx = bondingManager
.connect(nonTranscoder)
.unsetRewardCaller(nonTranscoder.address)
await expect(unsetRewardCallerTx).to.be.revertedWith(
"only relevant transcoder can unset"
)
})

it("should always checkpoint the reward recipient, not the RewardCaller", async () => {
await bondingManager
.connect(transcoder)
.setRewardCaller(nonTranscoder.address)

const rewardCallerTx = await bondingManager
.connect(nonTranscoder)
.reward()

await expectCheckpoints(fixture, rewardCallerTx, {
account: transcoder.address,
startRound: currentRound + 2,
bondedAmount: 1000,
delegateAddress: transcoder.address,
delegatedAmount: 2000,
lastClaimRound: currentRound,
lastRewardRound: currentRound + 1
})
})
})
})

describe("updateTranscoderWithFees", () => {
Expand Down
Loading