Skip to content

Commit 1f32176

Browse files
committed
fix(L2GraphTokenGateway): only call a specific onTokenTransfer function in L1-L2 callhooks
1 parent 7bd0ab2 commit 1f32176

File tree

4 files changed

+93
-39
lines changed

4 files changed

+93
-39
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
/**
4+
* @title Interface for contracts that can receive callhooks through the Arbitrum GRT bridge
5+
* @dev Any contract that can receive a callhook on L2, send through the bridge from L1, must
6+
* be whitelisted by the governor, but also implement this interface that contains
7+
* the function that will actually be called by the L2GraphTokenGateway.
8+
*/
9+
pragma solidity ^0.7.6;
10+
11+
interface ICallhookReceiver {
12+
/**
13+
* @dev Receive tokens with a callhook from the bridge
14+
* @param _from Token sender in L1
15+
* @param _amount Amount of tokens that were transferred
16+
* @param _data ABI-encoded callhook data
17+
*/
18+
function onTokenTransfer(
19+
address _from,
20+
uint256 _amount,
21+
bytes calldata _data
22+
) external;
23+
}

contracts/l2/gateway/L2GraphTokenGateway.sol

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import "@openzeppelin/contracts/math/SafeMath.sol";
99
import "../../arbitrum/L2ArbitrumMessenger.sol";
1010
import "../../arbitrum/AddressAliasHelper.sol";
1111
import "../../gateway/GraphTokenGateway.sol";
12+
import "../../gateway/ICallhookReceiver.sol";
1213
import "../token/L2GraphToken.sol";
1314

1415
/**
@@ -240,15 +241,9 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran
240241
bytes memory gatewayData;
241242
(gatewayData, callhookData) = abi.decode(_data, (bytes, bytes));
242243
}
243-
bool success;
244-
// solhint-disable-next-line avoid-low-level-calls
245-
(success, ) = _to.call(callhookData);
246244
// Callhooks shouldn't revert, but if they do:
247245
// we revert, so that the retryable ticket can be re-attempted
248-
// later.
249-
if (!success) {
250-
revert("CALLHOOK_FAILED");
251-
}
246+
ICallhookReceiver(_to).onTokenTransfer(_from, _amount, callhookData);
252247
}
253248

254249
emit DepositFinalized(_l1Token, _from, _to, _amount);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.6;
4+
5+
import "../gateway/ICallhookReceiver.sol";
6+
7+
/**
8+
* @title GovernedMock contract
9+
*/
10+
contract CallhookReceiverMock is ICallhookReceiver {
11+
event TransferReceived(address from, uint256 amount, uint256 foo, uint256 bar);
12+
13+
/**
14+
* @dev Receive tokens with a callhook from the bridge
15+
* Expects two uint256 values encoded in _data.
16+
* Reverts if the first of these values is zero.
17+
* @param _from Token sender in L1
18+
* @param _amount Amount of tokens that were transferred
19+
* @param _data ABI-encoded callhook data
20+
*/
21+
function onTokenTransfer(
22+
address _from,
23+
uint256 _amount,
24+
bytes calldata _data
25+
) external override {
26+
uint256 foo;
27+
uint256 bar;
28+
(foo, bar) = abi.decode(_data, (uint256, uint256));
29+
require(foo != 0, "FOO_IS_ZERO");
30+
emit TransferReceived(_from, _amount, foo, bar);
31+
}
32+
}

test/l2/l2GraphTokenGateway.test.ts

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,17 @@ import { constants, ContractTransaction, Signer, utils } from 'ethers'
33

44
import { L2GraphToken } from '../../build/types/L2GraphToken'
55
import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway'
6+
import { CallhookReceiverMock } from '../../build/types/CallhookReceiverMock'
67

78
import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures'
89

910
import { FakeContract, smock } from '@defi-wonderland/smock'
1011

11-
import path from 'path'
12-
import { Artifacts } from 'hardhat/internal/artifacts'
13-
const ARTIFACTS_PATH = path.resolve('build/contracts')
14-
const artifacts = new Artifacts(ARTIFACTS_PATH)
15-
const rewardsManagerMockAbi = artifacts.readArtifactSync('RewardsManagerMock').abi
16-
1712
use(smock.matchers)
1813

1914
import { getAccounts, toGRT, Account, toBN, getL2SignerFromL1 } from '../lib/testHelpers'
2015
import { Interface } from 'ethers/lib/utils'
16+
import { deployContract } from '../lib/deployment'
2117

2218
const { AddressZero } = constants
2319

