Skip to content

Commit de6abef

Browse files
committed
fix: provide part of the keeper reward to L2 redeemer
1 parent a7467b3 commit de6abef

File tree

4 files changed

+124
-9
lines changed

4 files changed

+124
-9
lines changed

contracts/l2/reservoir/L2Reservoir.sol

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@ pragma solidity ^0.7.6;
44
pragma abicoder v2;
55

66
import "@openzeppelin/contracts/math/SafeMath.sol";
7+
import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol";
78

89
import "../../reservoir/IReservoir.sol";
910
import "../../reservoir/Reservoir.sol";
1011
import "./L2ReservoirStorage.sol";
1112

13+
interface IArbTxWithRedeemer {
14+
/**
15+
* @notice Gets the redeemer of the current retryable redeem attempt.
16+
* Returns the zero address if the current transaction is not a retryable redeem attempt.
17+
* If this is an auto-redeem, returns the fee refund address of the retryable.
18+
*/
19+
function getCurrentRedeemer() external view returns (address);
20+
}
21+
1222
/**
1323
* @title L2 Rewards Reservoir
1424
* @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 2.
@@ -18,8 +28,12 @@ import "./L2ReservoirStorage.sol";
1828
contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir {
1929
using SafeMath for uint256;
2030

31+
address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E;
32+
2133
event DripReceived(uint256 _normalizedTokenSupply);
2234
event NextDripNonceUpdated(uint256 _nonce);
35+
event L1ReservoirAddressUpdated(address _l1ReservoirAddress);
36+
event L2KeeperRewardFractionUpdated(uint256 _l2KeeperRewardFraction);
2337

2438
/**
2539
* @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller.
@@ -48,6 +62,29 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir {
4862
emit NextDripNonceUpdated(_nonce);
4963
}
5064

65+
/**
66+
* @dev Sets the L1 Reservoir address
67+
* This is the address on L1 that will appear as redeemer when a ticket
68+
* was auto-redeemed.
69+
* @param _l1ReservoirAddress New address for the L1Reservoir on L1
70+
*/
71+
function setL1ReservoirAddress(address _l1ReservoirAddress) external onlyGovernor {
72+
l1ReservoirAddress = _l1ReservoirAddress;
73+
emit L1ReservoirAddressUpdated(_l1ReservoirAddress);
74+
}
75+
76+
/**
77+
* @dev Sets the L2 keeper reward fraction
78+
* This is the fraction of the keeper reward that will be sent to the redeemer on L2
79+
* if the retryable ticket is not auto-redeemed
80+
* @param _l2KeeperRewardFraction New value for the fraction, with fixed point at 1e18
81+
*/
82+
function setL2KeeperRewardFraction(uint256 _l2KeeperRewardFraction) external onlyGovernor {
83+
require(_l2KeeperRewardFraction <= TOKEN_DECIMALS, "INVALID_VALUE");
84+
l2KeeperRewardFraction = _l2KeeperRewardFraction;
85+
emit L2KeeperRewardFractionUpdated(_l2KeeperRewardFraction);
86+
}
87+
5188
/**
5289
* @dev Get new total rewards accumulated since the last drip.
5390
* This is deltaR = p * r ^ (blocknum - t0) - p, where:
@@ -107,14 +144,19 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir {
107144
}
108145
normalizedTokenSupplyCache = _normalizedTokenSupply;
109146
IGraphToken grt = graphToken();
110-
// We'd like to reward the keeper that redeemed the tx in L2
111-
// but this won't work right now as tx.origin will actually be the L1 sender.
112-
// uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS);
113-
// solhint-disable-next-line avoid-tx-origin
114-
// grt.transfer(tx.origin, _l2KeeperReward);
115-
// grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward));
116-
// So for now we just send all the rewards to teh L1 keeper:
117-
grt.transfer(_l1Keeper, _keeperReward);
147+
148+
// Part of the reward always goes to whoever redeemed the ticket in L2,
149+
// unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir
150+
address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer();
151+
if (redeemer != l1ReservoirAddress) {
152+
uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS);
153+
grt.transfer(redeemer, _l2KeeperReward);
154+
grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward));
155+
} else {
156+
// In an auto-redeem, we just send all the rewards to teh L1 keeper:
157+
grt.transfer(_l1Keeper, _keeperReward);
158+
}
159+
118160
emit DripReceived(normalizedTokenSupplyCache);
119161
}
120162

contracts/l2/reservoir/L2ReservoirStorage.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ contract L2ReservoirV1Storage {
1515
contract L2ReservoirV2Storage is L2ReservoirV1Storage {
1616
// Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18)
1717
uint256 public l2KeeperRewardFraction;
18+
// Address of the L1Reservoir on L1, used to check if a ticket was auto-redeemed
19+
address public l1ReservoirAddress;
1820
}

contracts/reservoir/L1Reservoir.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir {
359359
* @param maxGas Max gas for the L2 retryable ticket execution
360360
* @param gasPriceBid Gas price for the L2 retryable ticket execution
361361
* @param maxSubmissionCost Max submission price for the L2 retryable ticket
362+
* @param keeperReward Reward to be given to the keeper that called drip
363+
* @param keeper Beneficiary address for the keeper that called drip
362364
*/
363365
function _sendNewTokensAndStateToL2(
364366
uint256 nTokens,

test/l2/l2Reservoir.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BigNumber, constants, ContractTransaction, utils } from 'ethers'
44
import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures'
55

