Skip to content

Commit 66d1d84

Browse files
feat: add reward call delegation
Implements an alternative discussed approach that allows orchestrators to configure a custom reward caller. Co-authored-by: SidestreamSweatyPumpkin <sweatypumpkin@sidestream.tech>
1 parent d03671e commit 66d1d84

5 files changed

Lines changed: 192 additions & 9 deletions

File tree

.github/workflows/test.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ jobs:
4747
name: ${{ github.event.repository.name }}
4848
token: ${{ secrets.CI_CODECOV_TOKEN }}
4949

50+
- name: Enforce test coverage threshold
51+
run: yarn test:coverage:check
52+
5053
editorconfig:
5154
name: Run editorconfig checker
5255
runs-on: ubuntu-latest

contracts/bonding/BondingManager.sol

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
102102
// If the balance of the treasury in LPT is above this value, automatic treasury contributions will halt.
103103
uint256 public treasuryBalanceCeiling;
104104

105+
// Allow reward() calls by pre-defined set of addresses
106+
mapping(address => address) private rewardCallerToTranscoder;
107+
105108
// Check if sender is TicketBroker
106109
modifier onlyTicketBroker() {
107110
_onlyTicketBroker();
@@ -188,6 +191,26 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
188191
emit ParameterUpdate("numActiveTranscoders");
189192
}
190193