@@ -37,11 +33,14 @@ describe('L2GraphTokenGateway', () => {
3733
let fixtureContracts: L2FixtureContracts
3834
let grt: L2GraphToken
3935
let l2GraphTokenGateway: L2GraphTokenGateway
36+
let callhookReceiverMock: CallhookReceiverMock
4037

4138
const senderTokens = toGRT('1000')
4239
const defaultData = '0x'
43-
const mockIface = new Interface(rewardsManagerMockAbi)
44-
const notEmptyCallHookData = mockIface.encodeFunctionData('pow', [toBN(1), toBN(2), toBN(3)])
40+
const notEmptyCallHookData = utils.defaultAbiCoder.encode(
41+
['uint256', 'uint256'],
42+
[toBN('1337'), toBN('42')],
43+
)
4544
const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode(
4645
['bytes', 'bytes'],
4746
['0x', notEmptyCallHookData],
@@ -64,6 +63,11 @@ describe('L2GraphTokenGateway', () => {
6463
fixtureContracts = await fixture.loadL2(governor.signer)
6564
;({ grt, l2GraphTokenGateway } = fixtureContracts)
6665

66+
callhookReceiverMock = (await deployContract(
67+
'CallhookReceiverMock',
68+
governor.signer,
69+
)) as unknown as CallhookReceiverMock
70+
6771
// Give some funds to the token sender
6872
await grt.connect(governor.signer).mint(tokenSender.address, senderTokens)
6973
})
@@ -319,32 +323,28 @@ describe('L2GraphTokenGateway', () => {
319323
describe('finalizeInboundTransfer', function () {
320324
const testValidFinalizeTransfer = async function (
321325
data: string,
326+
to?: string,
322327
): Promise<ContractTransaction> {
328+
to = to ?? l2Receiver.address
323329
const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address)
324330
await me.signer.sendTransaction({
325331
to: await mockL1GatewayL2Alias.getAddress(),
326332
value: utils.parseUnits('1', 'ether'),
327333
})
328334
const tx = l2GraphTokenGateway
329335
.connect(mockL1GatewayL2Alias)
330-
.finalizeInboundTransfer(
331-
mockL1GRT.address,
332-
tokenSender.address,
333-
l2Receiver.address,
334-
toGRT('10'),
335-
data,
336-
)
336+
.finalizeInboundTransfer(mockL1GRT.address, tokenSender.address, to, toGRT('10'), data)
337337
await expect(tx)
338338
.emit(l2GraphTokenGateway, 'DepositFinalized')
339-
.withArgs(mockL1GRT.address, tokenSender.address, l2Receiver.address, toGRT('10'))
339+
.withArgs(mockL1GRT.address, tokenSender.address, to, toGRT('10'))
340340

341-
await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Receiver.address, toGRT('10'))
341+
await expect(tx).emit(grt, 'BridgeMinted').withArgs(to, toGRT('10'))
342342

343343
// Unchanged
344344
const senderBalance = await grt.balanceOf(tokenSender.address)
345345
await expect(senderBalance).eq(toGRT('1000'))
346346
// 10 newly minted GRT
347-
const receiverBalance = await grt.balanceOf(l2Receiver.address)
347+
const receiverBalance = await grt.balanceOf(to)
348348
await expect(receiverBalance).eq(toGRT('10'))
349349
return tx
350350
}
@@ -375,19 +375,23 @@ describe('L2GraphTokenGateway', () => {
375375
it('mints and sends tokens when called by the aliased gateway', async function () {
376376
await testValidFinalizeTransfer(defaultData)
377377
})
378-
it('calls a callhook if the sender is whitelisted', async function () {
379-
const rewardsManagerMock = await smock.fake('RewardsManagerMock', {
380-
address: l2Receiver.address,
381-
})
382-
rewardsManagerMock.pow.returns(1)
383-
await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData)
384-
expect(rewardsManagerMock.pow).to.have.been.calledWith(toBN(1), toBN(2), toBN(3))
378+
it('calls a callhook if the transfer includes calldata', async function () {
379+
const tx = await testValidFinalizeTransfer(
380+
defaultDataWithNotEmptyCallHookData,
381+
callhookReceiverMock.address,
382+
)
383+
// Emitted by the callhook:
384+
await expect(tx)
385+
.emit(callhookReceiverMock, 'TransferReceived')
386+
.withArgs(tokenSender.address, toGRT('10'), toBN('1337'), toBN('42'))
385387
})
386388
it('reverts if a callhook reverts', async function () {
387-
const rewardsManagerMock = await smock.fake('RewardsManagerMock', {
388-
address: l2Receiver.address,
389-
})
390-
rewardsManagerMock.pow.reverts()
389+
// The 0 will make the callhook revert (see CallhookReceiverMock.sol)
390+
const callHookData = utils.defaultAbiCoder.encode(
391+
['uint256', 'uint256'],
392+
[toBN('0'), toBN('42')],
393+
)
394+
const data = utils.defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', callHookData])
391395
const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address)
392396
await me.signer.sendTransaction({
393397
to: await mockL1GatewayL2Alias.getAddress(),
@@ -398,11 +402,11 @@ describe('L2GraphTokenGateway', () => {
398402
.finalizeInboundTransfer(
399403
mockL1GRT.address,
400404
tokenSender.address,
401-
l2Receiver.address,
405+
callhookReceiverMock.address,
402406
toGRT('10'),
403-
defaultDataWithNotEmptyCallHookData,
407+
data,
404408
)
405-
await expect(tx).revertedWith('CALLHOOK_FAILED')
409+
await expect(tx).revertedWith('FOO_IS_ZERO')
406410
})
407411
})
408412
})

0 commit comments

Comments
 (0)