Skip to content

Commit 62e9270

Browse files
committed
feat: allow callhooks from whitelisted addresses in the Arbitrum GRT bridge
1 parent 5e18e16 commit 62e9270

File tree

4 files changed

+256
-29
lines changed

4 files changed

+256
-29
lines changed

contracts/gateway/L1GraphTokenGateway.sol

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
3030
address public l2Counterpart;
3131
// Address of the BridgeEscrow contract that holds the GRT in the bridge
3232
address public escrow;
33+
// Address of the L1 Reservoir that is the only sender allowed to send extra data
34+
mapping(address => bool) public callhookWhitelist;
3335

3436
// Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2
3537
event DepositInitiated(
@@ -57,6 +59,10 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
5759
event L2CounterpartAddressSet(address _l2Counterpart);
5860
// Emitted when the escrow address has been updated
5961
event EscrowAddressSet(address _escrow);
62+
// Emitted when an address is added to the callhook whitelist
63+
event AddedToCallhookWhitelist(address newWhitelisted);
64+
// Emitted when an address is removed from the callhook whitelist
65+
event RemovedFromCallhookWhitelist(address notWhitelisted);
6066

6167
/**
6268
* @dev Allows a function to be called only by the gateway's L2 counterpart.
@@ -122,6 +128,26 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
122128
emit EscrowAddressSet(_escrow);
123129
}
124130

131+
/**
132+
* @dev Adds an address to the callhook whitelist.
133+
* This address will be allowed to include callhooks when transferring tokens.
134+
* @param newWhitelisted Address to add to the whitelist
135+
*/
136+
function addToCallhookWhitelist(address newWhitelisted) external onlyGovernor {
137+
callhookWhitelist[newWhitelisted] = true;
138+
emit AddedToCallhookWhitelist(newWhitelisted);
139+
}
140+
141+
/**
142+
* @dev Removes an address from the callhook whitelist.
143+
* This address will no longer be allowed to include callhooks when transferring tokens.
144+
* @param notWhitelisted Address to remove from the whitelist
145+
*/
146+
function removeFromCallhookWhitelist(address notWhitelisted) external onlyGovernor {
147+
callhookWhitelist[notWhitelisted] = false;
148+
emit RemovedFromCallhookWhitelist(notWhitelisted);
149+
}
150+
125151
/**
126152
* @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox.
127153
* The tokens are escrowed by the gateway until they are withdrawn back to L1.
@@ -156,7 +182,10 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
156182
{
157183
bytes memory extraData;
158184
(from, maxSubmissionCost, extraData) = parseOutboundData(_data);
159-
require(extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED");
185+
require(
186+
extraData.length == 0 || callhookWhitelist[msg.sender] == true,
187+
"CALL_HOOK_DATA_NOT_ALLOWED"
188+
);
160189
require(maxSubmissionCost > 0, "NO_SUBMISSION_COST");
161190

162191
{

contracts/l2/gateway/L2GraphTokenGateway.sol

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger {
3030
address public l1Counterpart;
3131
// Address of the Arbitrum Gateway Router on L2
3232
address public l2Router;
33-
33+
// Addresses in L1 that are whitelisted to have callhooks on transfers
34+
mapping(address => bool) public callhookWhitelist;
3435
// Calldata included in an outbound transfer, stored as a structure for convenience and stack depth
3536
struct OutboundCalldata {
3637
address from;
@@ -61,6 +62,12 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger {
6162
event L1TokenAddressSet(address _l1GRT);
6263
// Emitted when the address of the counterpart gateway on L1 has been updated
6364
event L1CounterpartAddressSet(address _l1Counterpart);
65+
// Emitted when an address is added to the callhook whitelist
66+
event AddedToCallhookWhitelist(address newWhitelisted);
67+
// Emitted when an address is removed from the callhook whitelist
68+
event RemovedFromCallhookWhitelist(address notWhitelisted);
69+
// Emitted when a callhook call failed
70+
event CallhookFailed(address destination);
6471

6572
/**
6673
* @dev Checks that the sender is the L2 alias of the counterpart
@@ -108,6 +115,26 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger {
108115
emit L1CounterpartAddressSet(_l1Counterpart);
109116
}
110117

118+
/**
119+
* @dev Adds an L1 address to the callhook whitelist.
120+
* This address will be allowed to include callhooks when transferring tokens.
121+
* @param newWhitelisted Address to add to the whitelist
122+
*/
123+
function addToCallhookWhitelist(address newWhitelisted) external onlyGovernor {
124+
callhookWhitelist[newWhitelisted] = true;
125+
emit AddedToCallhookWhitelist(newWhitelisted);
126+
}
127+
128+
/**
129+
* @dev Removes an L1 address from the callhook whitelist.
130+
* This address will no longer be allowed to include callhooks when transferring tokens.
131+
* @param notWhitelisted Address to remove from the whitelist
132+
*/
133+
function removeFromCallhookWhitelist(address notWhitelisted) external onlyGovernor {
134+
callhookWhitelist[notWhitelisted] = false;
135+
emit RemovedFromCallhookWhitelist(notWhitelisted);
136+
}
137+
111138
/**
112139
* @notice Burns L2 tokens and initiates a transfer to L1.
113140
* The tokens will be available on L1 only after the wait period (7 days) is over,
@@ -196,17 +223,35 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger {
196223
* @param _from Address of the sender on L1
197224
* @param _to Recipient address on L2
198225
* @param _amount Amount of tokens transferred
226+
* @param _data Extra callhook data, only used when the sender is whitelisted
199227
*/
200228
function finalizeInboundTransfer(
201229
address _l1Token,
202230
address _from,
203231
address _to,
204232
uint256 _amount,
205-
bytes calldata // _data unused in L2
233+
bytes calldata _data
206234
) external payable override notPaused onlyL1Counterpart {
207235
require(_l1Token == l1GRT, "TOKEN_NOT_GRT");
208236
require(msg.value == 0, "INVALID_NONZERO_VALUE");
209237

238+
if (_data.length > 0 && callhookWhitelist[_from] == true) {
239+
bytes memory callhookData;
240+
{
241+
bytes memory gatewayData;
242+
(gatewayData, callhookData) = abi.decode(_data, (bytes, bytes));
243+
}
244+
bool success;
245+
// solhint-disable-next-line avoid-low-level-calls
246+
(success, ) = _to.call(callhookData);
247+
// Callhooks shouldn't revert, but if they do:
248+
// we revert, so that the retryable ticket can be re-attempted
249+
// later.
250+
if (!success) {
251+
revert("CALLHOOK_FAILED");
252+
}
253+
}
254+
210255
L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount);
211256

212257
emit DepositFinalized(_l1Token, _from, _to, _amount);

test/gateway/l1GraphTokenGateway.test.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,48 @@ describe('L1GraphTokenGateway', () => {
186186
expect(await l1GraphTokenGateway.escrow()).eq(bridgeEscrow.address)
187187
})
188188
})
189+
describe('addToCallhookWhitelist', function () {
190+
it('is not callable by addreses that are not the governor', async function () {
191+
const tx = l1GraphTokenGateway
192+
.connect(tokenSender.signer)
193+
.addToCallhookWhitelist(tokenSender.address)
194+
await expect(tx).revertedWith('Caller must be Controller governor')
195+
expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false)
196+
})
197+
it('adds an address to the callhook whitelist', async function () {
198+
const tx = l1GraphTokenGateway
199+
.connect(governor.signer)
200+
.addToCallhookWhitelist(tokenSender.address)
201+
await expect(tx)
202+
.emit(l1GraphTokenGateway, 'AddedToCallhookWhitelist')
203+
.withArgs(tokenSender.address)
204+
expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true)
205+
})
206+
})
207+
describe('removeFromCallhookWhitelist', function () {
208+
it('is not callable by addreses that are not the governor', async function () {
209+
await l1GraphTokenGateway
210+
.connect(governor.signer)
211+
.addToCallhookWhitelist(tokenSender.address)
212+
const tx = l1GraphTokenGateway
213+
.connect(tokenSender.signer)
214+
.removeFromCallhookWhitelist(tokenSender.address)
215+
await expect(tx).revertedWith('Caller must be Controller governor')
216+
expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true)
217+
})
218+
it('removes an address from the callhook whitelist', async function () {
219+
await l1GraphTokenGateway
220+
.connect(governor.signer)
221+
.addToCallhookWhitelist(tokenSender.address)
222+
const tx = l1GraphTokenGateway
223+
.connect(governor.signer)
224+
.removeFromCallhookWhitelist(tokenSender.address)
225+
await expect(tx)
226+
.emit(l1GraphTokenGateway, 'RemovedFromCallhookWhitelist')
227+
.withArgs(tokenSender.address)
228+
expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false)
229+
})
230+
})
189231
describe('Pausable behavior', () => {
190232
it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => {
191233
let tx = l1GraphTokenGateway.connect(tokenSender.signer).setPaused(false)
@@ -230,7 +272,7 @@ describe('L1GraphTokenGateway', () => {
230272
})
231273

232274
context('> after configuring and unpausing', function () {
233-
const createMsgData = function () {
275+
const createMsgData = function (callHookData: string) {
234276
const selector = l1GraphTokenGateway.interface.getSighash('finalizeInboundTransfer')
235277
const params = utils.defaultAbiCoder.encode(
236278
['address', 'address', 'address', 'uint256', 'bytes'],
@@ -239,7 +281,7 @@ describe('L1GraphTokenGateway', () => {
239281
tokenSender.address,
240282
l2Receiver.address,
241283
toGRT('10'),
242-
utils.defaultAbiCoder.encode(['bytes', 'bytes'], [emptyCallHookData, emptyCallHookData]),
284+
utils.defaultAbiCoder.encode(['bytes', 'bytes'], [emptyCallHookData, callHookData]),
243285
],
244286
)
245287
const outboundData = utils.hexlify(utils.concat([selector, params]))
@@ -283,7 +325,11 @@ describe('L1GraphTokenGateway', () => {
283325
)
284326
return expectedInboxAccsEntry
285327
}
286-
const testValidOutboundTransfer = async function (signer: Signer, data: string) {
328+
const testValidOutboundTransfer = async function (
329+
signer: Signer,
330+
data: string,
331+
callHookData: string,
332+
) {
287333
const tx = l1GraphTokenGateway
288334
.connect(signer)
289335
.outboundTransfer(grt.address, l2Receiver.address, toGRT('10'), maxGas, gasPriceBid, data, {
@@ -295,7 +341,7 @@ describe('L1GraphTokenGateway', () => {
295341
.emit(l1GraphTokenGateway, 'DepositInitiated')
296342
.withArgs(grt.address, tokenSender.address, l2Receiver.address, expectedSeqNum, toGRT('10'))
297343

298-
const msgData = createMsgData()
344+
const msgData = createMsgData(callHookData)
299345
const msgDataHash = utils.keccak256(msgData)
300346
const expectedInboxAccsEntry = createInboxAccsEntry(msgDataHash)
301347

@@ -365,15 +411,15 @@ describe('L1GraphTokenGateway', () => {
365411
})
366412
it('puts tokens in escrow and creates a retryable ticket', async function () {
367413
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10'))
368-
await testValidOutboundTransfer(tokenSender.signer, defaultData)
414+
await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData)
369415
})
370416
it('decodes the sender address from messages sent by the router', async function () {
371417
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10'))
372418
const routerEncodedData = utils.defaultAbiCoder.encode(
373419
['address', 'bytes'],
374420
[tokenSender.address, defaultData],
375421
)
376-
await testValidOutboundTransfer(mockRouter.signer, routerEncodedData)
422+
await testValidOutboundTransfer(mockRouter.signer, routerEncodedData, emptyCallHookData)
377423
})
378424
it('reverts when called with the wrong value', async function () {
379425
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10'))
@@ -392,7 +438,7 @@ describe('L1GraphTokenGateway', () => {
392438
)
393439
await expect(tx).revertedWith('WRONG_ETH_VALUE')
394440
})
395-
it('reverts when called with nonempty calldata', async function () {
441+
it('reverts when called with nonempty calldata, if the sender is not whitelisted', async function () {
396442
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10'))
397443
const tx = l1GraphTokenGateway
398444
.connect(tokenSender.signer)
@@ -409,6 +455,17 @@ describe('L1GraphTokenGateway', () => {
409455
)
410456
await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED')
411457
})
458+
it('allows sending nonempty calldata, if the sender is whitelisted', async function () {
459+
await l1GraphTokenGateway
460+
.connect(governor.signer)
461+
.addToCallhookWhitelist(tokenSender.address)
462+
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10'))
463+
await testValidOutboundTransfer(
464+
tokenSender.signer,
465+
defaultDataWithNotEmptyCallHookData,
466+
notEmptyCallHookData,
467+
)
468+
})
412469
it('reverts when the sender does not have enough GRT', async function () {
413470
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('1001'))
414471
const tx = l1GraphTokenGateway
@@ -503,7 +560,7 @@ describe('L1GraphTokenGateway', () => {
503560
})
504561
it('reverts if the gateway is revoked from escrow', async function () {
505562
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10'))
506-
await testValidOutboundTransfer(tokenSender.signer, defaultData)
563+
await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData)
507564
// At this point, the gateway holds 10 GRT in escrow
508565
// But we revoke the gateway's permission to move the funds:
509566
await bridgeEscrow.connect(governor.signer).revokeAll(l1GraphTokenGateway.address)
@@ -538,7 +595,7 @@ describe('L1GraphTokenGateway', () => {
538595
})
539596
it('sends tokens out of escrow', async function () {
540597
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10'))
541-
await testValidOutboundTransfer(tokenSender.signer, defaultData)
598+
await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData)
542599
// At this point, the gateway holds 10 GRT in escrow
543600
const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData(
544601
'finalizeInboundTransfer',

0 commit comments

Comments
 (0)