194+
/**
195+
* @notice Set a reward caller for a transcoder
196+
* @param _rewardCaller Address of the new reward caller
197+
*/
198+
function setRewardCaller(address _rewardCaller) external whenSystemNotPaused {
199+
require(rewardCallerToTranscoder[_rewardCaller] == address(0), "reward caller is already set");
200+
rewardCallerToTranscoder[_rewardCaller] = msg.sender;
201+
emit RewardCallerSet(msg.sender, _rewardCaller);
202+
}
203+
204+
/**
205+
* @notice Unset a reward caller for a transcoder
206+
* @param _rewardCaller Address of the existing reward caller
207+
*/
208+
function unsetRewardCaller(address _rewardCaller) external whenSystemNotPaused {
209+
require(rewardCallerToTranscoder[_rewardCaller] == msg.sender, "only relevant transcoder can unset");
210+
rewardCallerToTranscoder[_rewardCaller] = address(0);
211+
emit RewardCallerUnset(msg.sender, _rewardCaller);
212+
}
213+
191214
/**
192215
* @notice Sets commission rates as a transcoder and if the caller is not in the transcoder pool tries to add it
193216
* @dev Percentages are represented as numerators of fractions over MathUtils.PERC_DIVISOR
@@ -865,17 +888,20 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
865888
public
866889
whenSystemNotPaused
867890
currentRoundInitialized
868-
autoCheckpoint(msg.sender)
869891
{
870892
uint256 currentRound = roundsManager().currentRound();
871893

872-
require(isActiveTranscoder(msg.sender), "caller must be an active transcoder");
894+
address transcoderAddress = msg.sender;
895+
if (!isActiveTranscoder(transcoderAddress)) {
896+
transcoderAddress = rewardCallerToTranscoder[msg.sender];
897+
require(isActiveTranscoder(transcoderAddress), "caller must be an active transcoder or rewardCaller");
898+
}
873899
require(
874-
transcoders[msg.sender].lastRewardRound != currentRound,
900+
transcoders[transcoderAddress].lastRewardRound != currentRound,
875901
"caller has already called reward for the current round"
876902
);
877903

878-
Transcoder storage t = transcoders[msg.sender];
904+
Transcoder storage t = transcoders[transcoderAddress];
879905
EarningsPool.Data storage earningsPool = t.earningsPoolPerRound[currentRound];
880906

881907
// Set last round that transcoder called reward
@@ -908,17 +934,20 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
908934

909935
mtr.trustedTransferTokens(trsry, treasuryRewards);
910936

911-
emit TreasuryReward(msg.sender, trsry, treasuryRewards);
937+
emit TreasuryReward(transcoderAddress, trsry, treasuryRewards);
912938
}
913939

914940
uint256 transcoderRewards = totalRewardTokens.sub(treasuryRewards);
915941

916-
updateTranscoderWithRewards(msg.sender, transcoderRewards, currentRound, _newPosPrev, _newPosNext);
942+
updateTranscoderWithRewards(transcoderAddress, transcoderRewards, currentRound, _newPosPrev, _newPosNext);
917943

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

921-
emit Reward(msg.sender, transcoderRewards);
947+
emit Reward(transcoderAddress, transcoderRewards);
948+
949+
// Manual execution of the `autoCheckpoint` modifier due to conditional nature of `transcoderAddress`
950+
_checkpointBondingState(transcoderAddress, delegators[transcoderAddress], transcoders[transcoderAddress]);
922951
}
923952

924953
/**

contracts/bonding/IBondingManager.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ interface IBondingManager {
4444
uint256 startRound,
4545
uint256 endRound
4646
);
47+
event RewardCallerSet(address indexed transcoder, address indexed rewardCaller);
48+
event RewardCallerUnset(address indexed transcoder, address indexed rewardCaller);
4749

4850
// Deprecated events
4951
// These event signatures can be used to construct the appropriate topic hashes to filter for past logs corresponding

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"clean": "rm -rf cache artifacts typechain",
2222
"compile": "npx hardhat compile",
2323
"test:coverage": "npx hardhat coverage",
24+
"test:coverage:check": "npx istanbul check-coverage ./coverage.json --statements 100 --branches 100 --functions 100 --lines 100",
2425
"test": "npx hardhat test",
2526
"test:unit": "npx hardhat test test/unit/*.*",
2627
"test:integration": "npx hardhat test test/integration/**",

test/unit/BondingManager.js

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5474,7 +5474,9 @@ describe("BondingManager", () => {
54745474
it("should fail if caller is not a transcoder", async () => {
54755475
await expect(
54765476
bondingManager.connect(nonTranscoder).reward()
5477-
).to.be.revertedWith("caller must be an active transcoder")
5477+
).to.be.revertedWith(
5478+
"caller must be an active transcoder or rewardCaller"
5479+
)
54785480
})
54795481

54805482
it("should fail if caller is registered but not an active transcoder yet in the current round", async () => {
@@ -5484,7 +5486,9 @@ describe("BondingManager", () => {
54845486
)
54855487
await expect(
54865488
bondingManager.connect(transcoder).reward()
5487-
).to.be.revertedWith("caller must be an active transcoder")
5489+
).to.be.revertedWith(
5490+
"caller must be an active transcoder or rewardCaller"
5491+
)
54885492
})
54895493

54905494
it("should fail if caller already called reward during the current round", async () => {
@@ -6100,6 +6104,150 @@ describe("BondingManager", () => {
61006104
atCeilingTest("when above limit", 1500)
61016105
})
61026106
})
6107+
6108+
describe("reward delegation", () => {
6109+
const transcoderRewards = 1000
6110+
6111+
it("should only allow active transcoders to set a RewardCaller", async () => {
6112+
const registeredTranscoder = signers[2]
6113+
await bondingManager
6114+
.connect(registeredTranscoder)
6115+
.bond(1000, registeredTranscoder.address)
6116+
await bondingManager
6117+
.connect(registeredTranscoder)
6118+
.transcoder(5, 10)
6119+
6120+
const registeredTranscoderTx = bondingManager
6121+
.connect(registeredTranscoder)
6122+
.setRewardCaller(nonTranscoder.address)
6123+
6124+
await expect(registeredTranscoderTx).to.be.revertedWith(
6125+
"caller must be an active transcoder"
6126+
)
6127+
6128+
const activeTranscoderTx = bondingManager
6129+
.connect(transcoder)
6130+
.setRewardCaller(nonTranscoder.address)
6131+
6132+
await expect(activeTranscoderTx)
6133+
.to.emit(bondingManager, "RewardCallerSet")
6134+
.withArgs(transcoder.address, nonTranscoder.address)
6135+
})
6136+
6137+
it("should allow a transcoder to call reward even if RewardCaller is set", async () => {
6138+
const setRewardCallerTx = bondingManager
6139+
.connect(transcoder)
6140+
.setRewardCaller(nonTranscoder.address)
6141+
await expect(setRewardCallerTx)
6142+
.to.emit(bondingManager, "RewardCallerSet")
6143+
.withArgs(transcoder.address, nonTranscoder.address)
6144+
6145+
const rewardTx = bondingManager.connect(transcoder).reward()
6146+
await expect(rewardTx)
6147+
.to.emit(bondingManager, "Reward")
6148+
.withArgs(transcoder.address, transcoderRewards)
6149+
6150+
await fixture.roundsManager.setMockUint256(
6151+
functionSig("currentRound()"),
6152+
currentRound + 3
6153+
)
6154+
6155+
const unsetRewardCallerTx = bondingManager
6156+
.connect(transcoder)
6157+
.unsetRewardCaller(nonTranscoder.address)
6158+
await expect(unsetRewardCallerTx)
6159+
.to.emit(bondingManager, "RewardCallerUnset")
6160+
.withArgs(transcoder.address, nonTranscoder.address)
6161+
6162+
const rewardTx2 = bondingManager.connect(transcoder).reward()
6163+
await expect(rewardTx2)
6164+
.to.emit(bondingManager, "Reward")
6165+
.withArgs(transcoder.address, transcoderRewards)
6166+
})
6167+
6168+
it("should allow a RewardCaller to call reward", async () => {
6169+
const setRewardCallerTx = bondingManager
6170+
.connect(transcoder)
6171+
.setRewardCaller(nonTranscoder.address)
6172+
await expect(setRewardCallerTx)
6173+
.to.emit(bondingManager, "RewardCallerSet")
6174+
.withArgs(transcoder.address, nonTranscoder.address)
6175+
6176+
const rewardTx = bondingManager.connect(nonTranscoder).reward()
6177+
await expect(rewardTx)
6178+
.to.emit(bondingManager, "Reward")
6179+
.withArgs(transcoder.address, transcoderRewards)
6180+
6181+
await fixture.roundsManager.setMockUint256(
6182+
functionSig("currentRound()"),
6183+
currentRound + 3
6184+
)
6185+
6186+
const unsetRewardCallerTx = bondingManager
6187+
.connect(transcoder)
6188+
.unsetRewardCaller(nonTranscoder.address)
6189+
await expect(unsetRewardCallerTx)
6190+
.to.emit(bondingManager, "RewardCallerUnset")
6191+
.withArgs(transcoder.address, nonTranscoder.address)
6192+
6193+
const rewardTx2 = bondingManager.connect(nonTranscoder).reward()
6194+
await expect(rewardTx2).to.be.revertedWith(
6195+
"caller must be an active transcoder or rewardCaller"
6196+
)
6197+
})
6198+
6199+
it("impossible to set the same RewardCaller twice", async () => {
6200+
const setRewardCallerTx = bondingManager
6201+
.connect(transcoder)
6202+
.setRewardCaller(nonTranscoder.address)
6203+
await expect(setRewardCallerTx)
6204+
.to.emit(bondingManager, "RewardCallerSet")
6205+
.withArgs(transcoder.address, nonTranscoder.address)
6206+
6207+
const setRewardCallerTx2 = bondingManager
6208+
.connect(transcoder)
6209+
.setRewardCaller(nonTranscoder.address)
6210+
await expect(setRewardCallerTx2).to.be.revertedWith(
6211+
"reward caller is already set"
6212+
)
6213+
})
6214+
6215+
it("impossible to unset the RewardCaller for another transcoder", async () => {
6216+
const setRewardCallerTx = bondingManager
6217+
.connect(transcoder)
6218+
.setRewardCaller(nonTranscoder.address)
6219+
await expect(setRewardCallerTx)
6220+
.to.emit(bondingManager, "RewardCallerSet")
6221+
.withArgs(transcoder.address, nonTranscoder.address)
6222+
6223+
const unsetRewardCallerTx = bondingManager
6224+
.connect(nonTranscoder)
6225+
.unsetRewardCaller(nonTranscoder.address)
6226+
await expect(unsetRewardCallerTx).to.be.revertedWith(
6227+
"only relevant transcoder can unset"
6228+
)
6229+
})
6230+
6231+
it("should always checkpoint the reward recipient, not the RewardCaller", async () => {
6232+
await bondingManager
6233+
.connect(transcoder)
6234+
.setRewardCaller(nonTranscoder.address)
6235+
6236+
const rewardCallerTx = await bondingManager
6237+
.connect(nonTranscoder)
6238+
.reward()
6239+
6240+
await expectCheckpoints(fixture, rewardCallerTx, {
6241+
account: transcoder.address,
6242+
startRound: currentRound + 2,
6243+
bondedAmount: 1000,
6244+
delegateAddress: transcoder.address,
6245+
delegatedAmount: 2000,
6246+
lastClaimRound: currentRound,
6247+
lastRewardRound: currentRound + 1
6248+
})
6249+
})
6250+
})
61036251
})
61046252

61056253
describe("updateTranscoderWithFees", () => {

0 commit comments

Comments
 (0)