From 889cc5e9b51657c40bd77914d3d2eea0f18e1d24 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Mon, 26 Oct 2026 09:55:05 -0300 Subject: [PATCH 1/3] test: add TokenUtilsMock for the graph-token Foundry tests --- .../contracts/tests/TokenUtilsMock.sol | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 packages/contracts/contracts/tests/TokenUtilsMock.sol diff --git a/packages/contracts/contracts/tests/TokenUtilsMock.sol b/packages/contracts/contracts/tests/TokenUtilsMock.sol new file mode 100644 index 000000000..eeb9f20af --- /dev/null +++ b/packages/contracts/contracts/tests/TokenUtilsMock.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +// solhint-disable use-natspec + +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; +import { TokenUtils } from "../utils/TokenUtils.sol"; + +contract TokenUtilsMock { + function pullTokens(IGraphToken token, address from, uint256 amount) external { + TokenUtils.pullTokens(token, from, amount); + } + + function pushTokens(IGraphToken token, address to, uint256 amount) external { + TokenUtils.pushTokens(token, to, amount); + } + + function burnTokens(IGraphToken token, uint256 amount) external { + TokenUtils.burnTokens(token, amount); + } +} From 1b79322fcdb1dfcaf81efb34c4cb40bd5c5cd1bb Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Mon, 26 Oct 2026 09:55:06 -0300 Subject: [PATCH 2/3] test(graph-token): port token, escrow and token utils tests to Foundry --- .../tests/unit/gateway/bridgeEscrow.test.ts | 77 ------ .../tests/unit/graphToken.test.ts | 5 - .../tests/unit/l2/l2GraphToken.test.ts | 110 -------- .../tests/unit/lib/graphTokenTests.ts | 238 ----------------- packages/graph-token/foundry.toml | 22 ++ packages/graph-token/package.json | 19 ++ .../test/unit/gateway/BridgeEscrow.t.sol | 80 ++++++ .../unit/interfaces/IBridgeEscrowLike.sol | 10 + .../test/unit/interfaces/IGraphTokenLike.sol | 53 ++++ .../unit/interfaces/IL2GraphTokenLike.sol | 25 ++ .../test/unit/interfaces/ITokenUtilsMock.sol | 12 + .../test/unit/shared/ProtocolFixture.t.sol | 66 +++++ .../test/unit/token/GraphToken.t.sol | 27 ++ .../test/unit/token/GraphTokenShared.t.sol | 252 ++++++++++++++++++ .../test/unit/token/L2GraphToken.t.sol | 149 +++++++++++ .../test/unit/utils/TokenUtils.t.sol | 75 ++++++ pnpm-lock.yaml | 12 + .../contracts-migration/test-mapping/t2a.md | 42 +++ 18 files changed, 844 insertions(+), 430 deletions(-) delete mode 100644 packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts delete mode 100644 packages/contracts-test/tests/unit/graphToken.test.ts delete mode 100644 packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts delete mode 100644 packages/contracts-test/tests/unit/lib/graphTokenTests.ts create mode 100644 packages/graph-token/foundry.toml create mode 100644 packages/graph-token/package.json create mode 100644 packages/graph-token/test/unit/gateway/BridgeEscrow.t.sol create mode 100644 packages/graph-token/test/unit/interfaces/IBridgeEscrowLike.sol create mode 100644 packages/graph-token/test/unit/interfaces/IGraphTokenLike.sol create mode 100644 packages/graph-token/test/unit/interfaces/IL2GraphTokenLike.sol create mode 100644 packages/graph-token/test/unit/interfaces/ITokenUtilsMock.sol create mode 100644 packages/graph-token/test/unit/shared/ProtocolFixture.t.sol create mode 100644 packages/graph-token/test/unit/token/GraphToken.t.sol create mode 100644 packages/graph-token/test/unit/token/GraphTokenShared.t.sol create mode 100644 packages/graph-token/test/unit/token/L2GraphToken.t.sol create mode 100644 packages/graph-token/test/unit/utils/TokenUtils.t.sol create mode 100644 scripts/contracts-migration/test-mapping/t2a.md diff --git a/packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts b/packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts deleted file mode 100644 index abeb21e39..000000000 --- a/packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { GraphToken } from '@graphprotocol/contracts' -import { BridgeEscrow } from '@graphprotocol/contracts' -import { GraphNetworkContracts, toGRT } from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { BigNumber } from 'ethers' -import hre from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' - -describe('BridgeEscrow', () => { - const graph = hre.graph() - let governor: SignerWithAddress - let tokenReceiver: SignerWithAddress - let spender: SignerWithAddress - - let fixture: NetworkFixture - - let contracts: GraphNetworkContracts - let grt: GraphToken - let bridgeEscrow: BridgeEscrow - - const nTokens = toGRT('1000') - - before(async function () { - ;[tokenReceiver, spender] = await graph.getTestAccounts() - ;({ governor } = await graph.getNamedAccounts()) - - fixture = new NetworkFixture(graph.provider) - contracts = await fixture.load(governor) - grt = contracts.GraphToken as GraphToken - bridgeEscrow = contracts.BridgeEscrow as BridgeEscrow - - // Give some funds to the Escrow - await grt.connect(governor).mint(bridgeEscrow.address, nTokens) - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('approveAll', function () { - it('cannot be called by someone other than the governor', async function () { - const tx = bridgeEscrow.connect(tokenReceiver).approveAll(spender.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('allows a spender to transfer GRT held by the contract', async function () { - expect(await grt.allowance(bridgeEscrow.address, spender.address)).eq(0) - const tx = grt.connect(spender).transferFrom(bridgeEscrow.address, tokenReceiver.address, nTokens) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - await bridgeEscrow.connect(governor).approveAll(spender.address) - await expect(grt.connect(spender).transferFrom(bridgeEscrow.address, tokenReceiver.address, nTokens)).to.emit( - grt, - 'Transfer', - ) - expect(await grt.balanceOf(tokenReceiver.address)).to.eq(nTokens) - }) - }) - - describe('revokeAll', function () { - it('cannot be called by someone other than the governor', async function () { - const tx = bridgeEscrow.connect(tokenReceiver).revokeAll(spender.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it("revokes a spender's permission to transfer GRT held by the contract", async function () { - await bridgeEscrow.connect(governor).approveAll(spender.address) - await bridgeEscrow.connect(governor).revokeAll(spender.address) - // We shouldn't be able to transfer _anything_ - const tx = grt.connect(spender).transferFrom(bridgeEscrow.address, tokenReceiver.address, BigNumber.from('1')) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - }) - }) -}) diff --git a/packages/contracts-test/tests/unit/graphToken.test.ts b/packages/contracts-test/tests/unit/graphToken.test.ts deleted file mode 100644 index b42274ac4..000000000 --- a/packages/contracts-test/tests/unit/graphToken.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { grtTests } from './lib/graphTokenTests' - -describe('GraphToken', () => { - grtTests.bind(this)(false) -}) diff --git a/packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts b/packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts deleted file mode 100644 index 5b45c7cd8..000000000 --- a/packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { L2GraphToken } from '@graphprotocol/contracts' -import { GraphNetworkContracts, toGRT } from '@graphprotocol/sdk' -import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' -import { grtTests } from '../lib/graphTokenTests' - -describe('L2GraphToken', () => { - describe('Base GRT behavior', () => { - grtTests.bind(this)(true) - }) - describe('Extended L2 behavior', () => { - const graph = hre.graph() - let mockL2Gateway: SignerWithAddress - let mockL1GRT: SignerWithAddress - let governor: SignerWithAddress - let user: SignerWithAddress - - let contracts: GraphNetworkContracts - let fixture: NetworkFixture - let grt: L2GraphToken - - before(async function () { - ;[mockL1GRT, mockL2Gateway, user] = await graph.getTestAccounts() - ;({ governor } = await graph.getNamedAccounts()) - fixture = new NetworkFixture(graph.provider) - contracts = await fixture.load(governor, true) - grt = contracts.L2GraphToken as L2GraphToken - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('setGateway', function () { - it('cannot be called by someone who is not the governor', async function () { - const tx = grt.connect(mockL2Gateway).setGateway(mockL2Gateway.address) - await expect(tx).revertedWith('Only Governor can call') - }) - it('sets the L2 Gateway address when called by the governor', async function () { - const tx = grt.connect(governor).setGateway(mockL2Gateway.address) - await expect(tx).emit(grt, 'GatewaySet').withArgs(mockL2Gateway.address) - expect(await grt.gateway()).eq(mockL2Gateway.address) - }) - }) - describe('setL1Address', function () { - it('cannot be called by someone who is not the governor', async function () { - const tx = grt.connect(mockL2Gateway).setL1Address(mockL1GRT.address) - await expect(tx).revertedWith('Only Governor can call') - }) - it('sets the L1 GRT address when called by the governor', async function () { - const tx = grt.connect(governor).setL1Address(mockL1GRT.address) - await expect(tx).emit(grt, 'L1AddressSet').withArgs(mockL1GRT.address) - expect(await grt.l1Address()).eq(mockL1GRT.address) - }) - }) - describe('bridge minting and burning', function () { - beforeEach(async function () { - // Configure the l1Address and gateway - await grt.connect(governor).setL1Address(mockL1GRT.address) - await grt.connect(governor).setGateway(mockL2Gateway.address) - }) - describe('bridgeMint', function () { - it('cannot be called by someone who is not the gateway', async function () { - const tx = grt.connect(governor).bridgeMint(user.address, toGRT('100')) - await expect(tx).revertedWith('NOT_GATEWAY') - }) - it('mints GRT into a destination account', async function () { - const tx = grt.connect(mockL2Gateway).bridgeMint(user.address, toGRT('100')) - await expect(tx).emit(grt, 'BridgeMinted').withArgs(user.address, toGRT('100')) - expect(await grt.balanceOf(user.address)).eq(toGRT('100')) - }) - }) - describe('bridgeBurn', function () { - it('cannot be called by someone who is not the gateway', async function () { - const tx = grt.connect(governor).bridgeBurn(user.address, toGRT('100')) - await expect(tx).revertedWith('NOT_GATEWAY') - }) - it('requires approval for burning', async function () { - await grt.connect(mockL2Gateway).bridgeMint(user.address, toGRT('100')) - const tx = grt.connect(mockL2Gateway).bridgeBurn(user.address, toGRT('20')) - await expect(tx).revertedWith('ERC20: burn amount exceeds allowance') - }) - it('fails if the user does not have enough funds', async function () { - await grt.connect(mockL2Gateway).bridgeMint(user.address, toGRT('10')) - await grt.connect(user).approve(mockL2Gateway.address, toGRT('20')) - const tx = grt.connect(mockL2Gateway).bridgeBurn(user.address, toGRT('20')) - await expect(tx).revertedWith('ERC20: burn amount exceeds balance') - }) - it('burns GRT from an account when approved', async function () { - await grt.connect(mockL2Gateway).bridgeMint(user.address, toGRT('100')) - await grt.connect(user).approve(mockL2Gateway.address, toGRT('20')) - const tx = grt.connect(mockL2Gateway).bridgeBurn(user.address, toGRT('20')) - await expect(tx).emit(grt, 'BridgeBurned').withArgs(user.address, toGRT('20')) - expect(await grt.balanceOf(user.address)).eq(toGRT('80')) - }) - }) - it('does not allow the bridge to mint as a regular minter', async function () { - const tx = grt.connect(mockL2Gateway).mint(user.address, toGRT('100')) - await expect(tx).revertedWith('Only minter can call') - }) - }) - }) -}) diff --git a/packages/contracts-test/tests/unit/lib/graphTokenTests.ts b/packages/contracts-test/tests/unit/lib/graphTokenTests.ts deleted file mode 100644 index dadab32f4..000000000 --- a/packages/contracts-test/tests/unit/lib/graphTokenTests.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { L2GraphToken } from '@graphprotocol/contracts' -import { GraphToken } from '@graphprotocol/contracts' -import { GraphNetworkContracts, Permit, signPermit, toBN, toGRT } from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { BigNumber, constants, ethers, Signature, Wallet } from 'ethers' -import hre from 'hardhat' - -import { NetworkFixture } from './fixtures' - -const { AddressZero, MaxUint256 } = constants - -const L1SALT = '0x51f3d585afe6dfeb2af01bba0889a36c1db03beec88c6a4d0c53817069026afa' -const L2SALT = '0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591' - -export function grtTests(isL2: boolean): void { - let me: Wallet - let other: Wallet - let governor: SignerWithAddress - let salt: string - let fixture: NetworkFixture - let fixtureContracts: GraphNetworkContracts - - const graph = hre.graph() - - const mePrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' - const otherPrivateKey = '0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1' - - let grt: GraphToken | L2GraphToken - - async function permitMaxOK(): Promise { - return permitOK(MaxUint256) - } - - async function permitOK(value: BigNumber): Promise { - const nonce = await grt.nonces(me.address) - return { - owner: me.address, - spender: other.address, - value: value, - nonce: nonce, - deadline: toBN('0'), - } - } - - async function permitExpired(): Promise { - const permit = await permitMaxOK() - permit.deadline = toBN('1') - return permit - } - - async function permitDeny(): Promise { - const permit = await permitMaxOK() - permit.value = toBN('0') - return permit - } - - function createPermitTransaction(permit: Permit, signer: string, salt: string) { - const signature: Signature = signPermit(signer, graph.chainId, grt.address, permit, salt) - const wallet = new ethers.Wallet(signer, graph.provider) - return grt - .connect(wallet) - .permit(permit.owner, permit.spender, permit.value, permit.deadline, signature.v, signature.r, signature.s) - } - - before(async function () { - ;({ governor } = await graph.getNamedAccounts()) - me = new ethers.Wallet(mePrivateKey, graph.provider) - other = new ethers.Wallet(otherPrivateKey, graph.provider) - - fixture = new NetworkFixture(graph.provider) - fixtureContracts = await fixture.load(governor, isL2) - grt = fixtureContracts.GraphToken as GraphToken - salt = isL2 ? L2SALT : L1SALT - - // Mint some tokens - const tokens = toGRT('10000') - await grt.connect(governor).mint(me.address, tokens) - }) - - describe('permit', function () { - it('should permit max token allowance', async function () { - // Allow to transfer tokens - const tokensToApprove = toGRT('1000') - const permit = await permitOK(tokensToApprove) - const tx = createPermitTransaction(permit, mePrivateKey, salt) - await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, tokensToApprove) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(tokensToApprove) - - // Transfer tokens should work - const tokens = toGRT('100') - await grt.connect(other).transferFrom(me.address, other.address, tokens) - }) - - it('should permit max token allowance', async function () { - // Allow to transfer tokens - const permit = await permitMaxOK() - const tx = createPermitTransaction(permit, mePrivateKey, salt) - await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, MaxUint256) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(MaxUint256) - - // Transfer tokens should work - const tokens = toGRT('100') - await grt.connect(other).transferFrom(me.address, other.address, tokens) - }) - - it('reject to transfer more tokens than approved by permit', async function () { - // Allow to transfer tokens - const tokensToApprove = toGRT('1000') - const permit = await permitOK(tokensToApprove) - await createPermitTransaction(permit, mePrivateKey, salt) - - // Should not transfer more than approved - const tooManyTokens = toGRT('1001') - const tx = grt.connect(other).transferFrom(me.address, other.address, tooManyTokens) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - - // Should transfer up to the approved amount - await grt.connect(other).transferFrom(me.address, other.address, tokensToApprove) - }) - - it('reject use two permits with same nonce', async function () { - // Allow to transfer tokens - const permit = await permitMaxOK() - await createPermitTransaction(permit, mePrivateKey, salt) - - // Try to re-use the permit - const tx = createPermitTransaction(permit, mePrivateKey, salt) - await expect(tx).revertedWith('GRT: invalid permit') - }) - - it('reject use expired permit', async function () { - const permit = await permitExpired() - const tx = createPermitTransaction(permit, mePrivateKey, salt) - await expect(tx).revertedWith('GRT: expired permit') - }) - - it('reject permit if holder address does not match', async function () { - const permit = await permitMaxOK() - const tx = createPermitTransaction(permit, otherPrivateKey, salt) - await expect(tx).revertedWith('GRT: invalid permit') - }) - - it('should deny transfer from if permit was denied', async function () { - // Allow to transfer tokens - const permit1 = await permitMaxOK() - await createPermitTransaction(permit1, mePrivateKey, salt) - - // Deny transfer tokens - const permit2 = await permitDeny() - await createPermitTransaction(permit2, mePrivateKey, salt) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(toBN('0')) - - // Try to transfer without permit should fail - const tokens = toGRT('100') - const tx = grt.connect(other).transferFrom(me.address, other.address, tokens) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - }) - }) - - describe('mint', function () { - describe('mint', function () { - it('reject mint if not minter', async function () { - const tx = grt.connect(me).mint(me.address, toGRT('100')) - await expect(tx).revertedWith('Only minter can call') - }) - }) - - describe('addMinter', function () { - it('reject add a new minter if not allowed', async function () { - const tx = grt.connect(me).addMinter(me.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('should add a new minter', async function () { - expect(await grt.isMinter(me.address)).eq(false) - const tx = grt.connect(governor).addMinter(me.address) - await expect(tx).emit(grt, 'MinterAdded').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(true) - }) - }) - - context('> when is minter', function () { - beforeEach(async function () { - await grt.connect(governor).addMinter(me.address) - expect(await grt.isMinter(me.address)).eq(true) - }) - - describe('mint', function () { - it('should mint', async function () { - const beforeTokens = await grt.balanceOf(me.address) - - const tokensToMint = toGRT('100') - const tx = grt.connect(me).mint(me.address, tokensToMint) - await expect(tx).emit(grt, 'Transfer').withArgs(AddressZero, me.address, tokensToMint) - - const afterTokens = await grt.balanceOf(me.address) - expect(afterTokens).eq(beforeTokens.add(tokensToMint)) - }) - - it('should mint if governor', async function () { - const tokensToMint = toGRT('100') - await grt.connect(governor).mint(me.address, tokensToMint) - }) - }) - - describe('removeMinter', function () { - it('reject remove a minter if not allowed', async function () { - const tx = grt.connect(me).removeMinter(me.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('should remove a minter', async function () { - const tx = grt.connect(governor).removeMinter(me.address) - await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(false) - }) - }) - - describe('renounceMinter', function () { - it('should renounce to be a minter', async function () { - const tx = grt.connect(me).renounceMinter() - await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(false) - }) - }) - }) - }) -} diff --git a/packages/graph-token/foundry.toml b/packages/graph-token/foundry.toml new file mode 100644 index 000000000..35b9b51af --- /dev/null +++ b/packages/graph-token/foundry.toml @@ -0,0 +1,22 @@ +[profile.default] +src = 'test' +out = 'forge-artifacts' +test = 'test' +libs = ["node_modules"] +cache_path = 'cache_forge' +auto_detect_remappings = false +remappings = [ + "@graphprotocol/=node_modules/@graphprotocol/", + "forge-std/=node_modules/forge-std/src/", +] +solc_version = '0.8.35' +evm_version = 'cancun' +fs_permissions = [{ access = "read", path = "node_modules/@graphprotocol/contracts/artifacts" }] +ignored_warnings_from = ["node_modules"] + +[lint] +# Disable lint-on-build: forge's lint walker follows symlinks without cycle detection and +# infinite-loops on this package's pnpm-hoisted node_modules. +lint_on_build = false +exclude_lints = ["mixed-case-function", "mixed-case-variable", "block-timestamp"] +ignore = ["node_modules/**", "**/node_modules/**"] diff --git a/packages/graph-token/package.json b/packages/graph-token/package.json new file mode 100644 index 000000000..964a91c74 --- /dev/null +++ b/packages/graph-token/package.json @@ -0,0 +1,19 @@ +{ + "name": "@graphprotocol/graph-token", + "version": "0.0.0", + "private": true, + "description": "The Graph token and bridge escrow contracts", + "license": "GPL-2.0-or-later", + "scripts": { + "build": "pnpm build:dep", + "build:dep": "pnpm --filter '@graphprotocol/graph-token^...' run build:self", + "clean": "rm -rf forge-artifacts/ cache_forge/", + "test": "pnpm build && pnpm test:self", + "test:self": "forge test" + }, + "devDependencies": { + "@graphprotocol/contracts": "workspace:^", + "@graphprotocol/interfaces": "workspace:^", + "forge-std": "catalog:" + } +} diff --git a/packages/graph-token/test/unit/gateway/BridgeEscrow.t.sol b/packages/graph-token/test/unit/gateway/BridgeEscrow.t.sol new file mode 100644 index 000000000..6c6cd3607 --- /dev/null +++ b/packages/graph-token/test/unit/gateway/BridgeEscrow.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IManaged } from "@graphprotocol/interfaces/contracts/contracts/governance/IManaged.sol"; +import { ProtocolFixture } from "../shared/ProtocolFixture.t.sol"; +import { IGraphTokenLike } from "../interfaces/IGraphTokenLike.sol"; +import { IBridgeEscrowLike } from "../interfaces/IBridgeEscrowLike.sol"; + +contract BridgeEscrowTest is ProtocolFixture { + string internal constant GRAPH_TOKEN = string.concat(PKG_ARTIFACTS, "token/GraphToken.sol/GraphToken.json"); + string internal constant BRIDGE_ESCROW = string.concat(PKG_ARTIFACTS, "gateway/BridgeEscrow.sol/BridgeEscrow.json"); + + uint256 internal constant N_TOKENS = 1000 ether; + + IGraphTokenLike internal grt; + IBridgeEscrowLike internal bridgeEscrow; + address internal tokenReceiver = makeAddr("tokenReceiver"); + address internal spender = makeAddr("spender"); + + function setUp() public { + vm.warp(1_700_000_000); + _deployProtocolBase(); + + vm.prank(governor); + grt = IGraphTokenLike(_deployArtifact(GRAPH_TOKEN, abi.encode(10_000_000_000 ether))); + _registerContract("GraphToken", address(grt)); + + address impl = _deployArtifact(BRIDGE_ESCROW, ""); + bridgeEscrow = IBridgeEscrowLike( + _deployBehindProxy(impl, abi.encodeCall(IBridgeEscrowLike.initialize, (address(controller)))) + ); + + // Cache GraphToken in the escrow + IManaged(address(bridgeEscrow)).syncAllContracts(); + + vm.prank(governor); + grt.mint(address(bridgeEscrow), N_TOKENS); + } + + function test_ApproveAll_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenReceiver); + bridgeEscrow.approveAll(spender); + } + + function test_ApproveAll_AllowsSpenderToTransfer() public { + assertEq(grt.allowance(address(bridgeEscrow), spender), 0); + + vm.expectRevert(bytes("ERC20: transfer amount exceeds allowance")); + vm.prank(spender); + grt.transferFrom(address(bridgeEscrow), tokenReceiver, N_TOKENS); + + vm.prank(governor); + bridgeEscrow.approveAll(spender); + + vm.expectEmit(address(grt)); + emit IGraphTokenLike.Transfer(address(bridgeEscrow), tokenReceiver, N_TOKENS); + vm.prank(spender); + grt.transferFrom(address(bridgeEscrow), tokenReceiver, N_TOKENS); + + assertEq(grt.balanceOf(tokenReceiver), N_TOKENS); + } + + function test_RevokeAll_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenReceiver); + bridgeEscrow.revokeAll(spender); + } + + function test_RevokeAll_RevokesSpender() public { + vm.startPrank(governor); + bridgeEscrow.approveAll(spender); + bridgeEscrow.revokeAll(spender); + vm.stopPrank(); + + vm.expectRevert(bytes("ERC20: transfer amount exceeds allowance")); + vm.prank(spender); + grt.transferFrom(address(bridgeEscrow), tokenReceiver, 1); + } +} diff --git a/packages/graph-token/test/unit/interfaces/IBridgeEscrowLike.sol b/packages/graph-token/test/unit/interfaces/IBridgeEscrowLike.sol new file mode 100644 index 000000000..a7e9c6be9 --- /dev/null +++ b/packages/graph-token/test/unit/interfaces/IBridgeEscrowLike.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface IBridgeEscrowLike { + function initialize(address controller) external; + + function approveAll(address spender) external; + + function revokeAll(address spender) external; +} diff --git a/packages/graph-token/test/unit/interfaces/IGraphTokenLike.sol b/packages/graph-token/test/unit/interfaces/IGraphTokenLike.sol new file mode 100644 index 000000000..f6ffd171f --- /dev/null +++ b/packages/graph-token/test/unit/interfaces/IGraphTokenLike.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface IGraphTokenLike { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + event MinterAdded(address indexed account); + event MinterRemoved(address indexed account); + + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 amount) external returns (bool); + + function transferFrom(address from, address to, uint256 amount) external returns (bool); + + function approve(address spender, uint256 amount) external returns (bool); + + function allowance(address owner, address spender) external view returns (uint256); + + function burn(uint256 amount) external; + + function burnFrom(address account, uint256 amount) external; + + function mint(address to, uint256 amount) external; + + function addMinter(address account) external; + + function removeMinter(address account) external; + + function renounceMinter() external; + + function isMinter(address account) external view returns (bool); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function nonces(address owner) external view returns (uint256); + + function governor() external view returns (address); + + function transferOwnership(address newGovernor) external; + + function acceptOwnership() external; +} diff --git a/packages/graph-token/test/unit/interfaces/IL2GraphTokenLike.sol b/packages/graph-token/test/unit/interfaces/IL2GraphTokenLike.sol new file mode 100644 index 000000000..47ccb039c --- /dev/null +++ b/packages/graph-token/test/unit/interfaces/IL2GraphTokenLike.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IGraphTokenLike } from "./IGraphTokenLike.sol"; + +interface IL2GraphTokenLike is IGraphTokenLike { + event BridgeMinted(address indexed account, uint256 amount); + event BridgeBurned(address indexed account, uint256 amount); + event GatewaySet(address gateway); + event L1AddressSet(address l1Address); + + function initialize(address owner) external; + + function gateway() external view returns (address); + + function l1Address() external view returns (address); + + function setGateway(address gw) external; + + function setL1Address(address addr) external; + + function bridgeMint(address account, uint256 amount) external; + + function bridgeBurn(address account, uint256 amount) external; +} diff --git a/packages/graph-token/test/unit/interfaces/ITokenUtilsMock.sol b/packages/graph-token/test/unit/interfaces/ITokenUtilsMock.sol new file mode 100644 index 000000000..601a381f9 --- /dev/null +++ b/packages/graph-token/test/unit/interfaces/ITokenUtilsMock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IGraphTokenLike } from "./IGraphTokenLike.sol"; + +interface ITokenUtilsMock { + function pullTokens(IGraphTokenLike token, address from, uint256 amount) external; + + function pushTokens(IGraphTokenLike token, address to, uint256 amount) external; + + function burnTokens(IGraphTokenLike token, uint256 amount) external; +} diff --git a/packages/graph-token/test/unit/shared/ProtocolFixture.t.sol b/packages/graph-token/test/unit/shared/ProtocolFixture.t.sol new file mode 100644 index 000000000..f38962c8e --- /dev/null +++ b/packages/graph-token/test/unit/shared/ProtocolFixture.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { Test } from "forge-std/Test.sol"; +import { IController } from "@graphprotocol/interfaces/contracts/contracts/governance/IController.sol"; +import { IGraphProxy } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxy.sol"; +import { IGraphProxyAdmin } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol"; + +abstract contract ProtocolFixture is Test { + string internal constant CORE_ARTIFACTS = "node_modules/@graphprotocol/contracts/artifacts/contracts/"; + string internal constant PKG_ARTIFACTS = "node_modules/@graphprotocol/contracts/artifacts/contracts/"; + + address internal governor; // makeAddr("governor"), set in _deployProtocolBase + IController internal controller; + IGraphProxyAdmin internal proxyAdmin; + + /// path is a full artifact path, e.g. string.concat(CORE_ARTIFACTS, "governance/Controller.sol/Controller.json"). + /// Deploys `.bytecode ++ args` with CREATE (honours an active vm.prank). + /// Reverts "artifact needs linking" if `.linkReferences` has any key (vm.parseJsonKeys), + /// and "artifact deploy failed" if CREATE returns 0. + function _deployArtifact(string memory path, bytes memory args) internal returns (address deployed) { + string memory json = vm.readFile(path); + require(vm.parseJsonKeys(json, ".linkReferences").length == 0, "artifact needs linking"); + bytes memory code = bytes.concat(vm.parseJsonBytes(json, ".bytecode"), args); + // solhint-disable-next-line no-inline-assembly + assembly { + deployed := create(0, add(code, 0x20), mload(code)) + } + require(deployed != address(0), "artifact deploy failed"); + } + + /// As governor: deploys Controller and GraphProxyAdmin from CORE_ARTIFACTS, then controller.setPaused(false). + /// Must be called with no active prank. + function _deployProtocolBase() internal { + governor = makeAddr("governor"); + vm.startPrank(governor); + controller = IController( + _deployArtifact(string.concat(CORE_ARTIFACTS, "governance/Controller.sol/Controller.json"), "") + ); + proxyAdmin = IGraphProxyAdmin( + _deployArtifact(string.concat(CORE_ARTIFACTS, "upgrades/GraphProxyAdmin.sol/GraphProxyAdmin.json"), "") + ); + controller.setPaused(false); + vm.stopPrank(); + } + + /// As governor: deploys GraphProxy(address(0), proxyAdmin) from CORE_ARTIFACTS, + /// proxyAdmin.upgrade(proxy, impl), proxyAdmin.acceptProxyAndCall(impl, proxy, initData). + /// Must be called with no active prank. + function _deployBehindProxy(address impl, bytes memory initData) internal returns (address proxy) { + vm.startPrank(governor); + proxy = _deployArtifact( + string.concat(CORE_ARTIFACTS, "upgrades/GraphProxy.sol/GraphProxy.json"), + abi.encode(address(0), address(proxyAdmin)) + ); + proxyAdmin.upgrade(IGraphProxy(proxy), impl); + proxyAdmin.acceptProxyAndCall(impl, IGraphProxy(proxy), initData); + vm.stopPrank(); + } + + /// As governor: controller.setContractProxy(keccak256(bytes(name)), addr). + function _registerContract(string memory name, address addr) internal { + vm.prank(governor); + controller.setContractProxy(keccak256(bytes(name)), addr); + } +} diff --git a/packages/graph-token/test/unit/token/GraphToken.t.sol b/packages/graph-token/test/unit/token/GraphToken.t.sol new file mode 100644 index 000000000..51ebdef02 --- /dev/null +++ b/packages/graph-token/test/unit/token/GraphToken.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { GraphTokenSharedTest } from "./GraphTokenShared.t.sol"; +import { IGraphTokenLike } from "../interfaces/IGraphTokenLike.sol"; + +contract GraphTokenTest is GraphTokenSharedTest { + string internal constant GRAPH_TOKEN = string.concat(PKG_ARTIFACTS, "token/GraphToken.sol/GraphToken.json"); + + IGraphTokenLike internal grt; + + function setUp() public { + vm.warp(1_700_000_000); + _deployProtocolBase(); + vm.prank(governor); + grt = IGraphTokenLike(_deployArtifact(GRAPH_TOKEN, abi.encode(10_000_000_000 ether))); + _setUpShared(); + } + + function _token() internal view override returns (IGraphTokenLike) { + return grt; + } + + function _salt() internal pure override returns (bytes32) { + return L1_SALT; + } +} diff --git a/packages/graph-token/test/unit/token/GraphTokenShared.t.sol b/packages/graph-token/test/unit/token/GraphTokenShared.t.sol new file mode 100644 index 000000000..9b9bd1bdc --- /dev/null +++ b/packages/graph-token/test/unit/token/GraphTokenShared.t.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { ProtocolFixture } from "../shared/ProtocolFixture.t.sol"; +import { IGraphTokenLike } from "../interfaces/IGraphTokenLike.sol"; + +/// Cases shared by GraphToken (L1) and L2GraphToken; only the EIP-712 salt differs +abstract contract GraphTokenSharedTest is ProtocolFixture { + bytes32 internal constant L1_SALT = 0x51f3d585afe6dfeb2af01bba0889a36c1db03beec88c6a4d0c53817069026afa; + bytes32 internal constant L2_SALT = 0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591; + + address internal me; + uint256 internal mePk; + address internal other; + uint256 internal otherPk; + + function _token() internal view virtual returns (IGraphTokenLike); + + function _salt() internal pure virtual returns (bytes32); + + /// Call after the token is deployed + function _setUpShared() internal { + (me, mePk) = makeAddrAndKey("me"); + (other, otherPk) = makeAddrAndKey("other"); + + vm.prank(governor); + _token().mint(me, 10_000 ether); + } + + function _domainSeparator() internal view returns (bytes32) { + return + keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ), + keccak256("Graph Token"), + keccak256("0"), + block.chainid, + address(_token()), + _salt() + ) + ); + } + + function _permitDigest( + address owner, + address spender, + uint256 value, + uint256 nonce, + uint256 deadline + ) internal view returns (bytes32) { + return + keccak256( + abi.encodePacked( + "\x19\x01", + _domainSeparator(), + keccak256( + abi.encode( + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), + owner, + spender, + value, + nonce, + deadline + ) + ) + ) + ); + } + + struct Permit { + address owner; + address spender; + uint256 value; + uint256 nonce; + uint256 deadline; + } + + function _permit(uint256 value, uint256 deadline) internal view returns (Permit memory) { + return Permit(me, other, value, _token().nonces(me), deadline); + } + + /// Signs `p` with `pk` and submits it + function _submitPermit(Permit memory p, uint256 pk) internal { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _permitDigest(p.owner, p.spender, p.value, p.nonce, p.deadline)); + _token().permit(p.owner, p.spender, p.value, p.deadline, v, r, s); + } + + // -- permit -- + + function test_Permit_PermitsTokenAllowance() public { + uint256 tokensToApprove = 1000 ether; + Permit memory p = _permit(tokensToApprove, 0); + + vm.expectEmit(address(_token())); + emit IGraphTokenLike.Approval(p.owner, p.spender, tokensToApprove); + _submitPermit(p, mePk); + + assertEq(_token().allowance(me, other), tokensToApprove); + + vm.prank(other); + _token().transferFrom(me, other, 100 ether); + } + + function test_Permit_PermitsMaxTokenAllowance() public { + Permit memory p = _permit(type(uint256).max, 0); + + vm.expectEmit(address(_token())); + emit IGraphTokenLike.Approval(p.owner, p.spender, type(uint256).max); + _submitPermit(p, mePk); + + assertEq(_token().allowance(me, other), type(uint256).max); + + vm.prank(other); + _token().transferFrom(me, other, 100 ether); + } + + function test_Permit_RevertWhen_TransferExceedsPermit() public { + uint256 tokensToApprove = 1000 ether; + _submitPermit(_permit(tokensToApprove, 0), mePk); + + vm.expectRevert(bytes("ERC20: transfer amount exceeds allowance")); + vm.prank(other); + _token().transferFrom(me, other, 1001 ether); + + vm.prank(other); + _token().transferFrom(me, other, tokensToApprove); + } + + function test_Permit_RevertWhen_NonceReused() public { + Permit memory p = _permit(type(uint256).max, 0); + _submitPermit(p, mePk); + + vm.expectRevert(bytes("GRT: invalid permit")); + this.submitPermit(p, mePk); + } + + function test_Permit_RevertWhen_Expired() public { + vm.warp(1_700_000_000); + Permit memory p = _permit(type(uint256).max, block.timestamp - 1); + + vm.expectRevert(bytes("GRT: expired permit")); + this.submitPermit(p, mePk); + } + + function test_Permit_RevertWhen_HolderDoesNotMatch() public { + Permit memory p = _permit(type(uint256).max, 0); + + vm.expectRevert(bytes("GRT: invalid permit")); + this.submitPermit(p, otherPk); + } + + function test_Permit_DeniesTransferFromAfterPermitDenied() public { + _submitPermit(_permit(type(uint256).max, 0), mePk); + _submitPermit(_permit(0, 0), mePk); + + assertEq(_token().allowance(me, other), 0); + + vm.expectRevert(bytes("ERC20: transfer amount exceeds allowance")); + vm.prank(other); + _token().transferFrom(me, other, 100 ether); + } + + /// External wrapper so vm.expectRevert targets the permit call and not the signing cheatcode + function submitPermit(Permit memory p, uint256 pk) external { + _submitPermit(p, pk); + } + + // -- mint -- + + function test_Mint_RevertWhen_NotMinter() public { + vm.expectRevert(bytes("Only minter can call")); + vm.prank(me); + _token().mint(me, 100 ether); + } + + function test_AddMinter_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + _token().addMinter(me); + } + + function test_AddMinter_AddsMinter() public { + assertFalse(_token().isMinter(me)); + + vm.expectEmit(address(_token())); + emit IGraphTokenLike.MinterAdded(me); + vm.prank(governor); + _token().addMinter(me); + + assertTrue(_token().isMinter(me)); + } + + function test_Mint_MintsWhenMinter() public { + vm.prank(governor); + _token().addMinter(me); + uint256 before = _token().balanceOf(me); + uint256 tokensToMint = 100 ether; + + vm.expectEmit(address(_token())); + emit IGraphTokenLike.Transfer(address(0), me, tokensToMint); + vm.prank(me); + _token().mint(me, tokensToMint); + + assertEq(_token().balanceOf(me), before + tokensToMint); + } + + function test_Mint_MintsWhenGovernor() public { + uint256 before = _token().balanceOf(me); + + vm.prank(governor); + _token().mint(me, 100 ether); + + assertEq(_token().balanceOf(me), before + 100 ether); + } + + function test_RemoveMinter_RevertWhen_NotGovernor() public { + vm.prank(governor); + _token().addMinter(me); + + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + _token().removeMinter(me); + } + + function test_RemoveMinter_RemovesMinter() public { + vm.prank(governor); + _token().addMinter(me); + + vm.expectEmit(address(_token())); + emit IGraphTokenLike.MinterRemoved(me); + vm.prank(governor); + _token().removeMinter(me); + + assertFalse(_token().isMinter(me)); + } + + function test_RenounceMinter_RenouncesMinter() public { + vm.prank(governor); + _token().addMinter(me); + + vm.expectEmit(address(_token())); + emit IGraphTokenLike.MinterRemoved(me); + vm.prank(me); + _token().renounceMinter(); + + assertFalse(_token().isMinter(me)); + } +} diff --git a/packages/graph-token/test/unit/token/L2GraphToken.t.sol b/packages/graph-token/test/unit/token/L2GraphToken.t.sol new file mode 100644 index 000000000..071cfbaa4 --- /dev/null +++ b/packages/graph-token/test/unit/token/L2GraphToken.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { GraphTokenSharedTest } from "./GraphTokenShared.t.sol"; +import { IGraphTokenLike } from "../interfaces/IGraphTokenLike.sol"; +import { IL2GraphTokenLike } from "../interfaces/IL2GraphTokenLike.sol"; + +contract L2GraphTokenTest is GraphTokenSharedTest { + string internal constant L2_GRAPH_TOKEN = + string.concat(PKG_ARTIFACTS, "l2/token/L2GraphToken.sol/L2GraphToken.json"); + + IL2GraphTokenLike internal grt; + address internal mockL2Gateway = makeAddr("mockL2Gateway"); + address internal mockL1GRT = makeAddr("mockL1GRT"); + address internal user = makeAddr("user"); + + function setUp() public { + vm.warp(1_700_000_000); + _deployProtocolBase(); + address impl = _deployArtifact(L2_GRAPH_TOKEN, ""); + grt = IL2GraphTokenLike(_deployBehindProxy(impl, abi.encodeCall(IL2GraphTokenLike.initialize, (governor)))); + _setUpShared(); + } + + function _token() internal view override returns (IGraphTokenLike) { + return grt; + } + + function _salt() internal pure override returns (bytes32) { + return L2_SALT; + } + + function _configureBridge() internal { + vm.startPrank(governor); + grt.setL1Address(mockL1GRT); + grt.setGateway(mockL2Gateway); + vm.stopPrank(); + } + + // -- setGateway -- + + function test_SetGateway_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(mockL2Gateway); + grt.setGateway(mockL2Gateway); + } + + function test_SetGateway_SetsGateway() public { + vm.expectEmit(address(grt)); + emit IL2GraphTokenLike.GatewaySet(mockL2Gateway); + vm.prank(governor); + grt.setGateway(mockL2Gateway); + + assertEq(grt.gateway(), mockL2Gateway); + } + + // -- setL1Address -- + + function test_SetL1Address_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(mockL2Gateway); + grt.setL1Address(mockL1GRT); + } + + function test_SetL1Address_SetsL1Address() public { + vm.expectEmit(address(grt)); + emit IL2GraphTokenLike.L1AddressSet(mockL1GRT); + vm.prank(governor); + grt.setL1Address(mockL1GRT); + + assertEq(grt.l1Address(), mockL1GRT); + } + + // -- bridgeMint -- + + function test_BridgeMint_RevertWhen_NotGateway() public { + _configureBridge(); + + vm.expectRevert(bytes("NOT_GATEWAY")); + vm.prank(governor); + grt.bridgeMint(user, 100 ether); + } + + function test_BridgeMint_MintsToAccount() public { + _configureBridge(); + + vm.expectEmit(address(grt)); + emit IL2GraphTokenLike.BridgeMinted(user, 100 ether); + vm.prank(mockL2Gateway); + grt.bridgeMint(user, 100 ether); + + assertEq(grt.balanceOf(user), 100 ether); + } + + // -- bridgeBurn -- + + function test_BridgeBurn_RevertWhen_NotGateway() public { + _configureBridge(); + + vm.expectRevert(bytes("NOT_GATEWAY")); + vm.prank(governor); + grt.bridgeBurn(user, 100 ether); + } + + function test_BridgeBurn_RevertWhen_NotApproved() public { + _configureBridge(); + vm.prank(mockL2Gateway); + grt.bridgeMint(user, 100 ether); + + vm.expectRevert(bytes("ERC20: burn amount exceeds allowance")); + vm.prank(mockL2Gateway); + grt.bridgeBurn(user, 20 ether); + } + + function test_BridgeBurn_RevertWhen_InsufficientBalance() public { + _configureBridge(); + vm.prank(mockL2Gateway); + grt.bridgeMint(user, 10 ether); + vm.prank(user); + grt.approve(mockL2Gateway, 20 ether); + + vm.expectRevert(bytes("ERC20: burn amount exceeds balance")); + vm.prank(mockL2Gateway); + grt.bridgeBurn(user, 20 ether); + } + + function test_BridgeBurn_BurnsWhenApproved() public { + _configureBridge(); + vm.prank(mockL2Gateway); + grt.bridgeMint(user, 100 ether); + vm.prank(user); + grt.approve(mockL2Gateway, 20 ether); + + vm.expectEmit(address(grt)); + emit IL2GraphTokenLike.BridgeBurned(user, 20 ether); + vm.prank(mockL2Gateway); + grt.bridgeBurn(user, 20 ether); + + assertEq(grt.balanceOf(user), 80 ether); + } + + function test_BridgeMintingAndBurning_RevertWhen_GatewayMintsAsRegularMinter() public { + _configureBridge(); + + vm.expectRevert(bytes("Only minter can call")); + vm.prank(mockL2Gateway); + grt.mint(user, 100 ether); + } +} diff --git a/packages/graph-token/test/unit/utils/TokenUtils.t.sol b/packages/graph-token/test/unit/utils/TokenUtils.t.sol new file mode 100644 index 000000000..9a7eb25de --- /dev/null +++ b/packages/graph-token/test/unit/utils/TokenUtils.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { ProtocolFixture } from "../shared/ProtocolFixture.t.sol"; +import { IGraphTokenLike } from "../interfaces/IGraphTokenLike.sol"; +import { ITokenUtilsMock } from "../interfaces/ITokenUtilsMock.sol"; + +contract TokenUtilsTest is ProtocolFixture { + string internal constant GRAPH_TOKEN = string.concat(PKG_ARTIFACTS, "token/GraphToken.sol/GraphToken.json"); + string internal constant TOKEN_UTILS_MOCK = + string.concat(PKG_ARTIFACTS, "tests/TokenUtilsMock.sol/TokenUtilsMock.json"); + + IGraphTokenLike internal grt; + ITokenUtilsMock internal utils; + address internal holder = makeAddr("holder"); + + function setUp() public { + vm.warp(1_700_000_000); + _deployProtocolBase(); + + vm.prank(governor); + grt = IGraphTokenLike(_deployArtifact(GRAPH_TOKEN, abi.encode(10_000_000_000 ether))); + utils = ITokenUtilsMock(_deployArtifact(TOKEN_UTILS_MOCK, "")); + + vm.prank(governor); + grt.mint(holder, 1000 ether); + } + + function test_TokenUtils_PullTokensMovesTokensToCaller() public { + vm.prank(holder); + grt.approve(address(utils), 400 ether); + + utils.pullTokens(grt, holder, 400 ether); + + assertEq(grt.balanceOf(holder), 600 ether); + assertEq(grt.balanceOf(address(utils)), 400 ether); + } + + function test_TokenUtils_PullTokensZeroDoesNothing() public { + // No approval: a nonzero pull would revert + utils.pullTokens(grt, holder, 0); + + assertEq(grt.balanceOf(holder), 1000 ether); + assertEq(grt.balanceOf(address(utils)), 0); + } + + function test_TokenUtils_PushTokensMovesTokensFromCaller() public { + vm.prank(holder); + grt.transfer(address(utils), 500 ether); + + utils.pushTokens(grt, holder, 300 ether); + + assertEq(grt.balanceOf(holder), 800 ether); + assertEq(grt.balanceOf(address(utils)), 200 ether); + } + + function test_TokenUtils_PushTokensZeroDoesNothing() public { + // Mock holds nothing: a nonzero push would revert + utils.pushTokens(grt, holder, 0); + + assertEq(grt.balanceOf(holder), 1000 ether); + assertEq(grt.balanceOf(address(utils)), 0); + } + + function test_TokenUtils_BurnTokensLowersTotalSupply() public { + vm.prank(holder); + grt.transfer(address(utils), 500 ether); + uint256 supplyBefore = grt.totalSupply(); + + utils.burnTokens(grt, 200 ether); + + assertEq(grt.totalSupply(), supplyBefore - 200 ether); + assertEq(grt.balanceOf(address(utils)), 300 ether); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5bf6f5c1..8e0e89ae6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -723,6 +723,18 @@ importers: specifier: 'catalog:' version: https://github.com/foundry-rs/forge-std/tarball/v1.14.0 + packages/graph-token: + devDependencies: + '@graphprotocol/contracts': + specifier: workspace:^ + version: link:../contracts + '@graphprotocol/interfaces': + specifier: workspace:^ + version: link:../interfaces + forge-std: + specifier: 'catalog:' + version: https://github.com/foundry-rs/forge-std/tarball/v1.14.0 + packages/hardhat-graph-protocol: dependencies: '@graphprotocol/toolshed': diff --git a/scripts/contracts-migration/test-mapping/t2a.md b/scripts/contracts-migration/test-mapping/t2a.md new file mode 100644 index 000000000..4a60b37e8 --- /dev/null +++ b/scripts/contracts-migration/test-mapping/t2a.md @@ -0,0 +1,42 @@ +# T2a test mapping: tokens, BridgeEscrow and TokenUtils + +New tests are in `packages/graph-token`. The cases of `lib/graphTokenTests.ts` run once per token in the old suite (`graphToken.test.ts` and `l2GraphToken.test.ts` both bind it); they are written once in `GraphTokenSharedTest` and inherited by `GraphTokenTest` and `L2GraphTokenTest`. `graphToken.test.ts` has no cases of its own. + +| Old file | Old test (describe › it) | New test (file::function) | Note | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | permit › should permit max token allowance (1000 GRT) | test/unit/token/GraphTokenShared.t.sol::test_Permit_PermitsTokenAllowance | Runs for GraphToken and L2GraphToken; identically titled in the old file | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | permit › should permit max token allowance (MaxUint256) | test/unit/token/GraphTokenShared.t.sol::test_Permit_PermitsMaxTokenAllowance | Runs for GraphToken and L2GraphToken; identically titled in the old file | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | permit › reject to transfer more tokens than approved by permit | test/unit/token/GraphTokenShared.t.sol::test_Permit_RevertWhen_TransferExceedsPermit | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | permit › reject use two permits with same nonce | test/unit/token/GraphTokenShared.t.sol::test_Permit_RevertWhen_NonceReused | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | permit › reject use expired permit | test/unit/token/GraphTokenShared.t.sol::test_Permit_RevertWhen_Expired | Runs for GraphToken and L2GraphToken; warps to 1_700_000_000, deadline = block.timestamp - 1 | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | permit › reject permit if holder address does not match | test/unit/token/GraphTokenShared.t.sol::test_Permit_RevertWhen_HolderDoesNotMatch | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | permit › should deny transfer from if permit was denied | test/unit/token/GraphTokenShared.t.sol::test_Permit_DeniesTransferFromAfterPermitDenied | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | mint › mint › reject mint if not minter | test/unit/token/GraphTokenShared.t.sol::test_Mint_RevertWhen_NotMinter | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | mint › addMinter › reject add a new minter if not allowed | test/unit/token/GraphTokenShared.t.sol::test_AddMinter_RevertWhen_NotGovernor | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | mint › addMinter › should add a new minter | test/unit/token/GraphTokenShared.t.sol::test_AddMinter_AddsMinter | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | mint › when is minter › mint › should mint | test/unit/token/GraphTokenShared.t.sol::test_Mint_MintsWhenMinter | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | mint › when is minter › mint › should mint if governor | test/unit/token/GraphTokenShared.t.sol::test_Mint_MintsWhenGovernor | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | mint › when is minter › removeMinter › reject remove a minter if not allowed | test/unit/token/GraphTokenShared.t.sol::test_RemoveMinter_RevertWhen_NotGovernor | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | mint › when is minter › removeMinter › should remove a minter | test/unit/token/GraphTokenShared.t.sol::test_RemoveMinter_RemovesMinter | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/lib/graphTokenTests.ts | mint › when is minter › renounceMinter › should renounce to be a minter | test/unit/token/GraphTokenShared.t.sol::test_RenounceMinter_RenouncesMinter | Runs for GraphToken and L2GraphToken | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › setGateway › cannot be called by someone who is not the governor | test/unit/token/L2GraphToken.t.sol::test_SetGateway_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › setGateway › sets the L2 Gateway address when called by the governor | test/unit/token/L2GraphToken.t.sol::test_SetGateway_SetsGateway | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › setL1Address › cannot be called by someone who is not the governor | test/unit/token/L2GraphToken.t.sol::test_SetL1Address_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › setL1Address › sets the L1 GRT address when called by the governor | test/unit/token/L2GraphToken.t.sol::test_SetL1Address_SetsL1Address | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › bridge minting and burning › bridgeMint › cannot be called by someone who is not the gateway | test/unit/token/L2GraphToken.t.sol::test_BridgeMint_RevertWhen_NotGateway | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › bridge minting and burning › bridgeMint › mints GRT into a destination account | test/unit/token/L2GraphToken.t.sol::test_BridgeMint_MintsToAccount | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › bridge minting and burning › bridgeBurn › cannot be called by someone who is not the gateway | test/unit/token/L2GraphToken.t.sol::test_BridgeBurn_RevertWhen_NotGateway | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › bridge minting and burning › bridgeBurn › requires approval for burning | test/unit/token/L2GraphToken.t.sol::test_BridgeBurn_RevertWhen_NotApproved | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › bridge minting and burning › bridgeBurn › fails if the user does not have enough funds | test/unit/token/L2GraphToken.t.sol::test_BridgeBurn_RevertWhen_InsufficientBalance | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › bridge minting and burning › bridgeBurn › burns GRT from an account when approved | test/unit/token/L2GraphToken.t.sol::test_BridgeBurn_BurnsWhenApproved | | +| packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts | Extended L2 behavior › bridge minting and burning › does not allow the bridge to mint as a regular minter | test/unit/token/L2GraphToken.t.sol::test_BridgeMintingAndBurning_RevertWhen_GatewayMintsAsRegularMinter | | +| packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts | BridgeEscrow › approveAll › cannot be called by someone other than the governor | test/unit/gateway/BridgeEscrow.t.sol::test_ApproveAll_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts | BridgeEscrow › approveAll › allows a spender to transfer GRT held by the contract | test/unit/gateway/BridgeEscrow.t.sol::test_ApproveAll_AllowsSpenderToTransfer | | +| packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts | BridgeEscrow › revokeAll › cannot be called by someone other than the governor | test/unit/gateway/BridgeEscrow.t.sol::test_RevokeAll_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts | BridgeEscrow › revokeAll › revokes a spender's permission to transfer GRT held by the contract | test/unit/gateway/BridgeEscrow.t.sol::test_RevokeAll_RevokesSpender | | + +## Coverage + +MISSING initialize: `L2GraphToken.initialize` and `BridgeEscrow.initialize` are called by the fixture through `abi.encodeCall` in the proxy init data (`acceptProxyAndCall`), not by a `.initialize(` call. Every test runs it, and it must succeed for the proxy to work. The other listed external/public functions are called directly. + +Additions with no old test: `test/unit/utils/TokenUtils.t.sol` (`TokenUtils` was only exercised through GNS and L2Curation, which are not in graph-token), through `TokenUtilsMock` in `packages/contracts/contracts/tests/`. From 325aa9e6f009fa36223b01f51dabd17e96f6a369 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Mon, 26 Oct 2026 10:27:07 -0300 Subject: [PATCH 3/3] test(graph-token): port gateway tests to Foundry --- .../unit/gateway/l1GraphTokenGateway.test.ts | 788 ------------------ .../tests/unit/l2/l2ArbitrumMessengerMock.ts | 49 -- .../tests/unit/l2/l2GraphTokenGateway.test.ts | 395 --------- .../gateway/L1GraphTokenGatewayConfig.t.sol | 245 ++++++ .../gateway/L1GraphTokenGatewayShared.t.sol | 102 +++ .../L1GraphTokenGatewayTransfers.t.sol | 465 +++++++++++ .../gateway/L2GraphTokenGatewayConfig.t.sol | 159 ++++ .../gateway/L2GraphTokenGatewayShared.t.sol | 80 ++ .../L2GraphTokenGatewayTransfers.t.sol | 159 ++++ .../unit/interfaces/IArbitrumMocksLike.sol | 35 + .../interfaces/IL1GraphTokenGatewayLike.sol | 98 +++ .../interfaces/IL2GraphTokenGatewayLike.sol | 59 ++ .../test/unit/mocks/NoCallhookReceiver.sol | 5 + .../contracts-migration/test-mapping/t2b.md | 86 ++ 14 files changed, 1493 insertions(+), 1232 deletions(-) delete mode 100644 packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts delete mode 100644 packages/contracts-test/tests/unit/l2/l2ArbitrumMessengerMock.ts delete mode 100644 packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts create mode 100644 packages/graph-token/test/unit/gateway/L1GraphTokenGatewayConfig.t.sol create mode 100644 packages/graph-token/test/unit/gateway/L1GraphTokenGatewayShared.t.sol create mode 100644 packages/graph-token/test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol create mode 100644 packages/graph-token/test/unit/gateway/L2GraphTokenGatewayConfig.t.sol create mode 100644 packages/graph-token/test/unit/gateway/L2GraphTokenGatewayShared.t.sol create mode 100644 packages/graph-token/test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol create mode 100644 packages/graph-token/test/unit/interfaces/IArbitrumMocksLike.sol create mode 100644 packages/graph-token/test/unit/interfaces/IL1GraphTokenGatewayLike.sol create mode 100644 packages/graph-token/test/unit/interfaces/IL2GraphTokenGatewayLike.sol create mode 100644 packages/graph-token/test/unit/mocks/NoCallhookReceiver.sol create mode 100644 scripts/contracts-migration/test-mapping/t2b.md diff --git a/packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts b/packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts deleted file mode 100644 index 7c87f3b9d..000000000 --- a/packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts +++ /dev/null @@ -1,788 +0,0 @@ -import { GraphToken } from '@graphprotocol/contracts' -import { BridgeMock } from '@graphprotocol/contracts' -import { InboxMock } from '@graphprotocol/contracts' -import { OutboxMock } from '@graphprotocol/contracts' -import { L1GraphTokenGateway } from '@graphprotocol/contracts' -import { L2GraphToken, L2GraphTokenGateway } from '@graphprotocol/contracts' -import { BridgeEscrow } from '@graphprotocol/contracts' -import { applyL1ToL2Alias, GraphNetworkContracts, helpers, toBN, toGRT } from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { constants, Signer, utils, Wallet } from 'ethers' -import hre from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' - -const { AddressZero } = constants - -describe('L1GraphTokenGateway', () => { - const graph = hre.graph() - let governor: SignerWithAddress - let tokenSender: SignerWithAddress - let l2Receiver: SignerWithAddress - let pauseGuardian: SignerWithAddress - - let fixture: NetworkFixture - let grt: GraphToken - let l1GraphTokenGateway: L1GraphTokenGateway - let bridgeEscrow: BridgeEscrow - - let bridgeMock: BridgeMock - let inboxMock: InboxMock - let outboxMock: OutboxMock - let routerMock: Wallet - let l2GRTMock: L2GraphToken - let l2GRTGatewayMock: L2GraphTokenGateway - - let fixtureContracts: GraphNetworkContracts - let l2MockContracts: GraphNetworkContracts - - const senderTokens = toGRT('1000') - const maxGas = toBN('1000000') - const maxSubmissionCost = toBN('7') - const gasPriceBid = toBN('2') - const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) - const emptyCallHookData = '0x' - const defaultData = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionCost, emptyCallHookData]) - const defaultDataNoSubmissionCost = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [toBN(0), emptyCallHookData]) - const notEmptyCallHookData = '0x12' - const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( - ['uint256', 'bytes'], - [maxSubmissionCost, notEmptyCallHookData], - ) - - before(async function () { - ;[tokenSender, l2Receiver] = await graph.getTestAccounts() - ;({ governor, pauseGuardian } = await graph.getNamedAccounts()) - - fixture = new NetworkFixture(graph.provider) - - // Deploy L1 - fixtureContracts = await fixture.load(governor) - grt = fixtureContracts.GraphToken as GraphToken - l1GraphTokenGateway = fixtureContracts.L1GraphTokenGateway as L1GraphTokenGateway - bridgeEscrow = fixtureContracts.BridgeEscrow as BridgeEscrow - - // Deploy L1 arbitrum bridge - // @ts-expect-error sdk deprecation - ;({ bridgeMock, inboxMock, outboxMock, routerMock } = await fixture.loadL1ArbitrumBridge(governor)) - - // Deploy L2 mock - l2MockContracts = await fixture.loadMock(true) - l2GRTMock = l2MockContracts.L2GraphToken as L2GraphToken - l2GRTGatewayMock = l2MockContracts.L2GraphTokenGateway as L2GraphTokenGateway - - // Give some funds to the token sender/router mock - await grt.connect(governor).mint(tokenSender.address, senderTokens) - await helpers.setBalance(routerMock.address, utils.parseEther('1')) - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - context('> immediately after deploy', function () { - describe('calculateL2TokenAddress', function () { - it('should return address zero as it was not set', async function () { - expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(AddressZero) - }) - }) - - describe('outboundTransfer', function () { - it('reverts because it is paused', async function () { - const tx = l1GraphTokenGateway - .connect(tokenSender) - .outboundTransfer(grt.address, l2Receiver.address, toGRT('10'), maxGas, gasPriceBid, defaultData, { - value: defaultEthValue, - }) - await expect(tx).revertedWith('Paused (contract)') - }) - }) - - describe('finalizeInboundTransfer', function () { - it('revert because it is paused', async function () { - const tx = l1GraphTokenGateway - .connect(tokenSender) - .finalizeInboundTransfer(grt.address, l2Receiver.address, tokenSender.address, toGRT('10'), defaultData) - await expect(tx).revertedWith('Paused (contract)') - }) - }) - - describe('setArbitrumAddresses', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l1GraphTokenGateway.connect(tokenSender).setArbitrumAddresses(inboxMock.address, routerMock.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('rejects setting an EOA as router or inbox', async function () { - let tx = l1GraphTokenGateway.connect(governor).setArbitrumAddresses(tokenSender.address, routerMock.address) - await expect(tx).revertedWith('INBOX_MUST_BE_CONTRACT') - tx = l1GraphTokenGateway.connect(governor).setArbitrumAddresses(inboxMock.address, tokenSender.address) - await expect(tx).revertedWith('ROUTER_MUST_BE_CONTRACT') - }) - it('sets inbox and router address', async function () { - const tx = l1GraphTokenGateway.connect(governor).setArbitrumAddresses(inboxMock.address, routerMock.address) - await expect(tx) - .emit(l1GraphTokenGateway, 'ArbitrumAddressesSet') - .withArgs(inboxMock.address, routerMock.address) - expect(await l1GraphTokenGateway.l1Router()).eq(routerMock.address) - expect(await l1GraphTokenGateway.inbox()).eq(inboxMock.address) - }) - }) - - describe('setL2TokenAddress', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l1GraphTokenGateway.connect(tokenSender).setL2TokenAddress(l2GRTMock.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('sets l2GRT', async function () { - const tx = l1GraphTokenGateway.connect(governor).setL2TokenAddress(l2GRTMock.address) - await expect(tx).emit(l1GraphTokenGateway, 'L2TokenAddressSet').withArgs(l2GRTMock.address) - expect(await l1GraphTokenGateway.l2GRT()).eq(l2GRTMock.address) - }) - }) - - describe('setL2CounterpartAddress', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l1GraphTokenGateway.connect(tokenSender).setL2CounterpartAddress(l2GRTGatewayMock.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('sets l2Counterpart which can be queried with counterpartGateway()', async function () { - const tx = l1GraphTokenGateway.connect(governor).setL2CounterpartAddress(l2GRTGatewayMock.address) - await expect(tx).emit(l1GraphTokenGateway, 'L2CounterpartAddressSet').withArgs(l2GRTGatewayMock.address) - expect(await l1GraphTokenGateway.l2Counterpart()).eq(l2GRTGatewayMock.address) - expect(await l1GraphTokenGateway.counterpartGateway()).eq(l2GRTGatewayMock.address) - }) - }) - describe('setEscrowAddress', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l1GraphTokenGateway.connect(tokenSender).setEscrowAddress(bridgeEscrow.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('sets escrow', async function () { - const tx = l1GraphTokenGateway.connect(governor).setEscrowAddress(bridgeEscrow.address) - await expect(tx).emit(l1GraphTokenGateway, 'EscrowAddressSet').withArgs(bridgeEscrow.address) - expect(await l1GraphTokenGateway.escrow()).eq(bridgeEscrow.address) - }) - }) - describe('addToCallhookAllowlist', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l1GraphTokenGateway - .connect(tokenSender) - .addToCallhookAllowlist(fixtureContracts.RewardsManager.address) - await expect(tx).revertedWith('Only Controller governor') - expect(await l1GraphTokenGateway.callhookAllowlist(fixtureContracts.RewardsManager.address)).eq(false) - }) - it('rejects adding an EOA to the callhook allowlist', async function () { - const tx = l1GraphTokenGateway.connect(governor).addToCallhookAllowlist(tokenSender.address) - await expect(tx).revertedWith('MUST_BE_CONTRACT') - }) - it('adds an address to the callhook allowlist', async function () { - const tx = l1GraphTokenGateway.connect(governor).addToCallhookAllowlist(fixtureContracts.RewardsManager.address) - await expect(tx) - .emit(l1GraphTokenGateway, 'AddedToCallhookAllowlist') - .withArgs(fixtureContracts.RewardsManager.address) - expect(await l1GraphTokenGateway.callhookAllowlist(fixtureContracts.RewardsManager.address)).eq(true) - }) - }) - describe('removeFromCallhookAllowlist', function () { - it('is not callable by addreses that are not the governor', async function () { - await l1GraphTokenGateway.connect(governor).addToCallhookAllowlist(fixtureContracts.RewardsManager.address) - const tx = l1GraphTokenGateway - .connect(tokenSender) - .removeFromCallhookAllowlist(fixtureContracts.RewardsManager.address) - await expect(tx).revertedWith('Only Controller governor') - expect(await l1GraphTokenGateway.callhookAllowlist(fixtureContracts.RewardsManager.address)).eq(true) - }) - it('removes an address from the callhook allowlist', async function () { - await l1GraphTokenGateway.connect(governor).addToCallhookAllowlist(fixtureContracts.RewardsManager.address) - const tx = l1GraphTokenGateway - .connect(governor) - .removeFromCallhookAllowlist(fixtureContracts.RewardsManager.address) - await expect(tx) - .emit(l1GraphTokenGateway, 'RemovedFromCallhookAllowlist') - .withArgs(fixtureContracts.RewardsManager.address) - expect(await l1GraphTokenGateway.callhookAllowlist(fixtureContracts.RewardsManager.address)).eq(false) - }) - }) - describe('Pausable behavior', () => { - it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { - let tx = l1GraphTokenGateway.connect(tokenSender).setPaused(false) - await expect(tx).revertedWith('Only Governor or Guardian') - tx = l1GraphTokenGateway.connect(tokenSender).setPaused(true) - await expect(tx).revertedWith('Only Governor or Guardian') - }) - it('cannot be unpaused if some state variables are not set', async function () { - let tx = l1GraphTokenGateway.connect(governor).setPaused(false) - await expect(tx).revertedWith('INBOX_NOT_SET') - await l1GraphTokenGateway.connect(governor).setArbitrumAddresses(inboxMock.address, routerMock.address) - tx = l1GraphTokenGateway.connect(governor).setPaused(false) - await expect(tx).revertedWith('L2_COUNTERPART_NOT_SET') - await l1GraphTokenGateway.connect(governor).setL2CounterpartAddress(l2GRTGatewayMock.address) - tx = l1GraphTokenGateway.connect(governor).setPaused(false) - await expect(tx).revertedWith('ESCROW_NOT_SET') - }) - it('can be paused and unpaused by the governor', async function () { - await fixture.configureL1Bridge(governor, fixtureContracts, l2MockContracts) - let tx = l1GraphTokenGateway.connect(governor).setPaused(true) - await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) - expect(await l1GraphTokenGateway.paused()).eq(true) - tx = l1GraphTokenGateway.connect(governor).setPaused(false) - await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) - expect(await l1GraphTokenGateway.paused()).eq(false) - }) - describe('setPauseGuardian', function () { - it('cannot be called by someone other than governor', async function () { - const tx = l1GraphTokenGateway.connect(tokenSender).setPauseGuardian(pauseGuardian.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('sets a new pause guardian', async function () { - const currentPauseGuardian = await l1GraphTokenGateway.pauseGuardian() - const tx = l1GraphTokenGateway.connect(governor).setPauseGuardian(pauseGuardian.address) - await expect(tx) - .emit(l1GraphTokenGateway, 'NewPauseGuardian') - .withArgs(currentPauseGuardian, pauseGuardian.address) - }) - it('allows a pause guardian to pause and unpause', async function () { - await fixture.configureL1Bridge(governor, fixtureContracts, l2MockContracts) - await l1GraphTokenGateway.connect(governor).setPauseGuardian(pauseGuardian.address) - let tx = l1GraphTokenGateway.connect(pauseGuardian).setPaused(true) - await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) - expect(await l1GraphTokenGateway.paused()).eq(true) - tx = l1GraphTokenGateway.connect(pauseGuardian).setPaused(false) - await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) - expect(await l1GraphTokenGateway.paused()).eq(false) - }) - }) - }) - }) - - context('> after configuring and unpausing', function () { - const createMsgData = function (callHookData: string) { - const selector = l1GraphTokenGateway.interface.getSighash('finalizeInboundTransfer') - const params = utils.defaultAbiCoder.encode( - ['address', 'address', 'address', 'uint256', 'bytes'], - [grt.address, tokenSender.address, l2Receiver.address, toGRT('10'), callHookData], - ) - const outboundData = utils.hexlify(utils.concat([selector, params])) - - const msgData = utils.solidityPack( - ['uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'bytes'], - [ - toBN(l2GRTGatewayMock.address), - toBN('0'), - defaultEthValue, - maxSubmissionCost, - applyL1ToL2Alias(tokenSender.address), - applyL1ToL2Alias(tokenSender.address), - maxGas, - gasPriceBid, - utils.hexDataLength(outboundData), - outboundData, - ], - ) - return msgData - } - const createInboxAccsEntry = function (msgDataHash: string) { - // The real bridge would emit the InboxAccs entry that came before this one, but our mock - // emits this, making it easier for us to validate here that all the parameters we sent are correct - const expectedInboxAccsEntry = utils.keccak256( - utils.solidityPack( - ['address', 'uint8', 'address', 'bytes32'], - [inboxMock.address, 9, l1GraphTokenGateway.address, msgDataHash], - ), - ) - return expectedInboxAccsEntry - } - const testValidOutboundTransfer = async function (signer: Signer, data: string, callHookData: string) { - const tx = l1GraphTokenGateway - .connect(signer) - .outboundTransfer(grt.address, l2Receiver.address, toGRT('10'), maxGas, gasPriceBid, data, { - value: defaultEthValue, - }) - // Our bridge mock returns an incrementing seqNum starting at 1 - const expectedSeqNum = 1 - await expect(tx) - .emit(l1GraphTokenGateway, 'DepositInitiated') - .withArgs(grt.address, tokenSender.address, l2Receiver.address, expectedSeqNum, toGRT('10')) - - const msgData = createMsgData(callHookData) - const msgDataHash = utils.keccak256(msgData) - const expectedInboxAccsEntry = createInboxAccsEntry(msgDataHash) - - await expect(tx).emit(inboxMock, 'InboxMessageDelivered').withArgs(1, msgData) - await expect(tx) - .emit(bridgeMock, 'MessageDelivered') - .withArgs( - expectedSeqNum, - expectedInboxAccsEntry, - inboxMock.address, - 9, - l1GraphTokenGateway.address, - msgDataHash, - ) - const escrowBalance = await grt.balanceOf(bridgeEscrow.address) - const senderBalance = await grt.balanceOf(tokenSender.address) - expect(escrowBalance).eq(toGRT('10')) - expect(senderBalance).eq(toGRT('990')) - } - before(async function () { - await fixture.configureL1Bridge(governor, fixtureContracts, l2MockContracts) - }) - - describe('updateL2MintAllowance', function () { - it('rejects calls that are not from the governor', async function () { - const tx = l1GraphTokenGateway - .connect(pauseGuardian.address) - .updateL2MintAllowance(toGRT('1'), await helpers.latestBlock()) - await expect(tx).revertedWith('Only Controller governor') - }) - it('does not allow using a future or current block number', async function () { - const issuancePerBlock = toGRT('120') - let issuanceUpdatedAtBlock = (await helpers.latestBlock()) + 2 - const tx1 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx1).revertedWith('BLOCK_MUST_BE_PAST') - issuanceUpdatedAtBlock = (await helpers.latestBlock()) + 1 // This will be block.number in our next tx - const tx2 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx2).revertedWith('BLOCK_MUST_BE_PAST') - issuanceUpdatedAtBlock = await helpers.latestBlock() // This will be block.number-1 in our next tx - const tx3 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx3) - .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') - .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) - }) - it('does not allow using a block number lower than or equal to the previous one', async function () { - const issuancePerBlock = toGRT('120') - const issuanceUpdatedAtBlock = await helpers.latestBlock() - const tx1 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx1) - .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') - .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) - const tx2 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx2).revertedWith('BLOCK_MUST_BE_INCREMENTING') - const tx3 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock - 1) - await expect(tx3).revertedWith('BLOCK_MUST_BE_INCREMENTING') - const tx4 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock + 1) - await expect(tx4) - .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') - .withArgs(issuancePerBlock, issuancePerBlock, issuanceUpdatedAtBlock + 1) - }) - it('updates the snapshot and issuance to follow a new linear function, accumulating up to the specified block', async function () { - const issuancePerBlock = toGRT('120') - const issuanceUpdatedAtBlock = (await helpers.latestBlock()) - 2 - const tx1 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx1) - .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') - .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) - // Now the mint allowance should be issuancePerBlock * 3 - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await helpers.latestBlock())).to.eq( - issuancePerBlock.mul(3), - ) - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(0) - expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(issuancePerBlock) - expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq(issuanceUpdatedAtBlock) - - await helpers.mine(10) - - const newIssuancePerBlock = toGRT('200') - const newIssuanceUpdatedAtBlock = (await helpers.latestBlock()) - 1 - - const expectedAccumulatedSnapshot = issuancePerBlock.mul(newIssuanceUpdatedAtBlock - issuanceUpdatedAtBlock) - const tx2 = l1GraphTokenGateway - .connect(governor) - .updateL2MintAllowance(newIssuancePerBlock, newIssuanceUpdatedAtBlock) - await expect(tx2) - .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') - .withArgs(expectedAccumulatedSnapshot, newIssuancePerBlock, newIssuanceUpdatedAtBlock) - - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await helpers.latestBlock())).to.eq( - expectedAccumulatedSnapshot.add(newIssuancePerBlock.mul(2)), - ) - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(expectedAccumulatedSnapshot) - expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(newIssuancePerBlock) - expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq(newIssuanceUpdatedAtBlock) - }) - }) - describe('setL2MintAllowanceParametersManual', function () { - it('rejects calls that are not from the governor', async function () { - const tx = l1GraphTokenGateway - .connect(pauseGuardian.address) - .setL2MintAllowanceParametersManual(toGRT('0'), toGRT('1'), await helpers.latestBlock()) - await expect(tx).revertedWith('Only Controller governor') - }) - it('does not allow using a future or current block number', async function () { - const issuancePerBlock = toGRT('120') - let issuanceUpdatedAtBlock = (await helpers.latestBlock()) + 2 - const tx1 = l1GraphTokenGateway - .connect(governor) - .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx1).revertedWith('BLOCK_MUST_BE_PAST') - issuanceUpdatedAtBlock = (await helpers.latestBlock()) + 1 // This will be block.number in our next tx - const tx2 = l1GraphTokenGateway - .connect(governor) - .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx2).revertedWith('BLOCK_MUST_BE_PAST') - issuanceUpdatedAtBlock = await helpers.latestBlock() // This will be block.number-1 in our next tx - const tx3 = l1GraphTokenGateway - .connect(governor) - .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx3) - .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') - .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) - }) - it('updates the snapshot and issuance to follow a new linear function, manually setting the snapshot value', async function () { - const issuancePerBlock = toGRT('120') - const issuanceUpdatedAtBlock = (await helpers.latestBlock()) - 2 - const snapshotValue = toGRT('10') - const tx1 = l1GraphTokenGateway - .connect(governor) - .setL2MintAllowanceParametersManual(snapshotValue, issuancePerBlock, issuanceUpdatedAtBlock) - await expect(tx1) - .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') - .withArgs(snapshotValue, issuancePerBlock, issuanceUpdatedAtBlock) - // Now the mint allowance should be 10 + issuancePerBlock * 3 - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await helpers.latestBlock())).to.eq( - snapshotValue.add(issuancePerBlock.mul(3)), - ) - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(snapshotValue) - expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(issuancePerBlock) - expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq(issuanceUpdatedAtBlock) - - await helpers.mine(10) - - const newIssuancePerBlock = toGRT('200') - const newIssuanceUpdatedAtBlock = (await helpers.latestBlock()) - 1 - const newSnapshotValue = toGRT('10') - - const tx2 = l1GraphTokenGateway - .connect(governor) - .setL2MintAllowanceParametersManual(newSnapshotValue, newIssuancePerBlock, newIssuanceUpdatedAtBlock) - await expect(tx2) - .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') - .withArgs(newSnapshotValue, newIssuancePerBlock, newIssuanceUpdatedAtBlock) - - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await helpers.latestBlock())).to.eq( - newSnapshotValue.add(newIssuancePerBlock.mul(2)), - ) - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(newSnapshotValue) - expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(newIssuancePerBlock) - expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq(newIssuanceUpdatedAtBlock) - }) - }) - describe('calculateL2TokenAddress', function () { - it('returns the L2 token address', async function () { - expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(l2GRTMock.address) - }) - it('returns the zero address if the input is any other address', async function () { - expect(await l1GraphTokenGateway.calculateL2TokenAddress(tokenSender.address)).eq(AddressZero) - }) - }) - - describe('outboundTransfer', function () { - it('reverts when called with the wrong token address', async function () { - const tx = l1GraphTokenGateway - .connect(tokenSender) - .outboundTransfer(tokenSender.address, l2Receiver.address, toGRT('10'), maxGas, gasPriceBid, defaultData, { - value: defaultEthValue, - }) - await expect(tx).revertedWith('TOKEN_NOT_GRT') - }) - it('puts tokens in escrow and creates a retryable ticket', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender, defaultData, emptyCallHookData) - }) - it('decodes the sender address from messages sent by the router', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - const routerEncodedData = utils.defaultAbiCoder.encode(['address', 'bytes'], [tokenSender.address, defaultData]) - await testValidOutboundTransfer(routerMock, routerEncodedData, emptyCallHookData) - }) - it('reverts when called with no submission cost', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - const tx = l1GraphTokenGateway - .connect(tokenSender) - .outboundTransfer( - grt.address, - l2Receiver.address, - toGRT('10'), - maxGas, - gasPriceBid, - defaultDataNoSubmissionCost, - { - value: defaultEthValue, - }, - ) - await expect(tx).revertedWith('NO_SUBMISSION_COST') - }) - it('reverts when called with nonempty calldata, if the sender is not allowlisted', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - const tx = l1GraphTokenGateway - .connect(tokenSender) - .outboundTransfer( - grt.address, - l2Receiver.address, - toGRT('10'), - maxGas, - gasPriceBid, - defaultDataWithNotEmptyCallHookData, - { - value: defaultEthValue, - }, - ) - await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED') - }) - it('allows sending nonempty calldata, if the sender is allowlisted', async function () { - // Make the sender a contract so that it can be allowed to send callhooks - await helpers.setCode(tokenSender.address, '0x1234') - await l1GraphTokenGateway.connect(governor).addToCallhookAllowlist(tokenSender.address) - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender, defaultDataWithNotEmptyCallHookData, notEmptyCallHookData) - }) - it('reverts when the sender does not have enough GRT', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('1001')) - const tx = l1GraphTokenGateway - .connect(tokenSender) - .outboundTransfer(grt.address, l2Receiver.address, toGRT('1001'), maxGas, gasPriceBid, defaultData, { - value: defaultEthValue, - }) - await expect(tx).revertedWith('ERC20: transfer amount exceeds balance') - }) - }) - - describe('finalizeInboundTransfer', function () { - it('reverts when called by an account that is not the bridge', async function () { - const tx = l1GraphTokenGateway - .connect(tokenSender) - .finalizeInboundTransfer(grt.address, l2Receiver.address, tokenSender.address, toGRT('10'), defaultData) - await expect(tx).revertedWith('NOT_FROM_BRIDGE') - }) - it('reverts when called by the bridge, but the tx was not started by the L2 gateway', async function () { - const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ - grt.address, - l2Receiver.address, - tokenSender.address, - toGRT('10'), - utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), - ]) - // The real outbox would require a proof, which would - // validate that the tx was initiated by the L2 gateway but our mock - // just executes unconditionally - const tx = outboxMock.connect(tokenSender).executeTransaction( - toBN('0'), - [], - toBN('0'), - l2Receiver.address, // Note this is not l2GRTGatewayMock - l1GraphTokenGateway.address, - toBN('1337'), - await helpers.latestBlock(), - toBN('133701337'), - toBN('0'), - encodedCalldata, - ) - await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') - }) - it('reverts if the gateway does not have tokens or allowance', async function () { - // This scenario should never really happen, but we still - // test that the gateway reverts in this case - const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ - grt.address, - l2Receiver.address, - tokenSender.address, - toGRT('10'), - utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), - ]) - // The real outbox would require a proof, which would - // validate that the tx was initiated by the L2 gateway but our mock - // just executes unconditionally - const tx = outboxMock - .connect(tokenSender) - .executeTransaction( - toBN('0'), - [], - toBN('0'), - l2GRTGatewayMock.address, - l1GraphTokenGateway.address, - toBN('1337'), - await helpers.latestBlock(), - toBN('133701337'), - toBN('0'), - encodedCalldata, - ) - await expect(tx).revertedWith('INVALID_L2_MINT_AMOUNT') - }) - it('reverts if the gateway is revoked from escrow', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender, defaultData, emptyCallHookData) - // At this point, the gateway holds 10 GRT in escrow - // But we revoke the gateway's permission to move the funds: - await bridgeEscrow.connect(governor).revokeAll(l1GraphTokenGateway.address) - const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ - grt.address, - l2Receiver.address, - tokenSender.address, - toGRT('8'), - utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), - ]) - // The real outbox would require a proof, which would - // validate that the tx was initiated by the L2 gateway but our mock - // just executes unconditionally - const tx = outboxMock - .connect(tokenSender) - .executeTransaction( - toBN('0'), - [], - toBN('0'), - l2GRTGatewayMock.address, - l1GraphTokenGateway.address, - toBN('1337'), - await helpers.latestBlock(), - toBN('133701337'), - toBN('0'), - encodedCalldata, - ) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - }) - it('sends tokens out of escrow', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender, defaultData, emptyCallHookData) - // At this point, the gateway holds 10 GRT in escrow - const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ - grt.address, - l2Receiver.address, - tokenSender.address, - toGRT('8'), - utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), - ]) - // The real outbox would require a proof, which would - // validate that the tx was initiated by the L2 gateway but our mock - // just executes unconditionally - const tx = outboxMock - .connect(tokenSender) - .executeTransaction( - toBN('0'), - [], - toBN('0'), - l2GRTGatewayMock.address, - l1GraphTokenGateway.address, - toBN('1337'), - await helpers.latestBlock(), - toBN('133701337'), - toBN('0'), - encodedCalldata, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'WithdrawalFinalized') - .withArgs(grt.address, l2Receiver.address, tokenSender.address, toBN('0'), toGRT('8')) - const escrowBalance = await grt.balanceOf(bridgeEscrow.address) - const senderBalance = await grt.balanceOf(tokenSender.address) - expect(escrowBalance).eq(toGRT('2')) - expect(senderBalance).eq(toGRT('998')) - }) - it('mints tokens up to the L2 mint allowance', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender, defaultData, emptyCallHookData) - - // Start accruing L2 mint allowance at 2 GRT per block - await l1GraphTokenGateway.connect(governor).updateL2MintAllowance(toGRT('2'), await helpers.latestBlock()) - await helpers.mine(2) - // Now it's been three blocks since the lastL2MintAllowanceUpdateBlock, so - // there should be 8 GRT allowed to be minted from L2 in the next block. - - // At this point, the gateway holds 10 GRT in escrow - const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ - grt.address, - l2Receiver.address, - tokenSender.address, - toGRT('18'), - utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), - ]) - // The real outbox would require a proof, which would - // validate that the tx was initiated by the L2 gateway but our mock - // just executes unconditionally - const tx = outboxMock - .connect(tokenSender) - .executeTransaction( - toBN('0'), - [], - toBN('0'), - l2GRTGatewayMock.address, - l1GraphTokenGateway.address, - toBN('1337'), - await helpers.latestBlock(), - toBN('133701337'), - toBN('0'), - encodedCalldata, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'WithdrawalFinalized') - .withArgs(grt.address, l2Receiver.address, tokenSender.address, toBN('0'), toGRT('18')) - .emit(l1GraphTokenGateway, 'TokensMintedFromL2') - .withArgs(toGRT('8')) - expect(await l1GraphTokenGateway.totalMintedFromL2()).to.eq(toGRT('8')) - expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await helpers.latestBlock())).to.eq( - toGRT('8'), - ) - - const escrowBalance = await grt.balanceOf(bridgeEscrow.address) - const senderBalance = await grt.balanceOf(tokenSender.address) - expect(escrowBalance).eq(toGRT('0')) - expect(senderBalance).eq(toGRT('1008')) - }) - it('reverts if the amount to mint is over the allowance', async function () { - await grt.connect(tokenSender).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender, defaultData, emptyCallHookData) - - // Start accruing L2 mint allowance at 2 GRT per block - await l1GraphTokenGateway.connect(governor).updateL2MintAllowance(toGRT('2'), await helpers.latestBlock()) - await helpers.mine(2) - // Now it's been three blocks since the lastL2MintAllowanceUpdateBlock, so - // there should be 8 GRT allowed to be minted from L2 in the next block. - - // At this point, the gateway holds 10 GRT in escrow - const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ - grt.address, - l2Receiver.address, - tokenSender.address, - toGRT('18.001'), - utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), - ]) - // The real outbox would require a proof, which would - // validate that the tx was initiated by the L2 gateway but our mock - // just executes unconditionally - const tx = outboxMock - .connect(tokenSender) - .executeTransaction( - toBN('0'), - [], - toBN('0'), - l2GRTGatewayMock.address, - l1GraphTokenGateway.address, - toBN('1337'), - await helpers.latestBlock(), - toBN('133701337'), - toBN('0'), - encodedCalldata, - ) - await expect(tx).revertedWith('INVALID_L2_MINT_AMOUNT') - }) - }) - }) -}) diff --git a/packages/contracts-test/tests/unit/l2/l2ArbitrumMessengerMock.ts b/packages/contracts-test/tests/unit/l2/l2ArbitrumMessengerMock.ts deleted file mode 100644 index 51d93bf12..000000000 --- a/packages/contracts-test/tests/unit/l2/l2ArbitrumMessengerMock.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Mock implementation of the L2ArbitrumMessenger contract - * This is used to override the sendTxToL1 function in the L2GraphTokenGateway contract - */ -export class L2ArbitrumMessengerMock { - private static _calls: Array<{ - l1CallValue: number - from: string - to: string - data: string - }> = [] - - /** - * Mock implementation of sendTxToL1 function - * @param l1CallValue The call value to send to L1 - * @param from The sender address - * @param to The destination address on L1 - * @param data The calldata to send to L1 - * @returns A transaction ID (always returns 1) - */ - public static sendTxToL1(l1CallValue: number, from: string, to: string, data: string): number { - this._calls.push({ l1CallValue, from, to, data }) - return 1 // Always return 1 as the transaction ID - } - - /** - * Check if sendTxToL1 was called with specific arguments - * @param to The expected destination address - * @param data The expected calldata - * @returns true if the function was called with the specified arguments - */ - public static calledWith(to: string, data: string): boolean { - return this._calls.some((call) => call.to === to && call.data === data) - } - - /** - * Reset all recorded calls - */ - public static reset(): void { - this._calls = [] - } - - /** - * Get the number of times sendTxToL1 was called - */ - public static get callCount(): number { - return this._calls.length - } -} diff --git a/packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts b/packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts deleted file mode 100644 index 45e6b0fc1..000000000 --- a/packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts +++ /dev/null @@ -1,395 +0,0 @@ -import { FakeContract, smock } from '@defi-wonderland/smock' -import { CallhookReceiverMock } from '@graphprotocol/contracts' -import { L2GraphToken } from '@graphprotocol/contracts' -import { L2GraphTokenGateway } from '@graphprotocol/contracts' -import { expect, use } from 'chai' -import { constants, ContractTransaction, Signer, utils, Wallet } from 'ethers' -import hre from 'hardhat' - -import { isRunningUnderCoverage } from '../../../utils/coverage' -import { NetworkFixture } from '../lib/fixtures' - -use(smock.matchers) - -import { GraphToken, L1GraphTokenGateway } from '@graphprotocol/contracts' -import { RewardsManager } from '@graphprotocol/contracts' -import { deploy, DeployType, GraphNetworkContracts, helpers, toBN, toGRT } from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' - -const { AddressZero } = constants - -describe('L2GraphTokenGateway', () => { - const graph = hre.graph() - let me: SignerWithAddress - let governor: SignerWithAddress - let tokenSender: SignerWithAddress - let l1Receiver: SignerWithAddress - let l2Receiver: SignerWithAddress - let pauseGuardian: SignerWithAddress - let fixture: NetworkFixture - let arbSysMock: FakeContract - - let fixtureContracts: GraphNetworkContracts - let l1MockContracts: GraphNetworkContracts - let l1GRTMock: GraphToken - let l1GRTGatewayMock: L1GraphTokenGateway - let routerMock: Wallet - - let grt: L2GraphToken - let l2GraphTokenGateway: L2GraphTokenGateway - let callhookReceiverMock: CallhookReceiverMock - let rewardsManager: RewardsManager - - const senderTokens = toGRT('1000') - const defaultData = '0x' - const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( - ['uint256', 'uint256'], - [toBN('1337'), toBN('42')], - ) - - before(async function () { - ;[me, governor, tokenSender, l1Receiver, l2Receiver, pauseGuardian] = await graph.getTestAccounts() - - fixture = new NetworkFixture(graph.provider) - - // Deploy L2 - fixtureContracts = await fixture.load(governor, true) - grt = fixtureContracts.GraphToken as L2GraphToken - l2GraphTokenGateway = fixtureContracts.L2GraphTokenGateway as L2GraphTokenGateway - rewardsManager = fixtureContracts.RewardsManager as RewardsManager - - // Deploy L2 arbitrum bridge - ;({ routerMock } = await fixture.loadL2ArbitrumBridge(governor)) - - // Deploy L1 mock - l1MockContracts = await fixture.loadMock(false) - l1GRTMock = l1MockContracts.GraphToken as GraphToken - l1GRTGatewayMock = l1MockContracts.L1GraphTokenGateway as L1GraphTokenGateway - - callhookReceiverMock = ( - await deploy(DeployType.Deploy, governor, { - name: 'CallhookReceiverMock', - }) - ).contract as CallhookReceiverMock - - // Give some funds to the token sender and router mock - await grt.connect(governor).mint(tokenSender.address, senderTokens) - await helpers.setBalance(routerMock.address, utils.parseEther('1')) - }) - - beforeEach(async function () { - await fixture.setUp() - // Thanks to Livepeer: https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/test/unit/L2/l2LPTGateway.test.ts#L86 - // Skip smock setup when running under coverage due to provider compatibility issues - if (!isRunningUnderCoverage()) { - arbSysMock = await smock.fake('ArbSys', { - address: '0x0000000000000000000000000000000000000064', - }) - arbSysMock.sendTxToL1.returns(1) - } - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - context('> immediately after deploy', function () { - describe('calculateL2TokenAddress', function () { - it('should return the zero address', async function () { - expect(await l2GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(AddressZero) - }) - }) - - describe('outboundTransfer', function () { - it('reverts because it is paused', async function () { - const tx = l2GraphTokenGateway - .connect(tokenSender) - ['outboundTransfer(address,address,uint256,bytes)'](grt.address, l1Receiver.address, toGRT('10'), defaultData) - await expect(tx).revertedWith('Paused (contract)') - }) - }) - - describe('finalizeInboundTransfer', function () { - it('revert because it is paused', async function () { - const tx = l2GraphTokenGateway - .connect(tokenSender) - .finalizeInboundTransfer(grt.address, tokenSender.address, l1Receiver.address, toGRT('10'), defaultData) - await expect(tx).revertedWith('Paused (contract)') - }) - }) - - describe('setL2Router', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l2GraphTokenGateway.connect(tokenSender).setL2Router(routerMock.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('sets router address', async function () { - const tx = l2GraphTokenGateway.connect(governor).setL2Router(routerMock.address) - await expect(tx).emit(l2GraphTokenGateway, 'L2RouterSet').withArgs(routerMock.address) - expect(await l2GraphTokenGateway.l2Router()).eq(routerMock.address) - }) - }) - - describe('setL1TokenAddress', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l2GraphTokenGateway.connect(tokenSender).setL1TokenAddress(l1GRTMock.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('sets l2GRT', async function () { - const tx = l2GraphTokenGateway.connect(governor).setL1TokenAddress(l1GRTMock.address) - await expect(tx).emit(l2GraphTokenGateway, 'L1TokenAddressSet').withArgs(l1GRTMock.address) - expect(await l2GraphTokenGateway.l1GRT()).eq(l1GRTMock.address) - }) - }) - - describe('setL1CounterpartAddress', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l2GraphTokenGateway.connect(tokenSender).setL1CounterpartAddress(l1GRTGatewayMock.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('sets L1Counterpart', async function () { - const tx = l2GraphTokenGateway.connect(governor).setL1CounterpartAddress(l1GRTGatewayMock.address) - await expect(tx).emit(l2GraphTokenGateway, 'L1CounterpartAddressSet').withArgs(l1GRTGatewayMock.address) - expect(await l2GraphTokenGateway.l1Counterpart()).eq(l1GRTGatewayMock.address) - }) - }) - describe('Pausable behavior', () => { - it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { - let tx = l2GraphTokenGateway.connect(tokenSender).setPaused(false) - await expect(tx).revertedWith('Only Governor or Guardian') - tx = l2GraphTokenGateway.connect(tokenSender).setPaused(true) - await expect(tx).revertedWith('Only Governor or Guardian') - }) - it('cannot be paused if some state variables are not set', async function () { - let tx = l2GraphTokenGateway.connect(governor).setPaused(false) - await expect(tx).revertedWith('L2_ROUTER_NOT_SET') - await l2GraphTokenGateway.connect(governor).setL2Router(routerMock.address) - tx = l2GraphTokenGateway.connect(governor).setPaused(false) - await expect(tx).revertedWith('L1_COUNTERPART_NOT_SET') - await l2GraphTokenGateway.connect(governor).setL1CounterpartAddress(l1GRTGatewayMock.address) - tx = l2GraphTokenGateway.connect(governor).setPaused(false) - await expect(tx).revertedWith('L1_GRT_NOT_SET') - }) - it('can be paused and unpaused by the governor', async function () { - await fixture.configureL2Bridge(governor, fixtureContracts, l1MockContracts) - let tx = l2GraphTokenGateway.connect(governor).setPaused(true) - await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) - expect(await l2GraphTokenGateway.paused()).eq(true) - tx = l2GraphTokenGateway.connect(governor).setPaused(false) - await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) - expect(await l2GraphTokenGateway.paused()).eq(false) - }) - describe('setPauseGuardian', function () { - it('cannot be called by someone other than governor', async function () { - const tx = l2GraphTokenGateway.connect(tokenSender).setPauseGuardian(pauseGuardian.address) - await expect(tx).revertedWith('Only Controller governor') - }) - it('sets a new pause guardian', async function () { - const currentPauseGuardian = await l2GraphTokenGateway.pauseGuardian() - const tx = l2GraphTokenGateway.connect(governor).setPauseGuardian(pauseGuardian.address) - await expect(tx) - .emit(l2GraphTokenGateway, 'NewPauseGuardian') - .withArgs(currentPauseGuardian, pauseGuardian.address) - }) - it('allows a pause guardian to pause and unpause', async function () { - await fixture.configureL2Bridge(governor, fixtureContracts, l1MockContracts) - await l2GraphTokenGateway.connect(governor).setPauseGuardian(pauseGuardian.address) - let tx = l2GraphTokenGateway.connect(pauseGuardian).setPaused(true) - await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) - expect(await l2GraphTokenGateway.paused()).eq(true) - tx = l2GraphTokenGateway.connect(pauseGuardian).setPaused(false) - await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) - expect(await l2GraphTokenGateway.paused()).eq(false) - }) - }) - }) - }) - - context('> after configuring and unpausing', function () { - const testValidOutboundTransfer = async function (signer: Signer, data: string) { - const tx = l2GraphTokenGateway - .connect(signer) - ['outboundTransfer(address,address,uint256,bytes)'](l1GRTMock.address, l1Receiver.address, toGRT('10'), data) - const expectedId = 1 - await expect(tx) - .emit(l2GraphTokenGateway, 'WithdrawalInitiated') - .withArgs(l1GRTMock.address, tokenSender.address, l1Receiver.address, expectedId, 0, toGRT('10')) - - // Should use the L1 Gateway's interface, but both come from ITokenGateway - const calldata = l2GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ - l1GRTMock.address, - tokenSender.address, - l1Receiver.address, - toGRT('10'), - utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), - ]) - await expect(tx) - .emit(l2GraphTokenGateway, 'TxToL1') - .withArgs(tokenSender.address, l1GRTGatewayMock.address, 1, calldata) - - // For some reason the call count doesn't work properly, - // and each function call is counted 12 times. - // Possibly related to https://github.com/defi-wonderland/smock/issues/85 ? - // expect(arbSysMock.sendTxToL1).to.have.been.calledOnce - - // Only check smock expectations when not running under coverage - if (!isRunningUnderCoverage() && arbSysMock) { - expect(arbSysMock.sendTxToL1).to.have.been.calledWith(l1GRTGatewayMock.address, calldata) - } - const senderBalance = await grt.balanceOf(tokenSender.address) - expect(senderBalance).eq(toGRT('990')) - } - before(async function () { - await fixture.configureL2Bridge(governor, fixtureContracts, l1MockContracts) - }) - - describe('calculateL2TokenAddress', function () { - it('returns the L2 token address', async function () { - expect(await l2GraphTokenGateway.calculateL2TokenAddress(l1GRTMock.address)).eq(grt.address) - }) - it('returns the zero address if the input is any other address', async function () { - expect(await l2GraphTokenGateway.calculateL2TokenAddress(tokenSender.address)).eq(AddressZero) - }) - }) - - describe('outboundTransfer', function () { - it('reverts when called with the wrong token address', async function () { - const tx = l2GraphTokenGateway - .connect(tokenSender) - [ - 'outboundTransfer(address,address,uint256,bytes)' - ](tokenSender.address, l1Receiver.address, toGRT('10'), defaultData) - await expect(tx).revertedWith('TOKEN_NOT_GRT') - }) - it('burns tokens and triggers an L1 call', async function () { - // Skip this test under coverage due to complex instrumentation issues - if (isRunningUnderCoverage()) { - this.skip() - return - } - - await grt.connect(tokenSender).approve(l2GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender, defaultData) - }) - it('decodes the sender address from messages sent by the router', async function () { - // Skip this test under coverage due to complex instrumentation issues - if (isRunningUnderCoverage()) { - this.skip() - return - } - - await grt.connect(tokenSender).approve(l2GraphTokenGateway.address, toGRT('10')) - const routerEncodedData = utils.defaultAbiCoder.encode(['address', 'bytes'], [tokenSender.address, defaultData]) - await testValidOutboundTransfer(routerMock, routerEncodedData) - }) - it('reverts when called with nonempty calldata', async function () { - await grt.connect(tokenSender).approve(l2GraphTokenGateway.address, toGRT('10')) - const tx = l2GraphTokenGateway - .connect(tokenSender) - [ - 'outboundTransfer(address,address,uint256,bytes)' - ](l1GRTMock.address, l1Receiver.address, toGRT('10'), defaultDataWithNotEmptyCallHookData) - await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED') - }) - it('reverts when the sender does not have enough GRT', async function () { - await grt.connect(tokenSender).approve(l2GraphTokenGateway.address, toGRT('1001')) - const tx = l2GraphTokenGateway - .connect(tokenSender) - [ - 'outboundTransfer(address,address,uint256,bytes)' - ](l1GRTMock.address, l1Receiver.address, toGRT('1001'), defaultData) - await expect(tx).revertedWith('ERC20: burn amount exceeds balance') - }) - }) - - describe('finalizeInboundTransfer', function () { - const testValidFinalizeTransfer = async function (data: string, to?: string): Promise { - to = to ?? l2Receiver.address - const l1GRTGatewayMockL2Alias = await helpers.getL2SignerFromL1(l1GRTGatewayMock.address) - await me.sendTransaction({ - to: await l1GRTGatewayMockL2Alias.getAddress(), - value: utils.parseUnits('1', 'ether'), - }) - const tx = l2GraphTokenGateway - .connect(l1GRTGatewayMockL2Alias) - .finalizeInboundTransfer(l1GRTMock.address, tokenSender.address, to, toGRT('10'), data) - await expect(tx) - .emit(l2GraphTokenGateway, 'DepositFinalized') - .withArgs(l1GRTMock.address, tokenSender.address, to, toGRT('10')) - - await expect(tx).emit(grt, 'BridgeMinted').withArgs(to, toGRT('10')) - - // Unchanged - const senderBalance = await grt.balanceOf(tokenSender.address) - expect(senderBalance).eq(toGRT('1000')) - // 10 newly minted GRT - const receiverBalance = await grt.balanceOf(to) - expect(receiverBalance).eq(toGRT('10')) - return tx - } - it('reverts when called by an account that is not the gateway', async function () { - const tx = l2GraphTokenGateway - .connect(tokenSender) - .finalizeInboundTransfer(l1GRTMock.address, tokenSender.address, l2Receiver.address, toGRT('10'), defaultData) - await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') - }) - it('reverts when called by an account that is the gateway but without the L2 alias', async function () { - const impersonatedGateway = await helpers.impersonateAccount(l1GRTGatewayMock.address) - const tx = l2GraphTokenGateway - .connect(impersonatedGateway) - .finalizeInboundTransfer(l1GRTMock.address, tokenSender.address, l2Receiver.address, toGRT('10'), defaultData) - await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') - }) - it('mints and sends tokens when called by the aliased gateway', async function () { - await testValidFinalizeTransfer(defaultData) - }) - it('calls a callhook if the transfer includes calldata', async function () { - const tx = await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData, callhookReceiverMock.address) - // Emitted by the callhook: - await expect(tx) - .emit(callhookReceiverMock, 'TransferReceived') - .withArgs(tokenSender.address, toGRT('10'), toBN('1337'), toBN('42')) - }) - it('reverts if a callhook reverts', async function () { - // The 0 will make the callhook revert (see CallhookReceiverMock.sol) - const callHookData = utils.defaultAbiCoder.encode(['uint256', 'uint256'], [toBN('0'), toBN('42')]) - const l1GRTGatewayMockL2Alias = await helpers.getL2SignerFromL1(l1GRTGatewayMock.address) - await me.sendTransaction({ - to: await l1GRTGatewayMockL2Alias.getAddress(), - value: utils.parseUnits('1', 'ether'), - }) - const tx = l2GraphTokenGateway - .connect(l1GRTGatewayMockL2Alias) - .finalizeInboundTransfer( - l1GRTMock.address, - tokenSender.address, - callhookReceiverMock.address, - toGRT('10'), - callHookData, - ) - await expect(tx).revertedWith('FOO_IS_ZERO') - }) - it('reverts if trying to call a callhook in a contract that does not implement onTokenTransfer', async function () { - const callHookData = utils.defaultAbiCoder.encode(['uint256'], [toBN('0')]) - const l1GRTGatewayMockL2Alias = await helpers.getL2SignerFromL1(l1GRTGatewayMock.address) - await me.sendTransaction({ - to: await l1GRTGatewayMockL2Alias.getAddress(), - value: utils.parseUnits('1', 'ether'), - }) - // RewardsManager does not implement onTokenTransfer, so this will fail - const tx = l2GraphTokenGateway - .connect(l1GRTGatewayMockL2Alias) - .finalizeInboundTransfer( - l1GRTMock.address, - tokenSender.address, - rewardsManager.address, - toGRT('10'), - callHookData, - ) - - // The error message may vary depending on the environment - await expect(tx).to.be.reverted - }) - }) - }) -}) diff --git a/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayConfig.t.sol b/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayConfig.t.sol new file mode 100644 index 000000000..1ea1e2bb1 --- /dev/null +++ b/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayConfig.t.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { L1GraphTokenGatewaySharedTest } from "./L1GraphTokenGatewayShared.t.sol"; +import { IL1GraphTokenGatewayLike } from "../interfaces/IL1GraphTokenGatewayLike.sol"; + +/// Cases from the "immediately after deploy" context: the gateway is paused and unconfigured +contract L1GraphTokenGatewayConfigTest is L1GraphTokenGatewaySharedTest { + // -- calculateL2TokenAddress -- + + function test_CalculateL2TokenAddress_ReturnsZeroWhenNotSet() public view { + assertEq(l1Gateway.calculateL2TokenAddress(address(grt)), address(0)); + } + + // -- outboundTransfer -- + + function test_OutboundTransfer_RevertWhen_Paused() public { + vm.expectRevert(bytes("Paused (contract)")); + vm.prank(tokenSender); + l1Gateway.outboundTransfer{ value: DEFAULT_ETH_VALUE }( + address(grt), + l2Receiver, + 10 ether, + MAX_GAS, + GAS_PRICE_BID, + defaultData + ); + } + + // -- finalizeInboundTransfer -- + + function test_FinalizeInboundTransfer_RevertWhen_Paused() public { + vm.expectRevert(bytes("Paused (contract)")); + vm.prank(tokenSender); + l1Gateway.finalizeInboundTransfer(address(grt), l2Receiver, tokenSender, 10 ether, defaultData); + } + + // -- setArbitrumAddresses -- + + function test_SetArbitrumAddresses_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l1Gateway.setArbitrumAddresses(address(inboxMock), routerMock); + } + + function test_SetArbitrumAddresses_RevertWhen_NotContract() public { + vm.expectRevert(bytes("INBOX_MUST_BE_CONTRACT")); + vm.prank(governor); + l1Gateway.setArbitrumAddresses(tokenSender, routerMock); + + vm.expectRevert(bytes("ROUTER_MUST_BE_CONTRACT")); + vm.prank(governor); + l1Gateway.setArbitrumAddresses(address(inboxMock), tokenSender); + } + + function test_SetArbitrumAddresses_SetsInboxAndRouter() public { + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.ArbitrumAddressesSet(address(inboxMock), routerMock); + vm.prank(governor); + l1Gateway.setArbitrumAddresses(address(inboxMock), routerMock); + + assertEq(l1Gateway.l1Router(), routerMock); + assertEq(l1Gateway.inbox(), address(inboxMock)); + } + + // -- setL2TokenAddress -- + + function test_SetL2TokenAddress_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l1Gateway.setL2TokenAddress(l2GRTMock); + } + + function test_SetL2TokenAddress_SetsL2GRT() public { + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2TokenAddressSet(l2GRTMock); + vm.prank(governor); + l1Gateway.setL2TokenAddress(l2GRTMock); + + assertEq(l1Gateway.l2GRT(), l2GRTMock); + } + + // -- setL2CounterpartAddress -- + + function test_SetL2CounterpartAddress_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l1Gateway.setL2CounterpartAddress(l2GatewayMock); + } + + function test_SetL2CounterpartAddress_SetsL2Counterpart() public { + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2CounterpartAddressSet(l2GatewayMock); + vm.prank(governor); + l1Gateway.setL2CounterpartAddress(l2GatewayMock); + + assertEq(l1Gateway.l2Counterpart(), l2GatewayMock); + assertEq(l1Gateway.counterpartGateway(), l2GatewayMock); + } + + // -- setEscrowAddress -- + + function test_SetEscrowAddress_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l1Gateway.setEscrowAddress(address(bridgeEscrow)); + } + + function test_SetEscrowAddress_SetsEscrow() public { + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.EscrowAddressSet(address(bridgeEscrow)); + vm.prank(governor); + l1Gateway.setEscrowAddress(address(bridgeEscrow)); + + assertEq(l1Gateway.escrow(), address(bridgeEscrow)); + } + + // -- addToCallhookAllowlist -- + + function test_AddToCallhookAllowlist_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l1Gateway.addToCallhookAllowlist(address(controller)); + + assertFalse(l1Gateway.callhookAllowlist(address(controller))); + } + + function test_AddToCallhookAllowlist_RevertWhen_NotContract() public { + vm.expectRevert(bytes("MUST_BE_CONTRACT")); + vm.prank(governor); + l1Gateway.addToCallhookAllowlist(tokenSender); + } + + function test_AddToCallhookAllowlist_AddsAddress() public { + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.AddedToCallhookAllowlist(address(controller)); + vm.prank(governor); + l1Gateway.addToCallhookAllowlist(address(controller)); + + assertTrue(l1Gateway.callhookAllowlist(address(controller))); + } + + // -- removeFromCallhookAllowlist -- + + function test_RemoveFromCallhookAllowlist_RevertWhen_NotGovernor() public { + vm.prank(governor); + l1Gateway.addToCallhookAllowlist(address(controller)); + + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l1Gateway.removeFromCallhookAllowlist(address(controller)); + + assertTrue(l1Gateway.callhookAllowlist(address(controller))); + } + + function test_RemoveFromCallhookAllowlist_RemovesAddress() public { + vm.prank(governor); + l1Gateway.addToCallhookAllowlist(address(controller)); + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.RemovedFromCallhookAllowlist(address(controller)); + vm.prank(governor); + l1Gateway.removeFromCallhookAllowlist(address(controller)); + + assertFalse(l1Gateway.callhookAllowlist(address(controller))); + } + + // -- Pausable behavior -- + + function test_PausableBehavior_RevertWhen_NotGovernorOrGuardian() public { + vm.expectRevert(bytes("Only Governor or Guardian")); + vm.prank(tokenSender); + l1Gateway.setPaused(false); + + vm.expectRevert(bytes("Only Governor or Guardian")); + vm.prank(tokenSender); + l1Gateway.setPaused(true); + } + + function test_PausableBehavior_RevertWhen_UnpausingWithStateNotSet() public { + vm.startPrank(governor); + vm.expectRevert(bytes("INBOX_NOT_SET")); + l1Gateway.setPaused(false); + + l1Gateway.setArbitrumAddresses(address(inboxMock), routerMock); + vm.expectRevert(bytes("L2_COUNTERPART_NOT_SET")); + l1Gateway.setPaused(false); + + l1Gateway.setL2CounterpartAddress(l2GatewayMock); + vm.expectRevert(bytes("ESCROW_NOT_SET")); + l1Gateway.setPaused(false); + vm.stopPrank(); + } + + function test_PausableBehavior_GovernorCanPauseAndUnpause() public { + _configureL1Bridge(); + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.PauseChanged(true); + vm.prank(governor); + l1Gateway.setPaused(true); + assertTrue(l1Gateway.paused()); + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.PauseChanged(false); + vm.prank(governor); + l1Gateway.setPaused(false); + assertFalse(l1Gateway.paused()); + } + + // -- setPauseGuardian -- + + function test_SetPauseGuardian_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l1Gateway.setPauseGuardian(pauseGuardian); + } + + function test_SetPauseGuardian_SetsPauseGuardian() public { + address currentPauseGuardian = l1Gateway.pauseGuardian(); + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.NewPauseGuardian(currentPauseGuardian, pauseGuardian); + vm.prank(governor); + l1Gateway.setPauseGuardian(pauseGuardian); + } + + function test_SetPauseGuardian_GuardianCanPauseAndUnpause() public { + _configureL1Bridge(); + vm.prank(governor); + l1Gateway.setPauseGuardian(pauseGuardian); + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.PauseChanged(true); + vm.prank(pauseGuardian); + l1Gateway.setPaused(true); + assertTrue(l1Gateway.paused()); + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.PauseChanged(false); + vm.prank(pauseGuardian); + l1Gateway.setPaused(false); + assertFalse(l1Gateway.paused()); + } +} diff --git a/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayShared.t.sol b/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayShared.t.sol new file mode 100644 index 000000000..99acfad31 --- /dev/null +++ b/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayShared.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IManaged } from "@graphprotocol/interfaces/contracts/contracts/governance/IManaged.sol"; +import { ProtocolFixture } from "../shared/ProtocolFixture.t.sol"; +import { IGraphTokenLike } from "../interfaces/IGraphTokenLike.sol"; +import { IBridgeEscrowLike } from "../interfaces/IBridgeEscrowLike.sol"; +import { IL1GraphTokenGatewayLike } from "../interfaces/IL1GraphTokenGatewayLike.sol"; +import { IBridgeMockLike, IInboxMockLike, IOutboxMockLike } from "../interfaces/IArbitrumMocksLike.sol"; + +abstract contract L1GraphTokenGatewaySharedTest is ProtocolFixture { + string internal constant GRAPH_TOKEN = string.concat(PKG_ARTIFACTS, "token/GraphToken.sol/GraphToken.json"); + string internal constant BRIDGE_ESCROW = string.concat(PKG_ARTIFACTS, "gateway/BridgeEscrow.sol/BridgeEscrow.json"); + string internal constant L1_GATEWAY = + string.concat(PKG_ARTIFACTS, "gateway/L1GraphTokenGateway.sol/L1GraphTokenGateway.json"); + string internal constant BRIDGE_MOCK = + string.concat(PKG_ARTIFACTS, "tests/arbitrum/BridgeMock.sol/BridgeMock.json"); + string internal constant INBOX_MOCK = string.concat(PKG_ARTIFACTS, "tests/arbitrum/InboxMock.sol/InboxMock.json"); + string internal constant OUTBOX_MOCK = + string.concat(PKG_ARTIFACTS, "tests/arbitrum/OutboxMock.sol/OutboxMock.json"); + + uint256 internal constant SENDER_TOKENS = 1000 ether; + uint256 internal constant MAX_GAS = 1_000_000; + uint256 internal constant MAX_SUBMISSION_COST = 7; + uint256 internal constant GAS_PRICE_BID = 2; + uint256 internal constant DEFAULT_ETH_VALUE = MAX_SUBMISSION_COST + MAX_GAS * GAS_PRICE_BID; + + IGraphTokenLike internal grt; + IBridgeEscrowLike internal bridgeEscrow; + IL1GraphTokenGatewayLike internal l1Gateway; + + IBridgeMockLike internal bridgeMock; + IInboxMockLike internal inboxMock; + IOutboxMockLike internal outboxMock; + + address internal tokenSender = makeAddr("tokenSender"); + address internal l2Receiver = makeAddr("l2Receiver"); + address internal pauseGuardian = makeAddr("pauseGuardian"); + address internal routerMock = makeAddr("routerMock"); + address internal l2GRTMock = makeAddr("l2GRTMock"); + address internal l2GatewayMock = makeAddr("l2GatewayMock"); + + bytes internal defaultData = abi.encode(MAX_SUBMISSION_COST, bytes("")); + + function setUp() public virtual { + vm.warp(1_700_000_000); + _deployProtocolBase(); + + vm.prank(governor); + grt = IGraphTokenLike(_deployArtifact(GRAPH_TOKEN, abi.encode(10_000_000_000 ether))); + _registerContract("GraphToken", address(grt)); + + bridgeEscrow = IBridgeEscrowLike( + _deployBehindProxy( + _deployArtifact(BRIDGE_ESCROW, ""), + abi.encodeCall(IBridgeEscrowLike.initialize, (address(controller))) + ) + ); + IManaged(address(bridgeEscrow)).syncAllContracts(); + + l1Gateway = IL1GraphTokenGatewayLike( + _deployBehindProxy( + _deployArtifact(L1_GATEWAY, ""), + abi.encodeCall(IL1GraphTokenGatewayLike.initialize, (address(controller))) + ) + ); + _registerContract("GraphTokenGateway", address(l1Gateway)); + IManaged(address(l1Gateway)).syncAllContracts(); + + bridgeMock = IBridgeMockLike(_deployArtifact(BRIDGE_MOCK, "")); + inboxMock = IInboxMockLike(_deployArtifact(INBOX_MOCK, "")); + outboxMock = IOutboxMockLike(_deployArtifact(OUTBOX_MOCK, "")); + bridgeMock.setInbox(address(inboxMock), true); + bridgeMock.setOutbox(address(outboxMock), true); + inboxMock.setBridge(address(bridgeMock)); + outboxMock.setBridge(address(bridgeMock)); + + vm.etch(routerMock, hex"1234"); + vm.etch(l2GRTMock, hex"1234"); + vm.etch(l2GatewayMock, hex"1234"); + + vm.startPrank(governor); + // As in the old fixture's graph config, the gateway mints what L2 sends beyond the escrow + grt.addMinter(address(l1Gateway)); + grt.mint(tokenSender, SENDER_TOKENS); + vm.stopPrank(); + vm.deal(tokenSender, 1 ether); + vm.deal(routerMock, 1 ether); + } + + /// Wires the gateway to the L2 mocks, escrow and Arbitrum bridge mocks, then unpauses it + function _configureL1Bridge() internal { + vm.startPrank(governor); + l1Gateway.setL2TokenAddress(l2GRTMock); + l1Gateway.setL2CounterpartAddress(l2GatewayMock); + l1Gateway.setEscrowAddress(address(bridgeEscrow)); + bridgeEscrow.approveAll(address(l1Gateway)); + l1Gateway.setArbitrumAddresses(address(inboxMock), routerMock); + l1Gateway.setPaused(false); + vm.stopPrank(); + } +} diff --git a/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol b/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol new file mode 100644 index 000000000..8da02f1ed --- /dev/null +++ b/packages/graph-token/test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IBridge } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IBridge.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; +import { L1GraphTokenGatewaySharedTest } from "./L1GraphTokenGatewayShared.t.sol"; +import { IL1GraphTokenGatewayLike } from "../interfaces/IL1GraphTokenGatewayLike.sol"; +import { IInboxMockLike } from "../interfaces/IArbitrumMocksLike.sol"; + +/// Cases from the "after configuring and unpausing" context +contract L1GraphTokenGatewayTransfersTest is L1GraphTokenGatewaySharedTest { + uint8 internal constant L1_MESSAGE_TYPE_SUBMIT_RETRYABLE_TX = 9; + uint160 internal constant L1_TO_L2_ALIAS_OFFSET = uint160(0x1111000000000000000000000000000000001111); + + bytes internal notEmptyCallHookData = hex"12"; + + function setUp() public override { + super.setUp(); + _configureL1Bridge(); + } + + function _applyL1ToL2Alias(address l1Address) internal pure returns (address) { + unchecked { + return address(uint160(l1Address) + L1_TO_L2_ALIAS_OFFSET); + } + } + + /// Message data the InboxMock builds for the retryable ticket + function _createMsgData(bytes memory callHookData) internal view returns (bytes memory) { + bytes memory outboundData = abi.encodeCall( + ITokenGateway.finalizeInboundTransfer, + (address(grt), tokenSender, l2Receiver, 10 ether, callHookData) + ); + assertEq( + l1Gateway.getOutboundCalldata(address(grt), tokenSender, l2Receiver, 10 ether, callHookData), + outboundData + ); + return + abi.encodePacked( + uint256(uint160(l2GatewayMock)), + uint256(0), + DEFAULT_ETH_VALUE, + MAX_SUBMISSION_COST, + uint256(uint160(_applyL1ToL2Alias(tokenSender))), + uint256(uint160(_applyL1ToL2Alias(tokenSender))), + MAX_GAS, + GAS_PRICE_BID, + outboundData.length, + outboundData + ); + } + + function _testValidOutboundTransfer(address signer, bytes memory data, bytes memory callHookData) internal { + bytes memory msgData = _createMsgData(callHookData); + bytes32 msgDataHash = keccak256(msgData); + // The real bridge would emit the InboxAccs entry that came before this one, but the mock + // emits this one, which lets us validate all the parameters we sent + bytes32 expectedInboxAccsEntry = keccak256( + abi.encodePacked(address(inboxMock), L1_MESSAGE_TYPE_SUBMIT_RETRYABLE_TX, address(l1Gateway), msgDataHash) + ); + // The bridge mock returns an incrementing seqNum starting at 1 + uint256 expectedSeqNum = 1; + + vm.expectEmit(address(bridgeMock)); + emit IBridge.MessageDelivered( + expectedSeqNum, + expectedInboxAccsEntry, + address(inboxMock), + L1_MESSAGE_TYPE_SUBMIT_RETRYABLE_TX, + address(l1Gateway), + msgDataHash + ); + vm.expectEmit(address(inboxMock)); + emit IInboxMockLike.InboxMessageDelivered(1, msgData); + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.DepositInitiated(address(grt), tokenSender, l2Receiver, expectedSeqNum, 10 ether); + vm.prank(signer); + l1Gateway.outboundTransfer{ value: DEFAULT_ETH_VALUE }( + address(grt), + l2Receiver, + 10 ether, + MAX_GAS, + GAS_PRICE_BID, + data + ); + + assertEq(grt.balanceOf(address(bridgeEscrow)), 10 ether); + assertEq(grt.balanceOf(tokenSender), 990 ether); + } + + /// The real outbox would require a proof validating that the tx was initiated by + /// the L2 gateway, but the mock just executes unconditionally + function _executeInbound(address l2Sender, uint256 amount) internal { + bytes memory encodedCalldata = abi.encodeCall( + ITokenGateway.finalizeInboundTransfer, + (address(grt), l2Receiver, tokenSender, amount, abi.encode(uint256(0), bytes(""))) + ); + vm.prank(tokenSender); + outboxMock.executeTransaction( + 0, + new bytes32[](0), + 0, + l2Sender, + address(l1Gateway), + 1337, + block.number, + 133701337, + 0, + encodedCalldata + ); + } + + function _approveAndTransfer() internal { + vm.prank(tokenSender); + grt.approve(address(l1Gateway), 10 ether); + _testValidOutboundTransfer(tokenSender, defaultData, ""); + } + + // -- updateL2MintAllowance -- + + function test_UpdateL2MintAllowance_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(pauseGuardian); + l1Gateway.updateL2MintAllowance(1 ether, block.number); + } + + function test_UpdateL2MintAllowance_RevertWhen_FutureOrCurrentBlock() public { + vm.roll(100); + uint256 issuancePerBlock = 120 ether; + + vm.startPrank(governor); + vm.expectRevert(bytes("BLOCK_MUST_BE_PAST")); + l1Gateway.updateL2MintAllowance(issuancePerBlock, block.number + 1); + + vm.expectRevert(bytes("BLOCK_MUST_BE_PAST")); + l1Gateway.updateL2MintAllowance(issuancePerBlock, block.number); + + uint256 issuanceUpdatedAtBlock = block.number - 1; + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2MintAllowanceUpdated(0, issuancePerBlock, issuanceUpdatedAtBlock); + l1Gateway.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock); + vm.stopPrank(); + } + + function test_UpdateL2MintAllowance_RevertWhen_BlockNotIncrementing() public { + vm.roll(100); + uint256 issuancePerBlock = 120 ether; + uint256 issuanceUpdatedAtBlock = block.number - 1; + + vm.startPrank(governor); + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2MintAllowanceUpdated(0, issuancePerBlock, issuanceUpdatedAtBlock); + l1Gateway.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock); + + vm.expectRevert(bytes("BLOCK_MUST_BE_INCREMENTING")); + l1Gateway.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock); + + vm.expectRevert(bytes("BLOCK_MUST_BE_INCREMENTING")); + l1Gateway.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock - 1); + + vm.roll(block.number + 1); + uint256 nextUpdateBlock = issuanceUpdatedAtBlock + 1; + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2MintAllowanceUpdated( + issuancePerBlock * (nextUpdateBlock - issuanceUpdatedAtBlock), + issuancePerBlock, + nextUpdateBlock + ); + l1Gateway.updateL2MintAllowance(issuancePerBlock, nextUpdateBlock); + vm.stopPrank(); + } + + function test_UpdateL2MintAllowance_AccumulatesUpToSpecifiedBlock() public { + vm.roll(100); + uint256 issuancePerBlock = 120 ether; + uint256 issuanceUpdatedAtBlock = block.number - 3; + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2MintAllowanceUpdated(0, issuancePerBlock, issuanceUpdatedAtBlock); + vm.prank(governor); + l1Gateway.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock); + + assertEq( + l1Gateway.accumulatedL2MintAllowanceAtBlock(block.number), + issuancePerBlock * (block.number - issuanceUpdatedAtBlock) + ); + assertEq(l1Gateway.accumulatedL2MintAllowanceSnapshot(), 0); + assertEq(l1Gateway.l2MintAllowancePerBlock(), issuancePerBlock); + assertEq(l1Gateway.lastL2MintAllowanceUpdateBlock(), issuanceUpdatedAtBlock); + + vm.roll(block.number + 10); + + uint256 newIssuancePerBlock = 200 ether; + uint256 newIssuanceUpdatedAtBlock = block.number - 2; + uint256 expectedAccumulatedSnapshot = issuancePerBlock * (newIssuanceUpdatedAtBlock - issuanceUpdatedAtBlock); + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2MintAllowanceUpdated( + expectedAccumulatedSnapshot, + newIssuancePerBlock, + newIssuanceUpdatedAtBlock + ); + vm.prank(governor); + l1Gateway.updateL2MintAllowance(newIssuancePerBlock, newIssuanceUpdatedAtBlock); + + assertEq( + l1Gateway.accumulatedL2MintAllowanceAtBlock(block.number), + expectedAccumulatedSnapshot + newIssuancePerBlock * (block.number - newIssuanceUpdatedAtBlock) + ); + assertEq(l1Gateway.accumulatedL2MintAllowanceSnapshot(), expectedAccumulatedSnapshot); + assertEq(l1Gateway.l2MintAllowancePerBlock(), newIssuancePerBlock); + assertEq(l1Gateway.lastL2MintAllowanceUpdateBlock(), newIssuanceUpdatedAtBlock); + } + + // -- setL2MintAllowanceParametersManual -- + + function test_SetL2MintAllowanceParametersManual_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(pauseGuardian); + l1Gateway.setL2MintAllowanceParametersManual(0, 1 ether, block.number); + } + + function test_SetL2MintAllowanceParametersManual_RevertWhen_FutureOrCurrentBlock() public { + vm.roll(100); + uint256 issuancePerBlock = 120 ether; + + vm.startPrank(governor); + vm.expectRevert(bytes("BLOCK_MUST_BE_PAST")); + l1Gateway.setL2MintAllowanceParametersManual(0, issuancePerBlock, block.number + 1); + + vm.expectRevert(bytes("BLOCK_MUST_BE_PAST")); + l1Gateway.setL2MintAllowanceParametersManual(0, issuancePerBlock, block.number); + + uint256 issuanceUpdatedAtBlock = block.number - 1; + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2MintAllowanceUpdated(0, issuancePerBlock, issuanceUpdatedAtBlock); + l1Gateway.setL2MintAllowanceParametersManual(0, issuancePerBlock, issuanceUpdatedAtBlock); + vm.stopPrank(); + } + + function test_SetL2MintAllowanceParametersManual_SetsSnapshotManually() public { + vm.roll(100); + uint256 issuancePerBlock = 120 ether; + uint256 issuanceUpdatedAtBlock = block.number - 3; + uint256 snapshotValue = 10 ether; + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2MintAllowanceUpdated(snapshotValue, issuancePerBlock, issuanceUpdatedAtBlock); + vm.prank(governor); + l1Gateway.setL2MintAllowanceParametersManual(snapshotValue, issuancePerBlock, issuanceUpdatedAtBlock); + + assertEq( + l1Gateway.accumulatedL2MintAllowanceAtBlock(block.number), + snapshotValue + issuancePerBlock * (block.number - issuanceUpdatedAtBlock) + ); + assertEq(l1Gateway.accumulatedL2MintAllowanceSnapshot(), snapshotValue); + assertEq(l1Gateway.l2MintAllowancePerBlock(), issuancePerBlock); + assertEq(l1Gateway.lastL2MintAllowanceUpdateBlock(), issuanceUpdatedAtBlock); + + vm.roll(block.number + 10); + + uint256 newIssuancePerBlock = 200 ether; + uint256 newIssuanceUpdatedAtBlock = block.number - 2; + uint256 newSnapshotValue = 10 ether; + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.L2MintAllowanceUpdated( + newSnapshotValue, + newIssuancePerBlock, + newIssuanceUpdatedAtBlock + ); + vm.prank(governor); + l1Gateway.setL2MintAllowanceParametersManual(newSnapshotValue, newIssuancePerBlock, newIssuanceUpdatedAtBlock); + + assertEq( + l1Gateway.accumulatedL2MintAllowanceAtBlock(block.number), + newSnapshotValue + newIssuancePerBlock * (block.number - newIssuanceUpdatedAtBlock) + ); + assertEq(l1Gateway.accumulatedL2MintAllowanceSnapshot(), newSnapshotValue); + assertEq(l1Gateway.l2MintAllowancePerBlock(), newIssuancePerBlock); + assertEq(l1Gateway.lastL2MintAllowanceUpdateBlock(), newIssuanceUpdatedAtBlock); + } + + // -- calculateL2TokenAddress -- + + function test_CalculateL2TokenAddress_ReturnsL2TokenAddress() public view { + assertEq(l1Gateway.calculateL2TokenAddress(address(grt)), l2GRTMock); + } + + function test_CalculateL2TokenAddress_ReturnsZeroForOtherAddress() public view { + assertEq(l1Gateway.calculateL2TokenAddress(tokenSender), address(0)); + } + + // -- outboundTransfer -- + + function test_OutboundTransfer_RevertWhen_WrongToken() public { + vm.expectRevert(bytes("TOKEN_NOT_GRT")); + vm.prank(tokenSender); + l1Gateway.outboundTransfer{ value: DEFAULT_ETH_VALUE }( + tokenSender, + l2Receiver, + 10 ether, + MAX_GAS, + GAS_PRICE_BID, + defaultData + ); + } + + function test_OutboundTransfer_EscrowsTokensAndCreatesRetryableTicket() public { + _approveAndTransfer(); + } + + function test_OutboundTransfer_DecodesSenderFromRouter() public { + vm.prank(tokenSender); + grt.approve(address(l1Gateway), 10 ether); + bytes memory routerEncodedData = abi.encode(tokenSender, defaultData); + _testValidOutboundTransfer(routerMock, routerEncodedData, ""); + } + + function test_OutboundTransfer_RevertWhen_NoSubmissionCost() public { + vm.prank(tokenSender); + grt.approve(address(l1Gateway), 10 ether); + + vm.expectRevert(bytes("NO_SUBMISSION_COST")); + vm.prank(tokenSender); + l1Gateway.outboundTransfer{ value: DEFAULT_ETH_VALUE }( + address(grt), + l2Receiver, + 10 ether, + MAX_GAS, + GAS_PRICE_BID, + abi.encode(uint256(0), bytes("")) + ); + } + + function test_OutboundTransfer_RevertWhen_CallhookSenderNotAllowlisted() public { + vm.prank(tokenSender); + grt.approve(address(l1Gateway), 10 ether); + + vm.expectRevert(bytes("CALL_HOOK_DATA_NOT_ALLOWED")); + vm.prank(tokenSender); + l1Gateway.outboundTransfer{ value: DEFAULT_ETH_VALUE }( + address(grt), + l2Receiver, + 10 ether, + MAX_GAS, + GAS_PRICE_BID, + abi.encode(MAX_SUBMISSION_COST, notEmptyCallHookData) + ); + } + + function test_OutboundTransfer_AllowsCallhookFromAllowlistedSender() public { + // Make the sender a contract so that it can be allowed to send callhooks + vm.etch(tokenSender, hex"1234"); + vm.prank(governor); + l1Gateway.addToCallhookAllowlist(tokenSender); + vm.prank(tokenSender); + grt.approve(address(l1Gateway), 10 ether); + + _testValidOutboundTransfer( + tokenSender, + abi.encode(MAX_SUBMISSION_COST, notEmptyCallHookData), + notEmptyCallHookData + ); + } + + function test_OutboundTransfer_RevertWhen_InsufficientBalance() public { + vm.prank(tokenSender); + grt.approve(address(l1Gateway), 1001 ether); + + vm.expectRevert(bytes("ERC20: transfer amount exceeds balance")); + vm.prank(tokenSender); + l1Gateway.outboundTransfer{ value: DEFAULT_ETH_VALUE }( + address(grt), + l2Receiver, + 1001 ether, + MAX_GAS, + GAS_PRICE_BID, + defaultData + ); + } + + // -- finalizeInboundTransfer -- + + function test_FinalizeInboundTransfer_RevertWhen_NotBridge() public { + vm.expectRevert(bytes("NOT_FROM_BRIDGE")); + vm.prank(tokenSender); + l1Gateway.finalizeInboundTransfer(address(grt), l2Receiver, tokenSender, 10 ether, defaultData); + } + + function test_FinalizeInboundTransfer_RevertWhen_NotFromL2Gateway() public { + vm.expectRevert(bytes("ONLY_COUNTERPART_GATEWAY")); + _executeInbound(l2Receiver, 10 ether); // l2Receiver is not the L2 gateway + } + + function test_FinalizeInboundTransfer_RevertWhen_NoTokensOrAllowance() public { + // Should never happen, but the gateway must still revert + vm.expectRevert(bytes("INVALID_L2_MINT_AMOUNT")); + _executeInbound(l2GatewayMock, 10 ether); + } + + function test_FinalizeInboundTransfer_RevertWhen_RevokedFromEscrow() public { + _approveAndTransfer(); + // The escrow holds 10 GRT, but the gateway can no longer move them + vm.prank(governor); + bridgeEscrow.revokeAll(address(l1Gateway)); + + vm.expectRevert(bytes("ERC20: transfer amount exceeds allowance")); + _executeInbound(l2GatewayMock, 8 ether); + } + + function test_FinalizeInboundTransfer_SendsTokensOutOfEscrow() public { + _approveAndTransfer(); + + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.WithdrawalFinalized(address(grt), l2Receiver, tokenSender, 0, 8 ether); + _executeInbound(l2GatewayMock, 8 ether); + + assertEq(grt.balanceOf(address(bridgeEscrow)), 2 ether); + assertEq(grt.balanceOf(tokenSender), 998 ether); + } + + function test_FinalizeInboundTransfer_MintsUpToL2MintAllowance() public { + _approveAndTransfer(); + + // Start accruing L2 mint allowance at 2 GRT per block + uint256 updateBlock = block.number; + vm.roll(updateBlock + 1); + vm.prank(governor); + l1Gateway.updateL2MintAllowance(2 ether, updateBlock); + vm.roll(updateBlock + 4); + uint256 allowance = 2 ether * (block.number - updateBlock); + assertEq(allowance, 8 ether); + + // The escrow holds 10 GRT + uint256 amount = 10 ether + allowance; + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.TokensMintedFromL2(allowance); + vm.expectEmit(address(l1Gateway)); + emit IL1GraphTokenGatewayLike.WithdrawalFinalized(address(grt), l2Receiver, tokenSender, 0, amount); + _executeInbound(l2GatewayMock, amount); + + assertEq(l1Gateway.totalMintedFromL2(), allowance); + assertEq(l1Gateway.accumulatedL2MintAllowanceAtBlock(block.number), allowance); + assertEq(grt.balanceOf(address(bridgeEscrow)), 0); + assertEq(grt.balanceOf(tokenSender), 990 ether + amount); + } + + function test_FinalizeInboundTransfer_RevertWhen_OverL2MintAllowance() public { + _approveAndTransfer(); + + // Start accruing L2 mint allowance at 2 GRT per block + uint256 updateBlock = block.number; + vm.roll(updateBlock + 1); + vm.prank(governor); + l1Gateway.updateL2MintAllowance(2 ether, updateBlock); + vm.roll(updateBlock + 4); + uint256 allowance = 2 ether * (block.number - updateBlock); + assertEq(allowance, 8 ether); + + // The escrow holds 10 GRT + vm.expectRevert(bytes("INVALID_L2_MINT_AMOUNT")); + _executeInbound(l2GatewayMock, 10 ether + allowance + 0.001 ether); + } +} diff --git a/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayConfig.t.sol b/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayConfig.t.sol new file mode 100644 index 000000000..d5e45bf45 --- /dev/null +++ b/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayConfig.t.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { L2GraphTokenGatewaySharedTest } from "./L2GraphTokenGatewayShared.t.sol"; +import { IL2GraphTokenGatewayLike } from "../interfaces/IL2GraphTokenGatewayLike.sol"; + +/// Cases from the "immediately after deploy" context: the gateway is paused and unconfigured +contract L2GraphTokenGatewayConfigTest is L2GraphTokenGatewaySharedTest { + // -- calculateL2TokenAddress -- + + function test_CalculateL2TokenAddress_ReturnsZeroWhenNotSet() public view { + assertEq(l2Gateway.calculateL2TokenAddress(address(grt)), address(0)); + } + + // -- outboundTransfer -- + + function test_OutboundTransfer_RevertWhen_Paused() public { + vm.expectRevert(bytes("Paused (contract)")); + vm.prank(tokenSender); + l2Gateway.outboundTransfer(address(grt), l1Receiver, 10 ether, defaultData); + } + + // -- finalizeInboundTransfer -- + + function test_FinalizeInboundTransfer_RevertWhen_Paused() public { + vm.expectRevert(bytes("Paused (contract)")); + vm.prank(tokenSender); + l2Gateway.finalizeInboundTransfer(address(grt), tokenSender, l1Receiver, 10 ether, defaultData); + } + + // -- setL2Router -- + + function test_SetL2Router_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l2Gateway.setL2Router(routerMock); + } + + function test_SetL2Router_SetsRouter() public { + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.L2RouterSet(routerMock); + vm.prank(governor); + l2Gateway.setL2Router(routerMock); + + assertEq(l2Gateway.l2Router(), routerMock); + } + + // -- setL1TokenAddress -- + + function test_SetL1TokenAddress_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l2Gateway.setL1TokenAddress(l1GRTMock); + } + + function test_SetL1TokenAddress_SetsL1GRT() public { + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.L1TokenAddressSet(l1GRTMock); + vm.prank(governor); + l2Gateway.setL1TokenAddress(l1GRTMock); + + assertEq(l2Gateway.l1GRT(), l1GRTMock); + } + + // -- setL1CounterpartAddress -- + + function test_SetL1CounterpartAddress_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l2Gateway.setL1CounterpartAddress(l1GatewayMock); + } + + function test_SetL1CounterpartAddress_SetsL1Counterpart() public { + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.L1CounterpartAddressSet(l1GatewayMock); + vm.prank(governor); + l2Gateway.setL1CounterpartAddress(l1GatewayMock); + + assertEq(l2Gateway.l1Counterpart(), l1GatewayMock); + } + + // -- Pausable behavior -- + + function test_PausableBehavior_RevertWhen_NotGovernorOrGuardian() public { + vm.expectRevert(bytes("Only Governor or Guardian")); + vm.prank(tokenSender); + l2Gateway.setPaused(false); + + vm.expectRevert(bytes("Only Governor or Guardian")); + vm.prank(tokenSender); + l2Gateway.setPaused(true); + } + + function test_PausableBehavior_RevertWhen_UnpausingWithStateNotSet() public { + vm.startPrank(governor); + vm.expectRevert(bytes("L2_ROUTER_NOT_SET")); + l2Gateway.setPaused(false); + + l2Gateway.setL2Router(routerMock); + vm.expectRevert(bytes("L1_COUNTERPART_NOT_SET")); + l2Gateway.setPaused(false); + + l2Gateway.setL1CounterpartAddress(l1GatewayMock); + vm.expectRevert(bytes("L1_GRT_NOT_SET")); + l2Gateway.setPaused(false); + vm.stopPrank(); + } + + function test_PausableBehavior_GovernorCanPauseAndUnpause() public { + _configureL2Bridge(); + + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.PauseChanged(true); + vm.prank(governor); + l2Gateway.setPaused(true); + assertTrue(l2Gateway.paused()); + + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.PauseChanged(false); + vm.prank(governor); + l2Gateway.setPaused(false); + assertFalse(l2Gateway.paused()); + } + + // -- setPauseGuardian -- + + function test_SetPauseGuardian_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(tokenSender); + l2Gateway.setPauseGuardian(pauseGuardian); + } + + function test_SetPauseGuardian_SetsPauseGuardian() public { + address currentPauseGuardian = l2Gateway.pauseGuardian(); + + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.NewPauseGuardian(currentPauseGuardian, pauseGuardian); + vm.prank(governor); + l2Gateway.setPauseGuardian(pauseGuardian); + } + + function test_SetPauseGuardian_GuardianCanPauseAndUnpause() public { + _configureL2Bridge(); + vm.prank(governor); + l2Gateway.setPauseGuardian(pauseGuardian); + + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.PauseChanged(true); + vm.prank(pauseGuardian); + l2Gateway.setPaused(true); + assertTrue(l2Gateway.paused()); + + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.PauseChanged(false); + vm.prank(pauseGuardian); + l2Gateway.setPaused(false); + assertFalse(l2Gateway.paused()); + } +} diff --git a/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayShared.t.sol b/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayShared.t.sol new file mode 100644 index 000000000..2e3b67a16 --- /dev/null +++ b/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayShared.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IManaged } from "@graphprotocol/interfaces/contracts/contracts/governance/IManaged.sol"; +import { ProtocolFixture } from "../shared/ProtocolFixture.t.sol"; +import { IL2GraphTokenLike } from "../interfaces/IL2GraphTokenLike.sol"; +import { IL2GraphTokenGatewayLike } from "../interfaces/IL2GraphTokenGatewayLike.sol"; + +abstract contract L2GraphTokenGatewaySharedTest is ProtocolFixture { + string internal constant L2_GRAPH_TOKEN = + string.concat(PKG_ARTIFACTS, "l2/token/L2GraphToken.sol/L2GraphToken.json"); + string internal constant L2_GATEWAY = + string.concat(PKG_ARTIFACTS, "l2/gateway/L2GraphTokenGateway.sol/L2GraphTokenGateway.json"); + string internal constant CALLHOOK_RECEIVER_MOCK = + string.concat(PKG_ARTIFACTS, "tests/CallhookReceiverMock.sol/CallhookReceiverMock.json"); + + address internal constant ARB_SYS = address(0x64); + uint256 internal constant SENDER_TOKENS = 1000 ether; + + IL2GraphTokenLike internal grt; + IL2GraphTokenGatewayLike internal l2Gateway; + address internal callhookReceiverMock; + + address internal tokenSender = makeAddr("tokenSender"); + address internal l1Receiver = makeAddr("l1Receiver"); + address internal l2Receiver = makeAddr("l2Receiver"); + address internal pauseGuardian = makeAddr("pauseGuardian"); + address internal routerMock = makeAddr("routerMock"); + address internal l1GRTMock = makeAddr("l1GRTMock"); + address internal l1GatewayMock = makeAddr("l1GatewayMock"); + + bytes internal defaultData = ""; + + function setUp() public virtual { + vm.warp(1_700_000_000); + _deployProtocolBase(); + + grt = IL2GraphTokenLike( + _deployBehindProxy( + _deployArtifact(L2_GRAPH_TOKEN, ""), + abi.encodeCall(IL2GraphTokenLike.initialize, (governor)) + ) + ); + _registerContract("GraphToken", address(grt)); + + l2Gateway = IL2GraphTokenGatewayLike( + _deployBehindProxy( + _deployArtifact(L2_GATEWAY, ""), + abi.encodeCall(IL2GraphTokenGatewayLike.initialize, (address(controller))) + ) + ); + _registerContract("GraphTokenGateway", address(l2Gateway)); + IManaged(address(l2Gateway)).syncAllContracts(); + + callhookReceiverMock = _deployArtifact(CALLHOOK_RECEIVER_MOCK, ""); + + vm.etch(routerMock, hex"1234"); + vm.etch(l1GRTMock, hex"1234"); + vm.etch(l1GatewayMock, hex"1234"); + + // ArbSys precompile: code so the extcodesize check passes, and sendTxToL1 returns id 1 + vm.etch(ARB_SYS, hex"00"); + vm.mockCall(ARB_SYS, abi.encodeWithSignature("sendTxToL1(address,bytes)"), abi.encode(uint256(1))); + + vm.prank(governor); + grt.mint(tokenSender, SENDER_TOKENS); + } + + /// Wires the gateway and token to the L1 mocks and router, then unpauses the gateway + function _configureL2Bridge() internal { + vm.startPrank(governor); + l2Gateway.setL1TokenAddress(l1GRTMock); + grt.setL1Address(l1GRTMock); + l2Gateway.setL1CounterpartAddress(l1GatewayMock); + l2Gateway.setL2Router(routerMock); + grt.setGateway(address(l2Gateway)); + l2Gateway.setPaused(false); + vm.stopPrank(); + } +} diff --git a/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol b/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol new file mode 100644 index 000000000..726d6d57c --- /dev/null +++ b/packages/graph-token/test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; +import { L2GraphTokenGatewaySharedTest } from "./L2GraphTokenGatewayShared.t.sol"; +import { IL2GraphTokenLike } from "../interfaces/IL2GraphTokenLike.sol"; +import { IL2GraphTokenGatewayLike } from "../interfaces/IL2GraphTokenGatewayLike.sol"; +import { ICallhookReceiverMockLike } from "../interfaces/IArbitrumMocksLike.sol"; +import { NoCallhookReceiver } from "../mocks/NoCallhookReceiver.sol"; + +/// Cases from the "after configuring and unpausing" context +contract L2GraphTokenGatewayTransfersTest is L2GraphTokenGatewaySharedTest { + uint160 internal constant L1_TO_L2_ALIAS_OFFSET = uint160(0x1111000000000000000000000000000000001111); + uint256 internal constant CALLHOOK_FOO = 1337; + uint256 internal constant CALLHOOK_BAR = 42; + + bytes internal defaultDataWithNotEmptyCallHookData = abi.encode(CALLHOOK_FOO, CALLHOOK_BAR); + + function setUp() public override { + super.setUp(); + _configureL2Bridge(); + } + + function _l1GatewayAlias() internal view returns (address) { + unchecked { + return address(uint160(l1GatewayMock) + L1_TO_L2_ALIAS_OFFSET); + } + } + + function _testValidOutboundTransfer(address signer, bytes memory data) internal { + uint256 expectedId = 1; + // Should use the L1 gateway's interface, but both come from ITokenGateway + bytes memory calldataToL1 = abi.encodeCall( + ITokenGateway.finalizeInboundTransfer, + (l1GRTMock, tokenSender, l1Receiver, 10 ether, abi.encode(uint256(0), bytes(""))) + ); + assertEq(l2Gateway.getOutboundCalldata(l1GRTMock, tokenSender, l1Receiver, 10 ether, ""), calldataToL1); + + vm.expectCall(ARB_SYS, abi.encodeWithSignature("sendTxToL1(address,bytes)", l1GatewayMock, calldataToL1)); + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.TxToL1(tokenSender, l1GatewayMock, expectedId, calldataToL1); + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.WithdrawalInitiated(l1GRTMock, tokenSender, l1Receiver, expectedId, 0, 10 ether); + vm.prank(signer); + l2Gateway.outboundTransfer(l1GRTMock, l1Receiver, 10 ether, data); + + assertEq(grt.balanceOf(tokenSender), 990 ether); + } + + /// Finalizes a 10 GRT deposit to `to` as the aliased L1 gateway. With `expectCallhook`, + /// `to` must emit the CallhookReceiverMock event for CALLHOOK_FOO and CALLHOOK_BAR. + function _testValidFinalizeTransfer(bytes memory data, address to, bool expectCallhook) internal { + vm.expectEmit(address(grt)); + emit IL2GraphTokenLike.BridgeMinted(to, 10 ether); + if (expectCallhook) { + vm.expectEmit(to); + emit ICallhookReceiverMockLike.TransferReceived(tokenSender, 10 ether, CALLHOOK_FOO, CALLHOOK_BAR); + } + vm.expectEmit(address(l2Gateway)); + emit IL2GraphTokenGatewayLike.DepositFinalized(l1GRTMock, tokenSender, to, 10 ether); + vm.prank(_l1GatewayAlias()); + l2Gateway.finalizeInboundTransfer(l1GRTMock, tokenSender, to, 10 ether, data); + + // Unchanged + assertEq(grt.balanceOf(tokenSender), 1000 ether); + // 10 newly minted GRT + assertEq(grt.balanceOf(to), 10 ether); + } + + // -- calculateL2TokenAddress -- + + function test_CalculateL2TokenAddress_ReturnsL2TokenAddress() public view { + assertEq(l2Gateway.calculateL2TokenAddress(l1GRTMock), address(grt)); + } + + function test_CalculateL2TokenAddress_ReturnsZeroForOtherAddress() public view { + assertEq(l2Gateway.calculateL2TokenAddress(tokenSender), address(0)); + } + + // -- outboundTransfer -- + + function test_OutboundTransfer_RevertWhen_WrongToken() public { + vm.expectRevert(bytes("TOKEN_NOT_GRT")); + vm.prank(tokenSender); + l2Gateway.outboundTransfer(tokenSender, l1Receiver, 10 ether, defaultData); + } + + function test_OutboundTransfer_BurnsTokensAndTriggersL1Call() public { + vm.prank(tokenSender); + grt.approve(address(l2Gateway), 10 ether); + _testValidOutboundTransfer(tokenSender, defaultData); + } + + function test_OutboundTransfer_DecodesSenderFromRouter() public { + vm.prank(tokenSender); + grt.approve(address(l2Gateway), 10 ether); + bytes memory routerEncodedData = abi.encode(tokenSender, defaultData); + _testValidOutboundTransfer(routerMock, routerEncodedData); + } + + function test_OutboundTransfer_RevertWhen_NonEmptyCallhookData() public { + vm.prank(tokenSender); + grt.approve(address(l2Gateway), 10 ether); + + vm.expectRevert(bytes("CALL_HOOK_DATA_NOT_ALLOWED")); + vm.prank(tokenSender); + l2Gateway.outboundTransfer(l1GRTMock, l1Receiver, 10 ether, defaultDataWithNotEmptyCallHookData); + } + + function test_OutboundTransfer_RevertWhen_InsufficientBalance() public { + vm.prank(tokenSender); + grt.approve(address(l2Gateway), 1001 ether); + + vm.expectRevert(bytes("ERC20: burn amount exceeds balance")); + vm.prank(tokenSender); + l2Gateway.outboundTransfer(l1GRTMock, l1Receiver, 1001 ether, defaultData); + } + + // -- finalizeInboundTransfer -- + + function test_FinalizeInboundTransfer_RevertWhen_NotGateway() public { + vm.expectRevert(bytes("ONLY_COUNTERPART_GATEWAY")); + vm.prank(tokenSender); + l2Gateway.finalizeInboundTransfer(l1GRTMock, tokenSender, l2Receiver, 10 ether, defaultData); + } + + function test_FinalizeInboundTransfer_RevertWhen_GatewayWithoutL2Alias() public { + vm.expectRevert(bytes("ONLY_COUNTERPART_GATEWAY")); + vm.prank(l1GatewayMock); + l2Gateway.finalizeInboundTransfer(l1GRTMock, tokenSender, l2Receiver, 10 ether, defaultData); + } + + function test_FinalizeInboundTransfer_MintsWhenCalledByAliasedGateway() public { + _testValidFinalizeTransfer(defaultData, l2Receiver, false); + } + + function test_FinalizeInboundTransfer_CallsCallhookWithCalldata() public { + _testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData, callhookReceiverMock, true); + } + + function test_FinalizeInboundTransfer_RevertWhen_CallhookReverts() public { + // A zero foo makes CallhookReceiverMock revert + bytes memory callHookData = abi.encode(uint256(0), CALLHOOK_BAR); + + vm.expectRevert(bytes("FOO_IS_ZERO")); + vm.prank(_l1GatewayAlias()); + l2Gateway.finalizeInboundTransfer(l1GRTMock, tokenSender, callhookReceiverMock, 10 ether, callHookData); + } + + function test_FinalizeInboundTransfer_RevertWhen_ReceiverHasNoCallhook() public { + address receiver = address(new NoCallhookReceiver()); + bytes memory callHookData = abi.encode(uint256(0)); + + // The revert reason depends on the environment + vm.expectRevert(); + vm.prank(_l1GatewayAlias()); + l2Gateway.finalizeInboundTransfer(l1GRTMock, tokenSender, receiver, 10 ether, callHookData); + } +} diff --git a/packages/graph-token/test/unit/interfaces/IArbitrumMocksLike.sol b/packages/graph-token/test/unit/interfaces/IArbitrumMocksLike.sol new file mode 100644 index 000000000..e9a81c330 --- /dev/null +++ b/packages/graph-token/test/unit/interfaces/IArbitrumMocksLike.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface IBridgeMockLike { + function setInbox(address inbox, bool enabled) external; + + function setOutbox(address outbox, bool enabled) external; +} + +interface IInboxMockLike { + event InboxMessageDelivered(uint256 indexed messageNum, bytes data); + + function setBridge(address bridge) external; +} + +interface IOutboxMockLike { + function setBridge(address bridge) external; + + function executeTransaction( + uint256 batchNum, + bytes32[] calldata proof, + uint256 index, + address l2Sender, + address destAddr, + uint256 l2Block, + uint256 l1Block, + uint256 l2Timestamp, + uint256 amount, + bytes calldata calldataForL1 + ) external; +} + +interface ICallhookReceiverMockLike { + event TransferReceived(address from, uint256 amount, uint256 foo, uint256 bar); +} diff --git a/packages/graph-token/test/unit/interfaces/IL1GraphTokenGatewayLike.sol b/packages/graph-token/test/unit/interfaces/IL1GraphTokenGatewayLike.sol new file mode 100644 index 000000000..659100395 --- /dev/null +++ b/packages/graph-token/test/unit/interfaces/IL1GraphTokenGatewayLike.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; + +interface IL1GraphTokenGatewayLike is ITokenGateway { + event DepositInitiated( + address l1Token, + address indexed from, + address indexed to, + uint256 indexed sequenceNumber, + uint256 amount + ); + event WithdrawalFinalized( + address l1Token, + address indexed from, + address indexed to, + uint256 indexed exitNum, + uint256 amount + ); + event ArbitrumAddressesSet(address inbox, address l1Router); + event L2TokenAddressSet(address l2GRT); + event L2CounterpartAddressSet(address l2Counterpart); + event EscrowAddressSet(address escrow); + event AddedToCallhookAllowlist(address newAllowlisted); + event RemovedFromCallhookAllowlist(address notAllowlisted); + event L2MintAllowanceUpdated( + uint256 accumulatedL2MintAllowanceSnapshot, + uint256 l2MintAllowancePerBlock, + uint256 lastL2MintAllowanceUpdateBlock + ); + event TokensMintedFromL2(uint256 amount); + event TxToL2(address indexed _from, address indexed _to, uint256 indexed _seqNum, bytes _data); + event PauseChanged(bool isPaused); + event NewPauseGuardian(address indexed oldPauseGuardian, address indexed pauseGuardian); + + function initialize(address controller) external; + + function setArbitrumAddresses(address inbox, address router) external; + + function setL2TokenAddress(address l2GRT) external; + + function setL2CounterpartAddress(address l2Counterpart) external; + + function setEscrowAddress(address escrow) external; + + function addToCallhookAllowlist(address newAllowlisted) external; + + function removeFromCallhookAllowlist(address notAllowlisted) external; + + function callhookAllowlist(address account) external view returns (bool); + + function updateL2MintAllowance(uint256 l2IssuancePerBlock, uint256 updateBlockNum) external; + + function setL2MintAllowanceParametersManual( + uint256 accumulatedAllowance, + uint256 l2IssuancePerBlock, + uint256 lastUpdateBlock + ) external; + + function accumulatedL2MintAllowanceAtBlock(uint256 blockNum) external view returns (uint256); + + function accumulatedL2MintAllowanceSnapshot() external view returns (uint256); + + function l2MintAllowancePerBlock() external view returns (uint256); + + function lastL2MintAllowanceUpdateBlock() external view returns (uint256); + + function totalMintedFromL2() external view returns (uint256); + + function l2GRT() external view returns (address); + + function inbox() external view returns (address); + + function l1Router() external view returns (address); + + function l2Counterpart() external view returns (address); + + function escrow() external view returns (address); + + function counterpartGateway() external view returns (address); + + function getOutboundCalldata( + address l1Token, + address from, + address to, + uint256 amount, + bytes memory data + ) external pure returns (bytes memory); + + function setPaused(bool newPaused) external; + + function paused() external view returns (bool); + + function setPauseGuardian(address newPauseGuardian) external; + + function pauseGuardian() external view returns (address); +} diff --git a/packages/graph-token/test/unit/interfaces/IL2GraphTokenGatewayLike.sol b/packages/graph-token/test/unit/interfaces/IL2GraphTokenGatewayLike.sol new file mode 100644 index 000000000..290cdb570 --- /dev/null +++ b/packages/graph-token/test/unit/interfaces/IL2GraphTokenGatewayLike.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; + +interface IL2GraphTokenGatewayLike is ITokenGateway { + event DepositFinalized(address indexed l1Token, address indexed from, address indexed to, uint256 amount); + event WithdrawalInitiated( + address l1Token, + address indexed from, + address indexed to, + uint256 indexed l2ToL1Id, + uint256 exitNum, + uint256 amount + ); + event L2RouterSet(address l2Router); + event L1TokenAddressSet(address l1GRT); + event L1CounterpartAddressSet(address l1Counterpart); + event TxToL1(address indexed _from, address indexed _to, uint256 indexed _id, bytes _data); + event PauseChanged(bool isPaused); + event NewPauseGuardian(address indexed oldPauseGuardian, address indexed pauseGuardian); + + function initialize(address controller) external; + + function setL2Router(address l2Router) external; + + function setL1TokenAddress(address l1GRT) external; + + function setL1CounterpartAddress(address l1Counterpart) external; + + function l2Router() external view returns (address); + + function l1GRT() external view returns (address); + + function l1Counterpart() external view returns (address); + + function outboundTransfer( + address l1Token, + address to, + uint256 amount, + bytes calldata data + ) external returns (bytes memory); + + function getOutboundCalldata( + address token, + address from, + address to, + uint256 amount, + bytes memory data + ) external pure returns (bytes memory); + + function setPaused(bool newPaused) external; + + function paused() external view returns (bool); + + function setPauseGuardian(address newPauseGuardian) external; + + function pauseGuardian() external view returns (address); +} diff --git a/packages/graph-token/test/unit/mocks/NoCallhookReceiver.sol b/packages/graph-token/test/unit/mocks/NoCallhookReceiver.sol new file mode 100644 index 000000000..7605f42a9 --- /dev/null +++ b/packages/graph-token/test/unit/mocks/NoCallhookReceiver.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/// Has no onTokenTransfer and no fallback, so any call to it reverts +contract NoCallhookReceiver {} diff --git a/scripts/contracts-migration/test-mapping/t2b.md b/scripts/contracts-migration/test-mapping/t2b.md new file mode 100644 index 000000000..5ecd25bed --- /dev/null +++ b/scripts/contracts-migration/test-mapping/t2b.md @@ -0,0 +1,86 @@ +# T2b test mapping: L1 and L2 GraphTokenGateway + +New tests are in `packages/graph-token`. Each old file is split by its top-level context: `> immediately after deploy` goes to `*Config.t.sol` and `> after configuring and unpausing` to `*Transfers.t.sol`; shared setup is in `*Shared.t.sol`. `packages/contracts-test/tests/unit/l2/l2ArbitrumMessengerMock.ts` is also deleted: it has no cases and nothing imports it. + +| Old file | Old test (describe › it) | New test (file::function) | Note | +| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › calculateL2TokenAddress › should return address zero as it was not set | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_CalculateL2TokenAddress_ReturnsZeroWhenNotSet | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › outboundTransfer › reverts because it is paused | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_OutboundTransfer_RevertWhen_Paused | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › finalizeInboundTransfer › revert because it is paused | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_FinalizeInboundTransfer_RevertWhen_Paused | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setArbitrumAddresses › is not callable by addreses that are not the governor | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetArbitrumAddresses_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setArbitrumAddresses › rejects setting an EOA as router or inbox | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetArbitrumAddresses_RevertWhen_NotContract | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setArbitrumAddresses › sets inbox and router address | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetArbitrumAddresses_SetsInboxAndRouter | Router is `makeAddr` + `vm.etch` | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setL2TokenAddress › is not callable by addreses that are not the governor | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetL2TokenAddress_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setL2TokenAddress › sets l2GRT | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetL2TokenAddress_SetsL2GRT | L2 token is `makeAddr` + `vm.etch` | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setL2CounterpartAddress › is not callable by addreses that are not the governor | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetL2CounterpartAddress_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setL2CounterpartAddress › sets l2Counterpart which can be queried with counterpartGateway() | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetL2CounterpartAddress_SetsL2Counterpart | L2 gateway is `makeAddr` + `vm.etch` | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setEscrowAddress › is not callable by addreses that are not the governor | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetEscrowAddress_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › setEscrowAddress › sets escrow | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetEscrowAddress_SetsEscrow | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › addToCallhookAllowlist › is not callable by addreses that are not the governor | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_AddToCallhookAllowlist_RevertWhen_NotGovernor | Controller replaces RewardsManager as the allowlisted contract | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › addToCallhookAllowlist › rejects adding an EOA to the callhook allowlist | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_AddToCallhookAllowlist_RevertWhen_NotContract | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › addToCallhookAllowlist › adds an address to the callhook allowlist | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_AddToCallhookAllowlist_AddsAddress | Controller replaces RewardsManager as the allowlisted contract | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › removeFromCallhookAllowlist › is not callable by addreses that are not the governor | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_RemoveFromCallhookAllowlist_RevertWhen_NotGovernor | Controller replaces RewardsManager as the allowlisted contract | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › removeFromCallhookAllowlist › removes an address from the callhook allowlist | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_RemoveFromCallhookAllowlist_RemovesAddress | Controller replaces RewardsManager as the allowlisted contract | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › Pausable behavior › cannot be paused or unpaused by someone other than governor or pauseGuardian | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_PausableBehavior_RevertWhen_NotGovernorOrGuardian | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › Pausable behavior › cannot be unpaused if some state variables are not set | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_PausableBehavior_RevertWhen_UnpausingWithStateNotSet | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › Pausable behavior › can be paused and unpaused by the governor | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_PausableBehavior_GovernorCanPauseAndUnpause | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › Pausable behavior › setPauseGuardian › cannot be called by someone other than governor | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetPauseGuardian_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › Pausable behavior › setPauseGuardian › sets a new pause guardian | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetPauseGuardian_SetsPauseGuardian | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > immediately after deploy › Pausable behavior › setPauseGuardian › allows a pause guardian to pause and unpause | test/unit/gateway/L1GraphTokenGatewayConfig.t.sol::test_SetPauseGuardian_GuardianCanPauseAndUnpause | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › updateL2MintAllowance › rejects calls that are not from the governor | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_UpdateL2MintAllowance_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › updateL2MintAllowance › does not allow using a future or current block number | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_UpdateL2MintAllowance_RevertWhen_FutureOrCurrentBlock | Blocks relative to `block.number` after `vm.roll(100)` | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › updateL2MintAllowance › does not allow using a block number lower than or equal to the previous one | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_UpdateL2MintAllowance_RevertWhen_BlockNotIncrementing | Explicit `vm.roll` before the final update; snapshot computed from the rolls | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › updateL2MintAllowance › updates the snapshot and issuance to follow a new linear function, accumulating up to the specified block | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_UpdateL2MintAllowance_AccumulatesUpToSpecifiedBlock | `helpers.mine(10)` becomes `vm.roll`; expected allowances computed from the rolls | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › setL2MintAllowanceParametersManual › rejects calls that are not from the governor | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_SetL2MintAllowanceParametersManual_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › setL2MintAllowanceParametersManual › does not allow using a future or current block number | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_SetL2MintAllowanceParametersManual_RevertWhen_FutureOrCurrentBlock | Blocks relative to `block.number` after `vm.roll(100)` | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › setL2MintAllowanceParametersManual › updates the snapshot and issuance to follow a new linear function, manually setting the snapshot value | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_SetL2MintAllowanceParametersManual_SetsSnapshotManually | `helpers.mine(10)` becomes `vm.roll`; expected allowances computed from the rolls | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › calculateL2TokenAddress › returns the L2 token address | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_CalculateL2TokenAddress_ReturnsL2TokenAddress | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › calculateL2TokenAddress › returns the zero address if the input is any other address | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_CalculateL2TokenAddress_ReturnsZeroForOtherAddress | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › outboundTransfer › reverts when called with the wrong token address | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_RevertWhen_WrongToken | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › outboundTransfer › puts tokens in escrow and creates a retryable ticket | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_EscrowsTokensAndCreatesRetryableTicket | Also asserts `getOutboundCalldata` matches the encoded L2 calldata | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › outboundTransfer › decodes the sender address from messages sent by the router | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_DecodesSenderFromRouter | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › outboundTransfer › reverts when called with no submission cost | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_RevertWhen_NoSubmissionCost | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › outboundTransfer › reverts when called with nonempty calldata, if the sender is not allowlisted | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_RevertWhen_CallhookSenderNotAllowlisted | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › outboundTransfer › allows sending nonempty calldata, if the sender is allowlisted | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_AllowsCallhookFromAllowlistedSender | `helpers.setCode` becomes `vm.etch` | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › outboundTransfer › reverts when the sender does not have enough GRT | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_RevertWhen_InsufficientBalance | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts when called by an account that is not the bridge | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_NotBridge | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts when called by the bridge, but the tx was not started by the L2 gateway | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_NotFromL2Gateway | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts if the gateway does not have tokens or allowance | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_NoTokensOrAllowance | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts if the gateway is revoked from escrow | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_RevokedFromEscrow | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › sends tokens out of escrow | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_SendsTokensOutOfEscrow | | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › mints tokens up to the L2 mint allowance | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_MintsUpToL2MintAllowance | Update at B, roll to B + 4: 8 GRT allowance at 2 GRT per block, computed from the rolls | +| packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts | L1GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts if the amount to mint is over the allowance | test/unit/gateway/L1GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_OverL2MintAllowance | Same rolls as the case above | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › calculateL2TokenAddress › should return the zero address | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_CalculateL2TokenAddress_ReturnsZeroWhenNotSet | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › outboundTransfer › reverts because it is paused | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_OutboundTransfer_RevertWhen_Paused | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › finalizeInboundTransfer › revert because it is paused | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_FinalizeInboundTransfer_RevertWhen_Paused | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › setL2Router › is not callable by addreses that are not the governor | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetL2Router_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › setL2Router › sets router address | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetL2Router_SetsRouter | Router is `makeAddr` + `vm.etch` | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › setL1TokenAddress › is not callable by addreses that are not the governor | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetL1TokenAddress_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › setL1TokenAddress › sets l2GRT | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetL1TokenAddress_SetsL1GRT | L1 token is `makeAddr` + `vm.etch` | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › setL1CounterpartAddress › is not callable by addreses that are not the governor | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetL1CounterpartAddress_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › setL1CounterpartAddress › sets L1Counterpart | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetL1CounterpartAddress_SetsL1Counterpart | L1 gateway is `makeAddr` + `vm.etch` | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › Pausable behavior › cannot be paused or unpaused by someone other than governor or pauseGuardian | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_PausableBehavior_RevertWhen_NotGovernorOrGuardian | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › Pausable behavior › cannot be paused if some state variables are not set | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_PausableBehavior_RevertWhen_UnpausingWithStateNotSet | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › Pausable behavior › can be paused and unpaused by the governor | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_PausableBehavior_GovernorCanPauseAndUnpause | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › Pausable behavior › setPauseGuardian › cannot be called by someone other than governor | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetPauseGuardian_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › Pausable behavior › setPauseGuardian › sets a new pause guardian | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetPauseGuardian_SetsPauseGuardian | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > immediately after deploy › Pausable behavior › setPauseGuardian › allows a pause guardian to pause and unpause | test/unit/gateway/L2GraphTokenGatewayConfig.t.sol::test_SetPauseGuardian_GuardianCanPauseAndUnpause | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › calculateL2TokenAddress › returns the L2 token address | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_CalculateL2TokenAddress_ReturnsL2TokenAddress | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › calculateL2TokenAddress › returns the zero address if the input is any other address | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_CalculateL2TokenAddress_ReturnsZeroForOtherAddress | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › outboundTransfer › reverts when called with the wrong token address | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_RevertWhen_WrongToken | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › outboundTransfer › burns tokens and triggers an L1 call | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_BurnsTokensAndTriggersL1Call | ArbSys faked with `vm.etch` + `vm.mockCall`, checked with `vm.expectCall`; also asserts `getOutboundCalldata`. No longer skipped under coverage | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › outboundTransfer › decodes the sender address from messages sent by the router | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_DecodesSenderFromRouter | As above | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › outboundTransfer › reverts when called with nonempty calldata | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_RevertWhen_NonEmptyCallhookData | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › outboundTransfer › reverts when the sender does not have enough GRT | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_OutboundTransfer_RevertWhen_InsufficientBalance | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts when called by an account that is not the gateway | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_NotGateway | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts when called by an account that is the gateway but without the L2 alias | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_GatewayWithoutL2Alias | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › mints and sends tokens when called by the aliased gateway | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_MintsWhenCalledByAliasedGateway | Aliased gateway pranked with an unchecked add of the alias offset | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › calls a callhook if the transfer includes calldata | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_CallsCallhookWithCalldata | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts if a callhook reverts | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_CallhookReverts | | +| packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts | L2GraphTokenGateway › > after configuring and unpausing › finalizeInboundTransfer › reverts if trying to call a callhook in a contract that does not implement onTokenTransfer | test/unit/gateway/L2GraphTokenGatewayTransfers.t.sol::test_FinalizeInboundTransfer_RevertWhen_ReceiverHasNoCallhook | `NoCallhookReceiver` replaces RewardsManager; bare `vm.expectRevert()` | + +## Coverage + +MISSING initialize: `L1GraphTokenGateway.initialize` and `L2GraphTokenGateway.initialize` are called by the fixture through `abi.encodeCall` in the proxy init data (`acceptProxyAndCall`), not by a `.initialize(` call. Every test runs it, and it must succeed for the proxy to work. The other listed external/public functions are called directly. + +Additions with no old test: none. `L2GraphTokenGateway.getOutboundCalldata` is not in the baseline (the old outbound cases were skipped under coverage); the two valid outbound helpers (L1 and L2) assert `getOutboundCalldata` against the expected encoding, so it is called directly.