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/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); + } +} 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/`.