From 80d26fdc95aec0725ff841c70b26e5ae29bca3f5 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 23 Oct 2026 16:25:08 -0300 Subject: [PATCH 1/2] test: add ManagedMock for the graph-core Foundry tests --- .../contracts/contracts/tests/ManagedMock.sol | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 packages/contracts/contracts/tests/ManagedMock.sol diff --git a/packages/contracts/contracts/tests/ManagedMock.sol b/packages/contracts/contracts/tests/ManagedMock.sol new file mode 100644 index 000000000..3520236cd --- /dev/null +++ b/packages/contracts/contracts/tests/ManagedMock.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +// solhint-disable use-natspec + +import { Managed } from "../governance/Managed.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; + +contract ManagedMock is Managed, GraphUpgradeable { + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + } + + function checkNotPartialPaused() external view notPartialPaused {} + + function checkNotPaused() external view notPaused {} + + function checkOnlyGovernor() external view onlyGovernor {} + + /// curation, epochManager, rewardsManager, staking, graphToken, graphTokenGateway, gns — in that order + function resolveAll() external view returns (address[7] memory) { + return + [ + address(curation()), + address(epochManager()), + address(rewardsManager()), + address(staking()), + address(graphToken()), + address(graphTokenGateway()), + address(gns()) + ]; + } +} From e7d309a199e69dc108b3ecdfc9cf20cd524b5288 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 23 Oct 2026 16:25:08 -0300 Subject: [PATCH 2/2] test(graph-core): port governance and proxy tests to Foundry --- .../tests/unit/governance/controller.test.ts | 134 ------------- .../tests/unit/governance/governed.test.ts | 45 ----- .../tests/unit/governance/pausing.test.ts | 110 ----------- .../tests/unit/upgrade/admin.test.ts | 180 ------------------ packages/graph-core/foundry.toml | 22 +++ packages/graph-core/package.json | 19 ++ .../test/unit/governance/Controller.t.sol | 119 ++++++++++++ .../test/unit/governance/Governed.t.sol | 53 ++++++ .../test/unit/governance/Managed.t.sol | 74 +++++++ .../test/unit/governance/Pausing.t.sol | 99 ++++++++++ .../test/unit/interfaces/IControllerLike.sol | 9 + .../test/unit/interfaces/IManagedMock.sol | 22 +++ .../graph-core/test/unit/shared/Fixture.t.sol | 39 ++++ .../test/unit/shared/GraphCoreTest.t.sol | 23 +++ .../test/unit/shared/ProtocolFixture.t.sol | 66 +++++++ .../test/unit/upgrades/GraphProxy.t.sol | 52 +++++ .../test/unit/upgrades/GraphProxyAdmin.t.sol | 142 ++++++++++++++ pnpm-lock.yaml | 12 ++ .../contracts-migration/test-mapping/t1.md | 46 +++++ 19 files changed, 797 insertions(+), 469 deletions(-) delete mode 100644 packages/contracts-test/tests/unit/governance/controller.test.ts delete mode 100644 packages/contracts-test/tests/unit/governance/governed.test.ts delete mode 100644 packages/contracts-test/tests/unit/governance/pausing.test.ts delete mode 100644 packages/contracts-test/tests/unit/upgrade/admin.test.ts create mode 100644 packages/graph-core/foundry.toml create mode 100644 packages/graph-core/package.json create mode 100644 packages/graph-core/test/unit/governance/Controller.t.sol create mode 100644 packages/graph-core/test/unit/governance/Governed.t.sol create mode 100644 packages/graph-core/test/unit/governance/Managed.t.sol create mode 100644 packages/graph-core/test/unit/governance/Pausing.t.sol create mode 100644 packages/graph-core/test/unit/interfaces/IControllerLike.sol create mode 100644 packages/graph-core/test/unit/interfaces/IManagedMock.sol create mode 100644 packages/graph-core/test/unit/shared/Fixture.t.sol create mode 100644 packages/graph-core/test/unit/shared/GraphCoreTest.t.sol create mode 100644 packages/graph-core/test/unit/shared/ProtocolFixture.t.sol create mode 100644 packages/graph-core/test/unit/upgrades/GraphProxy.t.sol create mode 100644 packages/graph-core/test/unit/upgrades/GraphProxyAdmin.t.sol create mode 100644 scripts/contracts-migration/test-mapping/t1.md diff --git a/packages/contracts-test/tests/unit/governance/controller.test.ts b/packages/contracts-test/tests/unit/governance/controller.test.ts deleted file mode 100644 index 06d5b1563..000000000 --- a/packages/contracts-test/tests/unit/governance/controller.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Controller } from '@graphprotocol/contracts' -import { EpochManager } from '@graphprotocol/contracts' -import { GraphNetworkContracts } from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { constants, utils } from 'ethers' -import hre from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' - -const { AddressZero } = constants - -describe('Managed', () => { - const graph = hre.graph() - let me: SignerWithAddress - let governor: SignerWithAddress - let mockController: SignerWithAddress - let newMockEpochManager: SignerWithAddress - - let fixture: NetworkFixture - - let contracts: GraphNetworkContracts - let epochManager: EpochManager - let controller: Controller - - before(async function () { - ;[me, mockController, newMockEpochManager] = await graph.getTestAccounts() - ;({ governor } = await graph.getNamedAccounts()) - // We just run the fixures to set up a contract with Managed, as this - // is cleaner and easier for us to test - fixture = new NetworkFixture(graph.provider) - contracts = await fixture.load(governor) - epochManager = contracts.EpochManager as EpochManager - controller = contracts.Controller as Controller - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('setContractProxy()', function () { - it('should set contract proxy and test get contract proxy', async function () { - // Set right in the constructor - expect(await epochManager.controller()).eq(controller.address) - - // Test the controller - const id = utils.id('EpochManager') - const tx = controller.connect(governor).setContractProxy(id, newMockEpochManager.address) - await expect(tx).emit(controller, 'SetContractProxy').withArgs(id, newMockEpochManager.address) - expect(await controller.getContractProxy(id)).eq(newMockEpochManager.address) - }) - - it('reject set contract proxy to address zero', async function () { - const id = utils.id('EpochManager') - const tx = controller.connect(governor).setContractProxy(id, AddressZero) - await expect(tx).revertedWith('Contract address must be set') - }) - - it('reject set contract proxy if not governor', async function () { - const id = utils.id('EpochManager') - const tx = controller.connect(me).setContractProxy(id, newMockEpochManager.address) - await expect(tx).revertedWith('Only Governor can call') - }) - }) - - describe('unsetContractProxy()', function () { - it('should unset contract proxy', async function () { - // Set contract - const id = utils.id('EpochManager') - await controller.connect(governor).setContractProxy(id, newMockEpochManager.address) - expect(await controller.getContractProxy(id)).eq(newMockEpochManager.address) - - // Unset contract - const tx = controller.connect(governor).unsetContractProxy(id) - await expect(tx).emit(controller, 'SetContractProxy').withArgs(id, AddressZero) - expect(await controller.getContractProxy(id)).eq(AddressZero) - }) - - it('reject to call if not authorized caller', async function () { - // Unset contract - const id = utils.id('EpochManager') - const tx = controller.connect(me).unsetContractProxy(id) - await expect(tx).revertedWith('Only Governor can call') - }) - }) - - describe('updateController()', function () { - it('should update controller on a manager', async function () { - const tx = controller.connect(governor).updateController(utils.id('EpochManager'), mockController.address) - await expect(tx).emit(epochManager, 'SetController').withArgs(mockController.address) - expect(await epochManager.controller()).eq(mockController.address) - }) - - it('should fail updating controller when not called by governor', async function () { - const tx = controller.connect(me).updateController(utils.id('EpochManager'), mockController.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('reject update controller to address zero', async function () { - const tx = controller.connect(governor).updateController(utils.id('EpochManager'), AddressZero) - await expect(tx).revertedWith('Controller must be set') - }) - }) - - describe('setController()', function () { - it('should fail setting controller when not called from Controller', async function () { - const tx = epochManager.connect(me).setController(mockController.address) - await expect(tx).revertedWith('Caller must be Controller') - }) - }) - - describe('setPauseGuardian()', function () { - it('should set the pause guardian', async function () { - const tx = controller.connect(governor).setPauseGuardian(me.address) - const currentPauseGuardian = await controller.pauseGuardian() - await expect(tx).emit(controller, 'NewPauseGuardian').withArgs(currentPauseGuardian, me.address) - expect(await controller.pauseGuardian()).eq(me.address) - }) - - it('reject to call if not authorized caller', async function () { - const tx = controller.connect(me).setPauseGuardian(me.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('reject set pause guardian to address zero', async function () { - const tx = controller.connect(governor).setPauseGuardian(AddressZero) - await expect(tx).revertedWith('PauseGuardian must be set') - }) - }) -}) diff --git a/packages/contracts-test/tests/unit/governance/governed.test.ts b/packages/contracts-test/tests/unit/governance/governed.test.ts deleted file mode 100644 index 0fd617b1e..000000000 --- a/packages/contracts-test/tests/unit/governance/governed.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import '@nomiclabs/hardhat-ethers' - -import { Governed } from '@graphprotocol/contracts' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' - -const { ethers } = hre -const { AddressZero } = ethers.constants - -describe('Governed', () => { - const graph = hre.graph() - let me: SignerWithAddress - let governor: SignerWithAddress - - let governed: Governed - - beforeEach(async function () { - ;[me, governor] = await graph.getTestAccounts() - - const factory = await ethers.getContractFactory('contracts/tests/GovernedMock.sol:GovernedMock') - governed = (await factory.connect(governor).deploy()) as Governed - }) - - it('should reject transfer if not allowed', async function () { - const tx = governed.connect(me).transferOwnership(me.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('should transfer and accept', async function () { - // Transfer ownership - const tx1 = governed.connect(governor).transferOwnership(me.address) - await expect(tx1).emit(governed, 'NewPendingOwnership').withArgs(AddressZero, me.address) - - // Reject accept if not the pending governor - await expect(governed.connect(governor).acceptOwnership()).revertedWith('Caller must be pending governor') - - // Accept ownership - const tx2 = governed.connect(me).acceptOwnership() - await expect(tx2).emit(governed, 'NewOwnership').withArgs(governor.address, me.address) - - // Clean pending governor - expect(await governed.pendingGovernor()).eq(AddressZero) - }) -}) diff --git a/packages/contracts-test/tests/unit/governance/pausing.test.ts b/packages/contracts-test/tests/unit/governance/pausing.test.ts deleted file mode 100644 index 0e2f0d912..000000000 --- a/packages/contracts-test/tests/unit/governance/pausing.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Controller } from '@graphprotocol/contracts' -import { IStaking } from '@graphprotocol/contracts' -import { GraphNetworkContracts, toGRT } from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' - -describe('Pausing', () => { - const graph = hre.graph() - let me: SignerWithAddress - let governor: SignerWithAddress - let guardian: SignerWithAddress - - let fixture: NetworkFixture - - let contracts: GraphNetworkContracts - let staking: IStaking - let controller: Controller - - const setPartialPause = async (account: SignerWithAddress, setValue: boolean) => { - const tx = controller.connect(account).setPartialPaused(setValue) - await expect(tx).emit(controller, 'PartialPauseChanged').withArgs(setValue) - expect(await controller.partialPaused()).eq(setValue) - } - const setPause = async (account: SignerWithAddress, setValue: boolean) => { - const tx = controller.connect(account).setPaused(setValue) - await expect(tx).emit(controller, 'PauseChanged').withArgs(setValue) - expect(await controller.paused()).eq(setValue) - } - before(async function () { - ;[me] = await graph.getTestAccounts() - ;({ governor, pauseGuardian: guardian } = await graph.getNamedAccounts()) - fixture = new NetworkFixture(graph.provider) - contracts = await fixture.load(governor) - staking = contracts.Staking as IStaking - controller = contracts.Controller as Controller - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - it('should set pause guardian', async function () { - const currentGuardian = await controller.pauseGuardian() - expect(await controller.pauseGuardian()).eq(currentGuardian) - const tx = controller.connect(governor).setPauseGuardian(guardian.address) - await expect(tx).emit(controller, 'NewPauseGuardian').withArgs(currentGuardian, guardian.address) - expect(await controller.pauseGuardian()).eq(guardian.address) - }) - it('should fail pause guardian when not governor', async function () { - const tx = controller.connect(me).setPauseGuardian(guardian.address) - await expect(tx).revertedWith('Only Governor can call') - }) - it('should set partialPaused and unset from governor and guardian', async function () { - expect(await controller.partialPaused()).eq(false) - // Governor set - await setPartialPause(governor, true) - // Governor unset - await setPartialPause(governor, false) - - await controller.connect(governor).setPauseGuardian(guardian.address) - // Guardian set - await setPartialPause(guardian, true) - // Guardian unset - await setPartialPause(guardian, false) - }) - it('should fail partial pause if not guardian or governor', async function () { - const tx = controller.connect(me).setPartialPaused(true) - await expect(tx).revertedWith('Only Governor or Guardian can call') - }) - it('should check that a function fails when partialPause is set', async function () { - await setPartialPause(governor, true) - - const tokensToStake = toGRT('100') - const tx = staking.connect(me).stake(tokensToStake) - await expect(tx).revertedWith('Partial-paused') - }) - it('should set pause and unset from governor and guardian', async function () { - expect(await controller.paused()).eq(false) - // Governor set - await setPause(governor, true) - // Governor unset - await setPause(governor, false) - - await controller.connect(governor).setPauseGuardian(guardian.address) - // Guardian set - await setPause(guardian, true) - // Guardian unset - await setPause(guardian, false) - }) - it('should fail pause if not guardian or governor', async function () { - const tx = controller.connect(me).setPaused(true) - await expect(tx).revertedWith('Only Governor or Guardian can call') - }) - it('should check that a function fails when pause is set', async function () { - await setPause(governor, true) - - const tokensToStake = toGRT('100') - const tx1 = staking.connect(me).stake(tokensToStake) - await expect(tx1).revertedWith('Paused') - - const tx2 = staking.connect(me).withdraw() - await expect(tx2).revertedWith('Paused') - }) -}) diff --git a/packages/contracts-test/tests/unit/upgrade/admin.test.ts b/packages/contracts-test/tests/unit/upgrade/admin.test.ts deleted file mode 100644 index bfe5c9e01..000000000 --- a/packages/contracts-test/tests/unit/upgrade/admin.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import '@nomiclabs/hardhat-ethers' - -import { GraphProxy } from '@graphprotocol/contracts' -import { Curation } from '@graphprotocol/contracts' -import { GraphProxyAdmin } from '@graphprotocol/contracts' -import { IStaking } from '@graphprotocol/contracts' -import { deploy, DeployType, GraphNetworkContracts, loadContractAt } from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' - -const { ethers } = hre -const { AddressZero } = ethers.constants - -describe('Upgrades', () => { - const graph = hre.graph() - let me: SignerWithAddress - let governor: SignerWithAddress - - let fixture: NetworkFixture - - let contracts: GraphNetworkContracts - let proxyAdmin: GraphProxyAdmin - let curation: Curation - let staking: IStaking - let stakingProxy: GraphProxy - - before(async function () { - ;[me, governor] = await graph.getTestAccounts() - - fixture = new NetworkFixture(graph.provider) - contracts = await fixture.load(governor) - staking = contracts.Staking as IStaking - proxyAdmin = contracts.GraphProxyAdmin as GraphProxyAdmin - curation = contracts.Curation as Curation - stakingProxy = loadContractAt('GraphProxy', staking.address, undefined, governor) as GraphProxy - - // Give some funds to the indexer and approve staking contract to use funds on indexer behalf - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('GraphProxyAdmin & GraphProxy', function () { - describe('getters', function () { - describe('admin()', function () { - it('should get the proxy admin of a proxy contract', async function () { - const proxyAdminAddress = await proxyAdmin.getProxyAdmin(staking.address) - expect(proxyAdmin.address).eq(proxyAdminAddress) - }) - - it('reject get admin from other than the ProxyAdmin', async function () { - await expect(stakingProxy.connect(governor).admin()).to.be.reverted - }) - }) - - describe('implementation()', function () { - it('should get implementation only from ProxyAdmin', async function () { - const implementationAddress = await proxyAdmin.getProxyImplementation(staking.address) - expect(implementationAddress).not.eq(AddressZero) - }) - - it('reject get implementation from other than the ProxyAdmin', async function () { - await expect(stakingProxy.implementation()).to.be.reverted - }) - }) - - describe('pendingImplementation()', function () { - it('should get pending implementation only from ProxyAdmin', async function () { - const pendingImplementationAddress = await proxyAdmin.getProxyPendingImplementation(staking.address) - expect(pendingImplementationAddress).eq(AddressZero) - }) - - it('reject get pending implementation from other than the ProxyAdmin', async function () { - await expect(stakingProxy.connect(governor).pendingImplementation()).to.be.reverted - }) - }) - }) - - describe('upgrade', function () { - it('should be able to upgrade contract', async function () { - // Get some other implementation to use just for the purpose of the test - const oldImplementationAddress = await proxyAdmin.getProxyImplementation(staking.address) - const newImplementationAddress = await proxyAdmin.getProxyImplementation(curation.address) - - const stakingProxy = loadContractAt('GraphProxy', staking.address, undefined, governor) - - // Upgrade the Staking:Proxy to a new implementation - const tx1 = proxyAdmin.connect(governor).upgrade(staking.address, newImplementationAddress) - await expect(tx1) - .emit(stakingProxy, 'PendingImplementationUpdated') - .withArgs(AddressZero, newImplementationAddress) - - const tx2 = proxyAdmin.connect(governor).acceptProxy(newImplementationAddress, staking.address) - await expect(tx2) - .emit(stakingProxy, 'ImplementationUpdated') - .withArgs(oldImplementationAddress, newImplementationAddress) - - // Implementation should be the new one - expect(await proxyAdmin.getProxyImplementation(curation.address)).eq(newImplementationAddress) - }) - - it('reject upgrade if not the governor of the ProxyAdmin', async function () { - const newImplementationAddress = await proxyAdmin.getProxyImplementation(curation.address) - - // Upgrade the Staking:Proxy to a new implementation - const tx = proxyAdmin.connect(me).upgrade(staking.address, newImplementationAddress) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('reject upgrade if not using the ProxyAdmin', async function () { - const newImplementationAddress = await proxyAdmin.getProxyImplementation(curation.address) - - // Due to the transparent proxy we should not be able to upgrade from other than the proxy admin - const tx = stakingProxy.connect(governor).upgradeTo(newImplementationAddress) - await expect(tx).to.be.reverted - }) - }) - - describe('acceptUpgrade', function () { - it('reject accept upgrade if not using the ProxyAdmin', async function () { - // Due to the transparent proxy we should not be able to accept upgrades from other than the proxy admin - const tx = stakingProxy.connect(governor).acceptUpgrade() - await expect(tx).to.be.reverted - }) - }) - - describe('acceptProxy', function () { - it('reject accept proxy if not using the ProxyAdmin', async function () { - const newImplementationAddress = await proxyAdmin.getProxyImplementation(curation.address) - const implementation = loadContractAt('Curation', newImplementationAddress, undefined, governor) - - // Start an upgrade to a new implementation - await proxyAdmin.connect(governor).upgrade(staking.address, newImplementationAddress) - - // Try to accept the proxy directly from the implementation, this should not work, only from the ProxyAdmin - const tx = implementation.connect(governor).acceptProxy(staking.address) - await expect(tx).revertedWith('Caller must be the proxy admin') - }) - }) - - describe('changeProxyAdmin', function () { - it('should set the proxy admin of a proxy', async function () { - const { contract: otherProxyAdmin } = await deploy(DeployType.Deploy, governor, { - name: 'GraphProxyAdmin', - }) - - await proxyAdmin.connect(governor).changeProxyAdmin(staking.address, otherProxyAdmin.address) - expect(await otherProxyAdmin.getProxyAdmin(staking.address)).eq(otherProxyAdmin.address) - - // Should not find the change admin function in the proxy due to transparent proxy - // as this ProxyAdmin is not longer the owner - const tx = proxyAdmin.connect(governor).changeProxyAdmin(staking.address, otherProxyAdmin.address) - await expect(tx).to.be.reverted - }) - - it('reject change admin if not the governor of the ProxyAdmin', async function () { - const { contract: otherProxyAdmin } = await deploy(DeployType.Deploy, governor, { - name: 'GraphProxyAdmin', - }) - - const tx = proxyAdmin.connect(me).changeProxyAdmin(staking.address, otherProxyAdmin.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('reject change admin if not using the ProxyAdmin', async function () { - // Due to the transparent proxy we should not be able to set admin from other than the proxy admin - const tx = stakingProxy.connect(governor).setAdmin(me.address) - await expect(tx).to.be.reverted - }) - }) - }) -}) diff --git a/packages/graph-core/foundry.toml b/packages/graph-core/foundry.toml new file mode 100644 index 000000000..35b9b51af --- /dev/null +++ b/packages/graph-core/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-core/package.json b/packages/graph-core/package.json new file mode 100644 index 000000000..bcb87b7cc --- /dev/null +++ b/packages/graph-core/package.json @@ -0,0 +1,19 @@ +{ + "name": "@graphprotocol/graph-core", + "version": "0.0.0", + "private": true, + "description": "The Graph governance and proxy contracts", + "license": "GPL-2.0-or-later", + "scripts": { + "build": "pnpm build:dep", + "build:dep": "pnpm --filter '@graphprotocol/graph-core^...' 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-core/test/unit/governance/Controller.t.sol b/packages/graph-core/test/unit/governance/Controller.t.sol new file mode 100644 index 000000000..b9411d0b7 --- /dev/null +++ b/packages/graph-core/test/unit/governance/Controller.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IControllerLike } from "../interfaces/IControllerLike.sol"; +import { GraphCoreTest } from "../shared/GraphCoreTest.t.sol"; + +contract ControllerTest is GraphCoreTest { + event SetContractProxy(bytes32 indexed id, address contractAddress); + event SetController(address controller); + event NewPauseGuardian(address indexed oldPauseGuardian, address indexed pauseGuardian); + + bytes32 internal constant EPOCH_MANAGER_ID = keccak256("EpochManager"); + + address internal me = makeAddr("me"); + address internal mockController = makeAddr("mockController"); + address internal newMockEpochManager = makeAddr("newMockEpochManager"); + + function setUp() public override { + super.setUp(); + _registerContract("EpochManager", address(managed)); + } + + // -- setContractProxy() -- + + function test_SetContractProxy_SetsAndGets() public { + assertEq(address(managed.controller()), address(controller)); + + vm.expectEmit(address(controller)); + emit SetContractProxy(EPOCH_MANAGER_ID, newMockEpochManager); + vm.prank(governor); + controller.setContractProxy(EPOCH_MANAGER_ID, newMockEpochManager); + assertEq(controller.getContractProxy(EPOCH_MANAGER_ID), newMockEpochManager); + } + + function test_SetContractProxy_RevertWhen_AddressZero() public { + vm.expectRevert(bytes("Contract address must be set")); + vm.prank(governor); + controller.setContractProxy(EPOCH_MANAGER_ID, address(0)); + } + + function test_SetContractProxy_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + controller.setContractProxy(EPOCH_MANAGER_ID, newMockEpochManager); + } + + // -- unsetContractProxy() -- + + function test_UnsetContractProxy_Unsets() public { + vm.prank(governor); + controller.setContractProxy(EPOCH_MANAGER_ID, newMockEpochManager); + assertEq(controller.getContractProxy(EPOCH_MANAGER_ID), newMockEpochManager); + + vm.expectEmit(address(controller)); + emit SetContractProxy(EPOCH_MANAGER_ID, address(0)); + vm.prank(governor); + controller.unsetContractProxy(EPOCH_MANAGER_ID); + assertEq(controller.getContractProxy(EPOCH_MANAGER_ID), address(0)); + } + + function test_UnsetContractProxy_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + controller.unsetContractProxy(EPOCH_MANAGER_ID); + } + + // -- updateController() -- + + function test_UpdateController_UpdatesManaged() public { + vm.expectEmit(address(managed)); + emit SetController(mockController); + vm.prank(governor); + controller.updateController(EPOCH_MANAGER_ID, mockController); + assertEq(address(managed.controller()), mockController); + } + + function test_UpdateController_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + controller.updateController(EPOCH_MANAGER_ID, mockController); + } + + function test_UpdateController_RevertWhen_ControllerZero() public { + vm.expectRevert(bytes("Controller must be set")); + vm.prank(governor); + controller.updateController(EPOCH_MANAGER_ID, address(0)); + } + + // -- setController() -- + + function test_SetController_RevertWhen_NotController() public { + vm.expectRevert(bytes("Caller must be Controller")); + vm.prank(me); + managed.setController(mockController); + } + + // -- setPauseGuardian() -- + + function test_SetPauseGuardian_Sets() public { + address currentPauseGuardian = IControllerLike(address(controller)).pauseGuardian(); + vm.expectEmit(address(controller)); + emit NewPauseGuardian(currentPauseGuardian, me); + vm.prank(governor); + controller.setPauseGuardian(me); + assertEq(IControllerLike(address(controller)).pauseGuardian(), me); + } + + function test_SetPauseGuardian_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + controller.setPauseGuardian(me); + } + + function test_SetPauseGuardian_RevertWhen_GuardianZero() public { + vm.expectRevert(bytes("PauseGuardian must be set")); + vm.prank(governor); + controller.setPauseGuardian(address(0)); + } +} diff --git a/packages/graph-core/test/unit/governance/Governed.t.sol b/packages/graph-core/test/unit/governance/Governed.t.sol new file mode 100644 index 000000000..70eb63803 --- /dev/null +++ b/packages/graph-core/test/unit/governance/Governed.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IGoverned } from "@graphprotocol/interfaces/contracts/contracts/governance/IGoverned.sol"; +import { ProtocolFixture } from "../shared/ProtocolFixture.t.sol"; + +contract GovernedTest is ProtocolFixture { + event NewPendingOwnership(address indexed from, address indexed to); + event NewOwnership(address indexed from, address indexed to); + + IGoverned internal governed; + address internal me = makeAddr("me"); + + function setUp() public { + vm.warp(1_700_000_000); + governor = makeAddr("governor"); + vm.prank(governor); + governed = IGoverned( + _deployArtifact(string.concat(PKG_ARTIFACTS, "tests/GovernedMock.sol/GovernedMock.json"), "") + ); + assertEq(governed.governor(), governor); + } + + function test_Governed_RevertWhen_TransferNotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + governed.transferOwnership(me); + } + + function test_Governed_TransferAndAccept() public { + vm.expectEmit(address(governed)); + emit NewPendingOwnership(address(0), me); + vm.prank(governor); + governed.transferOwnership(me); + + vm.expectRevert(bytes("Caller must be pending governor")); + vm.prank(governor); + governed.acceptOwnership(); + + vm.expectEmit(address(governed)); + emit NewOwnership(governor, me); + vm.prank(me); + governed.acceptOwnership(); + + assertEq(governed.pendingGovernor(), address(0)); + } + + function test_Governed_RevertWhen_TransferToZero() public { + vm.expectRevert(bytes("Governor must be set")); + vm.prank(governor); + governed.transferOwnership(address(0)); + } +} diff --git a/packages/graph-core/test/unit/governance/Managed.t.sol b/packages/graph-core/test/unit/governance/Managed.t.sol new file mode 100644 index 000000000..d45a93307 --- /dev/null +++ b/packages/graph-core/test/unit/governance/Managed.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { Vm } from "forge-std/Vm.sol"; +import { GraphCoreTest } from "../shared/GraphCoreTest.t.sol"; + +contract ManagedTest is GraphCoreTest { + event ContractSynced(bytes32 indexed nameHash, address contractAddress); + + address internal me = makeAddr("me"); + + /// In the order ManagedMock.resolveAll and Managed.syncAllContracts use + function _names() internal pure returns (string[7] memory) { + return ["Curation", "EpochManager", "RewardsManager", "Staking", "GraphToken", "GraphTokenGateway", "GNS"]; + } + + function _registerAll(string memory prefix) internal returns (address[7] memory addrs) { + string[7] memory names = _names(); + for (uint256 i = 0; i < 7; i++) { + addrs[i] = makeAddr(string.concat(prefix, names[i])); + _registerContract(names[i], addrs[i]); + } + } + + function test_Managed_SyncAllContractsEmitsPerChangedId() public { + address curation = makeAddr("curation"); + address staking = makeAddr("staking"); + address gns = makeAddr("gns"); + _registerContract("Curation", curation); + _registerContract("Staking", staking); + _registerContract("GNS", gns); + + vm.expectEmit(address(managed)); + emit ContractSynced(keccak256("Curation"), curation); + vm.expectEmit(address(managed)); + emit ContractSynced(keccak256("Staking"), staking); + vm.expectEmit(address(managed)); + emit ContractSynced(keccak256("GNS"), gns); + managed.syncAllContracts(); + + vm.recordLogs(); + managed.syncAllContracts(); + Vm.Log[] memory logs = vm.getRecordedLogs(); + for (uint256 i = 0; i < logs.length; i++) { + assertNotEq(logs[i].topics[0], ContractSynced.selector); + } + } + + function test_Managed_ResolveAllUsesControllerThenCache() public { + address[7] memory registered = _registerAll("first"); + address[7] memory resolved = managed.resolveAll(); + for (uint256 i = 0; i < 7; i++) { + assertEq(resolved[i], registered[i]); + } + + managed.syncAllContracts(); + _registerAll("second"); + + resolved = managed.resolveAll(); + for (uint256 i = 0; i < 7; i++) { + assertEq(resolved[i], registered[i]); + } + } + + function test_Managed_RevertWhen_CheckOnlyGovernorNotGovernor() public { + assertEq(controller.getGovernor(), governor); + vm.prank(governor); + managed.checkOnlyGovernor(); + + vm.expectRevert(bytes("Only Controller governor")); + vm.prank(me); + managed.checkOnlyGovernor(); + } +} diff --git a/packages/graph-core/test/unit/governance/Pausing.t.sol b/packages/graph-core/test/unit/governance/Pausing.t.sol new file mode 100644 index 000000000..1571eb903 --- /dev/null +++ b/packages/graph-core/test/unit/governance/Pausing.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IControllerLike } from "../interfaces/IControllerLike.sol"; +import { GraphCoreTest } from "../shared/GraphCoreTest.t.sol"; + +contract PausingTest is GraphCoreTest { + event PartialPauseChanged(bool isPaused); + event PauseChanged(bool isPaused); + event NewPauseGuardian(address indexed oldPauseGuardian, address indexed pauseGuardian); + + address internal me = makeAddr("me"); + address internal guardian = makeAddr("guardian"); + + function _setPartialPause(address account, bool value) internal { + vm.expectEmit(address(controller)); + emit PartialPauseChanged(value); + vm.prank(account); + controller.setPartialPaused(value); + assertEq(controller.partialPaused(), value); + } + + function _setPause(address account, bool value) internal { + vm.expectEmit(address(controller)); + emit PauseChanged(value); + vm.prank(account); + controller.setPaused(value); + assertEq(controller.paused(), value); + } + + function test_Pausing_SetPauseGuardian() public { + address currentGuardian = IControllerLike(address(controller)).pauseGuardian(); + vm.expectEmit(address(controller)); + emit NewPauseGuardian(currentGuardian, guardian); + vm.prank(governor); + controller.setPauseGuardian(guardian); + assertEq(IControllerLike(address(controller)).pauseGuardian(), guardian); + } + + function test_Pausing_RevertWhen_SetPauseGuardianNotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + controller.setPauseGuardian(guardian); + } + + function test_Pausing_SetAndUnsetPartialPaused() public { + assertFalse(controller.partialPaused()); + _setPartialPause(governor, true); + _setPartialPause(governor, false); + + vm.prank(governor); + controller.setPauseGuardian(guardian); + _setPartialPause(guardian, true); + _setPartialPause(guardian, false); + } + + function test_Pausing_RevertWhen_PartialPauseNotGovernorOrGuardian() public { + vm.expectRevert(bytes("Only Governor or Guardian can call")); + vm.prank(me); + controller.setPartialPaused(true); + } + + function test_Pausing_RevertWhen_PartialPaused() public { + _setPartialPause(governor, true); + + vm.expectRevert(bytes("Partial-paused")); + vm.prank(me); + managed.checkNotPartialPaused(); + } + + function test_Pausing_SetAndUnsetPaused() public { + assertFalse(controller.paused()); + _setPause(governor, true); + _setPause(governor, false); + + vm.prank(governor); + controller.setPauseGuardian(guardian); + _setPause(guardian, true); + _setPause(guardian, false); + } + + function test_Pausing_RevertWhen_PauseNotGovernorOrGuardian() public { + vm.expectRevert(bytes("Only Governor or Guardian can call")); + vm.prank(me); + controller.setPaused(true); + } + + function test_Pausing_RevertWhen_Paused() public { + _setPause(governor, true); + + vm.expectRevert(bytes("Paused")); + vm.prank(me); + managed.checkNotPartialPaused(); + + vm.expectRevert(bytes("Paused")); + vm.prank(me); + managed.checkNotPaused(); + } +} diff --git a/packages/graph-core/test/unit/interfaces/IControllerLike.sol b/packages/graph-core/test/unit/interfaces/IControllerLike.sol new file mode 100644 index 000000000..735f5d1da --- /dev/null +++ b/packages/graph-core/test/unit/interfaces/IControllerLike.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IController } from "@graphprotocol/interfaces/contracts/contracts/governance/IController.sol"; + +/// IController plus the `pauseGuardian` getter that Pausable exposes but IController does not. +interface IControllerLike is IController { + function pauseGuardian() external view returns (address); +} diff --git a/packages/graph-core/test/unit/interfaces/IManagedMock.sol b/packages/graph-core/test/unit/interfaces/IManagedMock.sol new file mode 100644 index 000000000..ea83053e7 --- /dev/null +++ b/packages/graph-core/test/unit/interfaces/IManagedMock.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IManaged } from "@graphprotocol/interfaces/contracts/contracts/governance/IManaged.sol"; +import { IGraphProxy } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxy.sol"; + +interface IManagedMock is IManaged { + function initialize(address controller) external; + + function checkNotPartialPaused() external view; + + function checkNotPaused() external view; + + function checkOnlyGovernor() external view; + + /// curation, epochManager, rewardsManager, staking, graphToken, graphTokenGateway, gns — in that order + function resolveAll() external view returns (address[7] memory); + + function acceptProxy(IGraphProxy proxy) external; + + function acceptProxyAndCall(IGraphProxy proxy, bytes calldata data) external; +} diff --git a/packages/graph-core/test/unit/shared/Fixture.t.sol b/packages/graph-core/test/unit/shared/Fixture.t.sol new file mode 100644 index 000000000..1b03aa050 --- /dev/null +++ b/packages/graph-core/test/unit/shared/Fixture.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IGraphProxy } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxy.sol"; +import { GraphCoreTest } from "./GraphCoreTest.t.sol"; + +contract FixtureTest is GraphCoreTest { + string internal constant CONTROLLER_ARTIFACT = + string.concat(CORE_ARTIFACTS, "governance/Controller.sol/Controller.json"); + string internal constant MISSING_ARTIFACT = string.concat(CORE_ARTIFACTS, "governance/Missing.sol/Missing.json"); + + function deployArtifact(string memory path) external returns (address) { + return _deployArtifact(path, ""); + } + + function test_Fixture_ArtifactReadable() public view { + assertTrue(vm.isFile(CONTROLLER_ARTIFACT)); + } + + function test_Fixture_RevertWhen_ArtifactMissing() public { + assertFalse(vm.isFile(MISSING_ARTIFACT)); + vm.expectRevert(); + this.deployArtifact(MISSING_ARTIFACT); + } + + function test_Fixture_ProxyWiredToImplementation() public view { + IGraphProxy proxy = IGraphProxy(address(managed)); + assertEq(proxyAdmin.getProxyAdmin(proxy), address(proxyAdmin)); + assertEq(proxyAdmin.getProxyImplementation(proxy), implA); + assertEq(proxyAdmin.getProxyPendingImplementation(proxy), address(0)); + assertEq(address(managed.controller()), address(controller)); + } + + function test_Fixture_ControllerUnpaused() public view { + assertFalse(controller.paused()); + assertFalse(controller.partialPaused()); + managed.checkNotPartialPaused(); + } +} diff --git a/packages/graph-core/test/unit/shared/GraphCoreTest.t.sol b/packages/graph-core/test/unit/shared/GraphCoreTest.t.sol new file mode 100644 index 000000000..85573389c --- /dev/null +++ b/packages/graph-core/test/unit/shared/GraphCoreTest.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { ProtocolFixture } from "./ProtocolFixture.t.sol"; +import { IManagedMock } from "../interfaces/IManagedMock.sol"; + +abstract contract GraphCoreTest is ProtocolFixture { + string internal constant MANAGED_MOCK = string.concat(PKG_ARTIFACTS, "tests/ManagedMock.sol/ManagedMock.json"); + + address internal implA; + address internal implB; + IManagedMock internal managed; + + function setUp() public virtual { + vm.warp(1_700_000_000); + _deployProtocolBase(); + implA = _deployArtifact(MANAGED_MOCK, ""); + implB = _deployArtifact(MANAGED_MOCK, ""); + managed = IManagedMock( + _deployBehindProxy(implA, abi.encodeCall(IManagedMock.initialize, (address(controller)))) + ); + } +} diff --git a/packages/graph-core/test/unit/shared/ProtocolFixture.t.sol b/packages/graph-core/test/unit/shared/ProtocolFixture.t.sol new file mode 100644 index 000000000..f38962c8e --- /dev/null +++ b/packages/graph-core/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-core/test/unit/upgrades/GraphProxy.t.sol b/packages/graph-core/test/unit/upgrades/GraphProxy.t.sol new file mode 100644 index 000000000..cd4a16a79 --- /dev/null +++ b/packages/graph-core/test/unit/upgrades/GraphProxy.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IGraphProxy } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxy.sol"; +import { IManagedMock } from "../interfaces/IManagedMock.sol"; +import { GraphCoreTest } from "../shared/GraphCoreTest.t.sol"; + +contract GraphProxyTest is GraphCoreTest { + IGraphProxy internal proxy; + + function setUp() public override { + super.setUp(); + proxy = IGraphProxy(address(managed)); + } + + function test_GraphProxy_RevertWhen_AcceptUpgradeAndCallFails() public { + vm.prank(governor); + proxyAdmin.upgrade(proxy, implB); + + // The delegatecall runs with msg.sender == implB, which is not the governor + vm.expectRevert(bytes("Impl call failed")); + vm.prank(implB); + proxy.acceptUpgradeAndCall(abi.encodeCall(IManagedMock.checkOnlyGovernor, ())); + } + + function test_GraphProxy_RevertWhen_AdminCallsFallback() public { + vm.expectRevert(bytes("Cannot fallback to proxy target")); + vm.prank(address(proxyAdmin)); + managed.checkNotPaused(); + } + + function test_GraphProxy_RevertWhen_SetAdminZero() public { + vm.expectRevert(bytes("Admin cant be the zero address")); + vm.prank(address(proxyAdmin)); + proxy.setAdmin(address(0)); + } + + function test_GraphProxy_RevertWhen_AcceptUpgradeWithoutPendingImplementation() public { + vm.expectRevert(bytes("Impl cannot be zero address")); + vm.prank(address(proxyAdmin)); + proxy.acceptUpgrade(); + } + + function test_GraphProxy_RevertWhen_AcceptUpgradeNotPendingImplementation() public { + vm.prank(governor); + proxyAdmin.upgrade(proxy, implB); + + vm.expectRevert(bytes("Only pending implementation")); + vm.prank(address(proxyAdmin)); + proxy.acceptUpgrade(); + } +} diff --git a/packages/graph-core/test/unit/upgrades/GraphProxyAdmin.t.sol b/packages/graph-core/test/unit/upgrades/GraphProxyAdmin.t.sol new file mode 100644 index 000000000..2cdfb4c9f --- /dev/null +++ b/packages/graph-core/test/unit/upgrades/GraphProxyAdmin.t.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IGraphProxy } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxy.sol"; +import { IGraphProxyAdmin } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol"; +import { IManagedMock } from "../interfaces/IManagedMock.sol"; +import { GraphCoreTest } from "../shared/GraphCoreTest.t.sol"; + +contract GraphProxyAdminTest is GraphCoreTest { + event PendingImplementationUpdated( + address indexed oldPendingImplementation, + address indexed newPendingImplementation + ); + event ImplementationUpdated(address indexed oldImplementation, address indexed newImplementation); + + address internal me = makeAddr("me"); + IGraphProxy internal proxy; + + function setUp() public override { + super.setUp(); + proxy = IGraphProxy(address(managed)); + } + + function _deployOtherProxyAdmin() internal returns (IGraphProxyAdmin) { + vm.prank(governor); + return + IGraphProxyAdmin( + _deployArtifact(string.concat(CORE_ARTIFACTS, "upgrades/GraphProxyAdmin.sol/GraphProxyAdmin.json"), "") + ); + } + + // -- admin() -- + + function test_Admin_GetsProxyAdmin() public view { + assertEq(proxyAdmin.getProxyAdmin(proxy), address(proxyAdmin)); + } + + function test_Admin_RevertWhen_NotProxyAdmin() public { + vm.expectRevert(); + vm.prank(governor); + proxy.admin(); + } + + // -- implementation() -- + + function test_Implementation_GetsFromProxyAdmin() public view { + assertEq(proxyAdmin.getProxyImplementation(proxy), implA); + } + + function test_Implementation_RevertWhen_NotProxyAdmin() public { + vm.expectRevert(); + vm.prank(governor); + proxy.implementation(); + } + + // -- pendingImplementation() -- + + function test_PendingImplementation_GetsFromProxyAdmin() public view { + assertEq(proxyAdmin.getProxyPendingImplementation(proxy), address(0)); + } + + function test_PendingImplementation_RevertWhen_NotProxyAdmin() public { + vm.expectRevert(); + vm.prank(governor); + proxy.pendingImplementation(); + } + + // -- upgrade -- + + function test_Upgrade_UpgradesContract() public { + vm.expectEmit(address(proxy)); + emit PendingImplementationUpdated(address(0), implB); + vm.prank(governor); + proxyAdmin.upgrade(proxy, implB); + + vm.expectEmit(address(proxy)); + emit ImplementationUpdated(implA, implB); + vm.prank(governor); + proxyAdmin.acceptProxy(implB, proxy); + + assertEq(proxyAdmin.getProxyImplementation(proxy), implB); + } + + function test_Upgrade_RevertWhen_NotGovernor() public { + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + proxyAdmin.upgrade(proxy, implB); + } + + function test_Upgrade_RevertWhen_NotProxyAdmin() public { + vm.expectRevert(); + vm.prank(governor); + proxy.upgradeTo(implB); + } + + // -- acceptUpgrade -- + + function test_AcceptUpgrade_RevertWhen_NotProxyAdmin() public { + vm.expectRevert(); + vm.prank(governor); + proxy.acceptUpgrade(); + } + + // -- acceptProxy -- + + function test_AcceptProxy_RevertWhen_NotProxyAdmin() public { + vm.prank(governor); + proxyAdmin.upgrade(proxy, implB); + + vm.expectRevert(bytes("Caller must be the proxy admin")); + vm.prank(governor); + IManagedMock(implB).acceptProxy(proxy); + } + + // -- changeProxyAdmin -- + + function test_ChangeProxyAdmin_SetsProxyAdmin() public { + IGraphProxyAdmin otherProxyAdmin = _deployOtherProxyAdmin(); + + vm.prank(governor); + proxyAdmin.changeProxyAdmin(proxy, address(otherProxyAdmin)); + assertEq(otherProxyAdmin.getProxyAdmin(proxy), address(otherProxyAdmin)); + + vm.expectRevert(); + vm.prank(governor); + proxyAdmin.changeProxyAdmin(proxy, address(otherProxyAdmin)); + } + + function test_ChangeProxyAdmin_RevertWhen_NotGovernor() public { + IGraphProxyAdmin otherProxyAdmin = _deployOtherProxyAdmin(); + + vm.expectRevert(bytes("Only Governor can call")); + vm.prank(me); + proxyAdmin.changeProxyAdmin(proxy, address(otherProxyAdmin)); + } + + function test_ChangeProxyAdmin_RevertWhen_NotProxyAdmin() public { + vm.expectRevert(); + vm.prank(governor); + proxy.setAdmin(me); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7dab5ef99..c5bf6f5c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -711,6 +711,18 @@ importers: specifier: ^5.5.0 version: 5.9.3 + packages/graph-core: + 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/t1.md b/scripts/contracts-migration/test-mapping/t1.md new file mode 100644 index 000000000..02e0d750c --- /dev/null +++ b/scripts/contracts-migration/test-mapping/t1.md @@ -0,0 +1,46 @@ +# T1 test mapping: governance and proxy tests + +New tests are in `packages/graph-core`. The `ManagedMock` proxy stands in for the EpochManager, Staking and Curation contracts the old tests used as "some Managed / GraphUpgradeable contract"; `implB` stands in for the Curation implementation. + +| Old file | Old test (describe › it) | New test (file::function) | Note | +| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| packages/contracts-test/tests/unit/governance/governed.test.ts | Governed › should reject transfer if not allowed | test/unit/governance/Governed.t.sol::test_Governed_RevertWhen_TransferNotGovernor | | +| packages/contracts-test/tests/unit/governance/governed.test.ts | Governed › should transfer and accept | test/unit/governance/Governed.t.sol::test_Governed_TransferAndAccept | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › setContractProxy() › should set contract proxy and test get contract proxy | test/unit/governance/Controller.t.sol::test_SetContractProxy_SetsAndGets | ManagedMock proxy registered as EpochManager | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › setContractProxy() › reject set contract proxy to address zero | test/unit/governance/Controller.t.sol::test_SetContractProxy_RevertWhen_AddressZero | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › setContractProxy() › reject set contract proxy if not governor | test/unit/governance/Controller.t.sol::test_SetContractProxy_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › unsetContractProxy() › should unset contract proxy | test/unit/governance/Controller.t.sol::test_UnsetContractProxy_Unsets | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › unsetContractProxy() › reject to call if not authorized caller | test/unit/governance/Controller.t.sol::test_UnsetContractProxy_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › updateController() › should update controller on a manager | test/unit/governance/Controller.t.sol::test_UpdateController_UpdatesManaged | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › updateController() › should fail updating controller when not called by governor | test/unit/governance/Controller.t.sol::test_UpdateController_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › updateController() › reject update controller to address zero | test/unit/governance/Controller.t.sol::test_UpdateController_RevertWhen_ControllerZero | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › setController() › should fail setting controller when not called from Controller | test/unit/governance/Controller.t.sol::test_SetController_RevertWhen_NotController | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › setPauseGuardian() › should set the pause guardian | test/unit/governance/Controller.t.sol::test_SetPauseGuardian_Sets | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › setPauseGuardian() › reject to call if not authorized caller | test/unit/governance/Controller.t.sol::test_SetPauseGuardian_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/governance/controller.test.ts | Managed › setPauseGuardian() › reject set pause guardian to address zero | test/unit/governance/Controller.t.sol::test_SetPauseGuardian_RevertWhen_GuardianZero | | +| packages/contracts-test/tests/unit/governance/pausing.test.ts | Pausing › should set pause guardian | test/unit/governance/Pausing.t.sol::test_Pausing_SetPauseGuardian | | +| packages/contracts-test/tests/unit/governance/pausing.test.ts | Pausing › should fail pause guardian when not governor | test/unit/governance/Pausing.t.sol::test_Pausing_RevertWhen_SetPauseGuardianNotGovernor | | +| packages/contracts-test/tests/unit/governance/pausing.test.ts | Pausing › should set partialPaused and unset from governor and guardian | test/unit/governance/Pausing.t.sol::test_Pausing_SetAndUnsetPartialPaused | | +| packages/contracts-test/tests/unit/governance/pausing.test.ts | Pausing › should fail partial pause if not guardian or governor | test/unit/governance/Pausing.t.sol::test_Pausing_RevertWhen_PartialPauseNotGovernorOrGuardian | | +| packages/contracts-test/tests/unit/governance/pausing.test.ts | Pausing › should check that a function fails when partialPause is set | test/unit/governance/Pausing.t.sol::test_Pausing_RevertWhen_PartialPaused | `ManagedMock.checkNotPartialPaused` replaces `Staking.stake` | +| packages/contracts-test/tests/unit/governance/pausing.test.ts | Pausing › should set pause and unset from governor and guardian | test/unit/governance/Pausing.t.sol::test_Pausing_SetAndUnsetPaused | | +| packages/contracts-test/tests/unit/governance/pausing.test.ts | Pausing › should fail pause if not guardian or governor | test/unit/governance/Pausing.t.sol::test_Pausing_RevertWhen_PauseNotGovernorOrGuardian | | +| packages/contracts-test/tests/unit/governance/pausing.test.ts | Pausing › should check that a function fails when pause is set | test/unit/governance/Pausing.t.sol::test_Pausing_RevertWhen_Paused | `checkNotPartialPaused` / `checkNotPaused` replace `Staking.stake` / `withdraw` | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › getters › admin() › should get the proxy admin of a proxy contract | test/unit/upgrades/GraphProxyAdmin.t.sol::test_Admin_GetsProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › getters › admin() › reject get admin from other than the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_Admin_RevertWhen_NotProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › getters › implementation() › should get implementation only from ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_Implementation_GetsFromProxyAdmin | asserts `== implA` instead of `!= 0` | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › getters › implementation() › reject get implementation from other than the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_Implementation_RevertWhen_NotProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › getters › pendingImplementation() › should get pending implementation only from ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_PendingImplementation_GetsFromProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › getters › pendingImplementation() › reject get pending implementation from other than the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_PendingImplementation_RevertWhen_NotProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › upgrade › should be able to upgrade contract | test/unit/upgrades/GraphProxyAdmin.t.sol::test_Upgrade_UpgradesContract | final check reads the upgraded proxy; the old test read the Curation proxy, whose implementation it had just looked up | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › upgrade › reject upgrade if not the governor of the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_Upgrade_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › upgrade › reject upgrade if not using the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_Upgrade_RevertWhen_NotProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › acceptUpgrade › reject accept upgrade if not using the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_AcceptUpgrade_RevertWhen_NotProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › acceptProxy › reject accept proxy if not using the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_AcceptProxy_RevertWhen_NotProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › changeProxyAdmin › should set the proxy admin of a proxy | test/unit/upgrades/GraphProxyAdmin.t.sol::test_ChangeProxyAdmin_SetsProxyAdmin | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › changeProxyAdmin › reject change admin if not the governor of the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_ChangeProxyAdmin_RevertWhen_NotGovernor | | +| packages/contracts-test/tests/unit/upgrade/admin.test.ts | Upgrades › GraphProxyAdmin & GraphProxy › changeProxyAdmin › reject change admin if not using the ProxyAdmin | test/unit/upgrades/GraphProxyAdmin.t.sol::test_ChangeProxyAdmin_RevertWhen_NotProxyAdmin | | + +## Coverage + +- `constructor` (Controller, GraphProxy, GraphProxyAdmin): not a callable function; every test runs it by deploying the artifact (`_deployProtocolBase`, `_deployBehindProxy`).