66
import { BigNumber as BN } from 'bignumber.js'
7+
import { FakeContract, smock } from '@defi-wonderland/smock'
78

89
import {
910
advanceBlocks,
@@ -30,11 +31,13 @@ const dripIssuanceRate = toBN('1000000023206889619')
3031
describe('L2Reservoir', () => {
3132
let governor: Account
3233
let testAccount1: Account
34+
let testAccount2: Account
3335
let mockRouter: Account
3436
let mockL1GRT: Account
3537
let mockL1Gateway: Account
3638
let mockL1Reservoir: Account
3739
let fixture: NetworkFixture
40+
let arbTxMock: FakeContract
3841

3942
let grt: L2GraphToken
4043
let l2Reservoir: L2Reservoir
@@ -122,7 +125,7 @@ describe('L2Reservoir', () => {
122125
}
123126

124127
before(async function () {
125-
;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir] =
128+
;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir, testAccount2] =
126129
await getAccounts()
127130

128131
fixture = new NetworkFixture()
@@ -136,6 +139,11 @@ describe('L2Reservoir', () => {
136139
mockL1Gateway.address,
137140
mockL1Reservoir.address,
138141
)
142+
143+
arbTxMock = await smock.fake('IArbTxWithRedeemer', {
144+
address: '0x000000000000000000000000000000000000006E',
145+
})
146+
arbTxMock.getCurrentRedeemer.returns(mockL1Reservoir.address)
139147
})
140148

141149
beforeEach(async function () {
@@ -157,7 +165,42 @@ describe('L2Reservoir', () => {
157165
await expect(await l2Reservoir.nextDripNonce()).to.eq(toBN('10'))
158166
})
159167
})
168+
169+
describe('setL1ReservoirAddress', async function () {
170+
it('rejects unauthorized calls', async function () {
171+
const tx = l2Reservoir
172+
.connect(testAccount1.signer)
173+
.setL1ReservoirAddress(testAccount1.address)
174+
await expect(tx).revertedWith('Caller must be Controller governor')
175+
})
176+
it('sets the L1Reservoir address', async function () {
177+
const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(testAccount1.address)
178+
await expect(tx).emit(l2Reservoir, 'L1ReservoirAddressUpdated').withArgs(testAccount1.address)
179+
await expect(await l2Reservoir.l1ReservoirAddress()).to.eq(testAccount1.address)
180+
})
181+
})
182+
183+
describe('setL2KeeperRewardFraction', async function () {
184+
it('rejects unauthorized calls', async function () {
185+
const tx = l2Reservoir.connect(testAccount1.signer).setL2KeeperRewardFraction(toBN(1))
186+
await expect(tx).revertedWith('Caller must be Controller governor')
187+
})
188+
it('rejects invalid values (> 1)', async function () {
189+
const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('1.000001'))
190+
await expect(tx).revertedWith('INVALID_VALUE')
191+
})
192+
it('sets the L1Reservoir address', async function () {
193+
const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.999'))
194+
await expect(tx).emit(l2Reservoir, 'L2KeeperRewardFractionUpdated').withArgs(toGRT('0.999'))
195+
await expect(await l2Reservoir.l2KeeperRewardFraction()).to.eq(toGRT('0.999'))
196+
})
197+
})
198+
160199
describe('receiveDrip', async function () {
200+
beforeEach(async function () {
201+
await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.2'))
202+
await l2Reservoir.connect(governor.signer).setL1ReservoirAddress(mockL1Reservoir.address)
203+
})
161204
it('rejects the call when not called by the gateway', async function () {
162205
const tx = l2Reservoir
163206
.connect(governor.signer)
@@ -232,6 +275,32 @@ describe('L2Reservoir', () => {
232275
.withArgs(l2Reservoir.address, testAccount1.address, reward)
233276
await expect(await grt.balanceOf(testAccount1.address)).to.eq(reward)
234277
})
278+
it('delivers part of the keeper reward to the L2 redeemer', async function () {
279+
arbTxMock.getCurrentRedeemer.returns(testAccount2.address)
280+
await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.25'))
281+
normalizedSupply = dripNormalizedSupply
282+
const reward = toGRT('16')
283+
const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip(
284+
dripNormalizedSupply,
285+
dripIssuanceRate,
286+
toBN('0'),
287+
reward,
288+
testAccount1.address,
289+
)
290+
const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward)
291+
dripBlock = await latestBlock()
292+
await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply)
293+
await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate)
294+
await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply)
295+
await expect(tx)
296+
.emit(grt, 'Transfer')
297+
.withArgs(l2Reservoir.address, testAccount1.address, toGRT('12'))
298+
await expect(tx)
299+
.emit(grt, 'Transfer')
300+
.withArgs(l2Reservoir.address, testAccount2.address, toGRT('4'))
301+
await expect(await grt.balanceOf(testAccount1.address)).to.eq(toGRT('12'))
302+
await expect(await grt.balanceOf(testAccount2.address)).to.eq(toGRT('4'))
303+
})
235304
it('updates the normalized supply cache and issuance rate', async function () {
236305
normalizedSupply = dripNormalizedSupply
237306
let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip(

0 commit comments

Comments
 (0)