diff --git a/packages/contracts-test/tests/unit/gns.test.ts b/packages/contracts-test/tests/unit/gns.test.ts deleted file mode 100644 index 92e5d83e8..000000000 --- a/packages/contracts-test/tests/unit/gns.test.ts +++ /dev/null @@ -1,1473 +0,0 @@ -import { formatGRT, SubgraphDeploymentID } from '@graphprotocol/common-ts' -import { L2GNS, L2GraphTokenGateway, SubgraphNFT } from '@graphprotocol/contracts' -import { Controller } from '@graphprotocol/contracts' -import { Curation } from '@graphprotocol/contracts' -import { GraphToken } from '@graphprotocol/contracts' -import { L1GNS } from '@graphprotocol/contracts' -import { L1GraphTokenGateway } from '@graphprotocol/contracts' -import { LegacyGNSMock } from '@graphprotocol/contracts' -import { - buildLegacySubgraphId, - buildSubgraph, - buildSubgraphId, - deploy, - DeployType, - GraphNetworkContracts, - helpers, - loadContractAt, - PublishSubgraph, - randomHexBytes, - Subgraph, - toBN, - toGRT, -} from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { BigNumber, ContractTransaction, ethers, Event } from 'ethers' -import { defaultAbiCoder } from 'ethers/lib/utils' -import hre from 'hardhat' - -import { isRunningUnderCoverage } from '../../utils/coverage' -import { NetworkFixture } from './lib/fixtures' -import { - AccountDefaultName, - burnSignal, - createDefaultName, - deprecateSubgraph, - mintSignal, - publishNewSubgraph, - publishNewVersion, -} from './lib/gnsUtils' - -const { AddressZero, HashZero } = ethers.constants - -// Utils -const toFloat = (n: BigNumber) => parseFloat(formatGRT(n)) -const toRound = (n: number) => n.toFixed(12) - -describe.skip('L1GNS @skip-on-coverage', () => { - const graph = hre.graph() - - let me: SignerWithAddress - let other: SignerWithAddress - let another: SignerWithAddress - let governor: SignerWithAddress - - let fixture: NetworkFixture - let l2MockContracts: GraphNetworkContracts - - let l2GNSMock: L2GNS - let l2GRTGatewayMock: L2GraphTokenGateway - - let gns: L1GNS - let legacyGNSMock: LegacyGNSMock - let grt: GraphToken - let curation: Curation - let controller: Controller - let subgraphNFT: SubgraphNFT - let l1GraphTokenGateway: L1GraphTokenGateway - - const tokens1000 = toGRT('1000') - const tokens10000 = toGRT('10000') - const tokens100000 = toGRT('100000') - const curationTaxPercentage = 50000 - - let newSubgraph0: PublishSubgraph - let newSubgraph1: PublishSubgraph - let defaultName: AccountDefaultName - - async function calcGNSBondingCurve( - gnsSupply: BigNumber, // nSignal - gnsReserveBalance: BigNumber, // vSignal - depositAmount: BigNumber, // GRT deposited - subgraphID: string, - ): Promise { - const signal = await curation.getCurationPoolSignal(subgraphID) - const curationTokens = await curation.getCurationPoolTokens(subgraphID) - const curationReserveRatio = await curation.defaultReserveRatio() - const expectedSignal = await calcCurationBondingCurve(signal, curationTokens, curationReserveRatio, depositAmount) - const expectedSignalBN = toGRT(String(expectedSignal.toFixed(18))) - - // Handle the initialization of the bonding curve - if (gnsSupply.eq(0)) { - return expectedSignal - } - // Since we known CW = 1, we can do the simplified formula of: - return (toFloat(gnsSupply) * toFloat(expectedSignalBN)) / toFloat(gnsReserveBalance) - } - - async function calcCurationBondingCurve( - supply: BigNumber, - reserveBalance: BigNumber, - reserveRatio: number, - depositAmount: BigNumber, - ): Promise { - // Handle the initialization of the bonding curve - const minSupply = toGRT('1') - if (supply.eq(0)) { - const minDeposit = await curation.minimumCurationDeposit() - if (depositAmount.lt(minDeposit)) { - throw new Error('deposit must be above minimum') - } - return ( - (await calcCurationBondingCurve(minSupply, minDeposit, reserveRatio, depositAmount.sub(minDeposit))) + - toFloat(minSupply) - ) - } - // Calculate bonding curve in the test - return toFloat(supply) * ((1 + toFloat(depositAmount) / toFloat(reserveBalance)) ** (reserveRatio / 1000000) - 1) - } - - const transferSignal = async ( - subgraphID: string, - owner: SignerWithAddress, - recipient: SignerWithAddress, - amount: BigNumber, - ): Promise => { - // Before state - const beforeOwnerNSignal = await gns.getCuratorSignal(subgraphID, owner.address) - const beforeRecipientNSignal = await gns.getCuratorSignal(subgraphID, recipient.address) - - // Transfer - const tx = gns.connect(owner).transferSignal(subgraphID, recipient.address, amount) - - await expect(tx).emit(gns, 'SignalTransferred').withArgs(subgraphID, owner.address, recipient.address, amount) - - // After state - const afterOwnerNSignal = await gns.getCuratorSignal(subgraphID, owner.address) - const afterRecipientNSignal = await gns.getCuratorSignal(subgraphID, recipient.address) - - // Check state - expect(afterOwnerNSignal).eq(beforeOwnerNSignal.sub(amount)) - expect(afterRecipientNSignal).eq(beforeRecipientNSignal.add(amount)) - - return tx - } - - const withdraw = async (account: SignerWithAddress, subgraphID: string): Promise => { - // Before state - const beforeCuratorNSignal = await gns.getCuratorSignal(subgraphID, account.address) - const beforeSubgraph = await gns.subgraphs(subgraphID) - const beforeGNSBalance = await grt.balanceOf(gns.address) - const tokensEstimate = beforeSubgraph.withdrawableGRT.mul(beforeCuratorNSignal).div(beforeSubgraph.nSignal) - - // Send tx - const tx = gns.connect(account).withdraw(subgraphID) - await expect(tx) - .emit(gns, 'GRTWithdrawn') - .withArgs(subgraphID, account.address, beforeCuratorNSignal, tokensEstimate) - - // curator nSignal should be updated - const afterCuratorNSignal = await gns.getCuratorSignal(subgraphID, account.address) - expect(afterCuratorNSignal).eq(toBN(0)) - - // overall n signal should be updated - const afterSubgraph = await gns.subgraphs(subgraphID) - expect(afterSubgraph.nSignal).eq(beforeSubgraph.nSignal.sub(beforeCuratorNSignal)) - - // Token balance should be updated - const afterGNSBalance = await grt.balanceOf(gns.address) - expect(afterGNSBalance).eq(beforeGNSBalance.sub(tokensEstimate)) - - return tx - } - const deployLegacyGNSMock = async (): Promise => { - const { contract: subgraphDescriptor } = await deploy(DeployType.Deploy, governor, { - name: 'SubgraphNFTDescriptor', - }) - const { contract: subgraphNFT } = await deploy(DeployType.Deploy, governor, { - name: 'SubgraphNFT', - args: [governor.address], - }) - - // Deploy - const deployResult = await deploy( - DeployType.DeployWithProxy, - governor, - { name: 'LegacyGNSMock', args: [controller.address, subgraphNFT.address] }, - graph.addressBook, - { - name: 'GraphProxy', - }, - ) - legacyGNSMock = deployResult.contract as LegacyGNSMock - - // Post-config - await subgraphNFT.connect(governor).setMinter(legacyGNSMock.address) - await subgraphNFT.connect(governor).setTokenDescriptor(subgraphDescriptor.address) - await legacyGNSMock.connect(governor).syncAllContracts() - await legacyGNSMock.connect(governor).approveAll() - await l1GraphTokenGateway.connect(governor).addToCallhookAllowlist(legacyGNSMock.address) - await legacyGNSMock.connect(governor).setCounterpartGNSAddress(l2GNSMock.address) - } - - before(async function () { - ;[me, other, governor, another] = await graph.getTestAccounts() - - fixture = new NetworkFixture(graph.provider) - - // Deploy L1 - const fixtureContracts = await fixture.load(governor) - grt = fixtureContracts.GraphToken as GraphToken - curation = fixtureContracts.Curation as Curation - gns = fixtureContracts.GNS as L1GNS - controller = fixtureContracts.Controller as Controller - l1GraphTokenGateway = fixtureContracts.L1GraphTokenGateway as L1GraphTokenGateway - subgraphNFT = fixtureContracts.SubgraphNFT as SubgraphNFT - - // Deploy L1 arbitrum bridge - await fixture.loadL1ArbitrumBridge(governor) - - // Deploy L2 mock - l2MockContracts = await fixture.loadMock(true) - l2GNSMock = l2MockContracts.L2GNS as L2GNS - l2GRTGatewayMock = l2MockContracts.L2GraphTokenGateway as L2GraphTokenGateway - - // Configure graph bridge - await fixture.configureL1Bridge(governor, fixtureContracts, l2MockContracts) - - newSubgraph0 = buildSubgraph() - newSubgraph1 = buildSubgraph() - defaultName = createDefaultName('graph') - // Give some funds to the signers and approve gns contract to use funds on signers behalf - await grt.connect(governor).mint(me.address, tokens100000) - await grt.connect(governor).mint(other.address, tokens100000) - await grt.connect(governor).mint(another.address, tokens100000) - await grt.connect(me).approve(gns.address, tokens100000) - await grt.connect(me).approve(curation.address, tokens100000) - await grt.connect(other).approve(gns.address, tokens100000) - await grt.connect(other).approve(curation.address, tokens100000) - await grt.connect(another).approve(gns.address, tokens100000) - await grt.connect(another).approve(curation.address, tokens100000) - // Update curation tax to test the functionality of it in disableNameSignal() - await curation.connect(governor).setCurationTaxPercentage(curationTaxPercentage) - - // Deploying a GNS mock with support for legacy subgraphs - await deployLegacyGNSMock() - await grt.connect(me).approve(legacyGNSMock.address, tokens100000) - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('Configuration', function () { - describe('setOwnerTaxPercentage', function () { - const newValue = 10 - - it('should set `ownerTaxPercentage`', async function () { - // Can set if allowed - await gns.connect(governor).setOwnerTaxPercentage(newValue) - expect(await gns.ownerTaxPercentage()).eq(newValue) - }) - - it('reject set `ownerTaxPercentage` if out of bounds', async function () { - const tx = gns.connect(governor).setOwnerTaxPercentage(1000001) - await expect(tx).revertedWith('Owner tax must be MAX_PPM or less') - }) - - it('reject set `ownerTaxPercentage` if not allowed', async function () { - const tx = gns.connect(me).setOwnerTaxPercentage(newValue) - await expect(tx).revertedWith('Only Controller governor') - }) - }) - - describe('setCounterpartGNSAddress', function () { - it('should set `counterpartGNSAddress`', async function () { - // Can set if allowed - const newValue = other.address - const tx = gns.connect(governor).setCounterpartGNSAddress(newValue) - await expect(tx).emit(gns, 'CounterpartGNSAddressUpdated').withArgs(newValue) - expect(await gns.counterpartGNSAddress()).eq(newValue) - }) - - it('reject set `counterpartGNSAddress` if not allowed', async function () { - const newValue = other.address - const tx = gns.connect(me).setCounterpartGNSAddress(newValue) - await expect(tx).revertedWith('Only Controller governor') - }) - }) - - describe('setSubgraphNFT', function () { - it('should set `setSubgraphNFT`', async function () { - const newValue = gns.address // I just use any contract address - const tx = gns.connect(governor).setSubgraphNFT(newValue) - await expect(tx).emit(gns, 'SubgraphNFTUpdated').withArgs(newValue) - expect(await gns.subgraphNFT()).eq(newValue) - }) - - it('revert set to empty address', async function () { - const tx = gns.connect(governor).setSubgraphNFT(AddressZero) - await expect(tx).revertedWith('NFT address cant be zero') - }) - - it('revert set to non-contract', async function () { - const tx = gns.connect(governor).setSubgraphNFT(randomHexBytes(20)) - await expect(tx).revertedWith('NFT must be valid') - }) - }) - }) - - describe('Publishing names and versions', function () { - describe('setDefaultName', function () { - it('setDefaultName emits the event', async function () { - const tx = gns.connect(me).setDefaultName(me.address, 0, defaultName.nameIdentifier, defaultName.name) - await expect(tx) - .emit(gns, 'SetDefaultName') - .withArgs(me.address, 0, defaultName.nameIdentifier, defaultName.name) - }) - - it('setDefaultName fails if not owner', async function () { - const tx = gns.connect(other).setDefaultName(me.address, 0, defaultName.nameIdentifier, defaultName.name) - await expect(tx).revertedWith('GNS: Only you can set your name') - }) - }) - - describe('updateSubgraphMetadata', function () { - let subgraph: Subgraph - - beforeEach(async function () { - subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - }) - - it('updateSubgraphMetadata emits the event', async function () { - const tx = gns.connect(me).updateSubgraphMetadata(subgraph.id, newSubgraph0.subgraphMetadata) - await expect(tx).emit(gns, 'SubgraphMetadataUpdated').withArgs(subgraph.id, newSubgraph0.subgraphMetadata) - }) - - it('updateSubgraphMetadata fails if not owner', async function () { - const tx = gns.connect(other).updateSubgraphMetadata(subgraph.id, newSubgraph0.subgraphMetadata) - await expect(tx).revertedWith('GNS: Must be authorized') - }) - }) - - describe('isPublished', function () { - it('should return if the subgraph is published', async function () { - const subgraphID = await buildSubgraphId(me.address, toBN(0), graph.chainId) - expect(await gns.isPublished(subgraphID)).eq(false) - await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - expect(await gns.isPublished(subgraphID)).eq(true) - }) - }) - - describe('publishNewSubgraph', function () { - it('should publish a new subgraph and first version with it', async function () { - await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - }) - - it('should publish a new subgraph with an incremented value', async function () { - const subgraph1 = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - const subgraph2 = await publishNewSubgraph(me, newSubgraph1, gns, graph.chainId) - expect(subgraph1.id).not.eq(subgraph2.id) - }) - - it('should prevent subgraphDeploymentID of 0 to be used', async function () { - const tx = gns - .connect(me) - .publishNewSubgraph(HashZero, newSubgraph0.versionMetadata, newSubgraph0.subgraphMetadata) - await expect(tx).revertedWith('GNS: Cannot set deploymentID to 0 in publish') - }) - }) - - describe('publishNewVersion', function () { - let subgraph: Subgraph - - beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - await mintSignal(me, subgraph.id, tokens10000, gns, curation) - }) - - it('should publish a new version on an existing subgraph', async function () { - await publishNewVersion(me, subgraph.id, newSubgraph1, gns, curation) - }) - - it('should publish a new version on an existing subgraph when owner tax is zero', async function () { - await gns.connect(governor).setOwnerTaxPercentage(0) - await publishNewVersion(me, subgraph.id, newSubgraph1, gns, curation) - }) - - it('should publish a new version on an existing subgraph when curation tax percentage is zero', async function () { - await curation.connect(governor).setCurationTaxPercentage(0) - await publishNewVersion(me, subgraph.id, newSubgraph1, gns, curation) - }) - - it('should publish a new version on an existing subgraph with no current signal', async function () { - const emptySignalSubgraph = await publishNewSubgraph(me, buildSubgraph(), gns, graph.chainId) - await publishNewVersion(me, emptySignalSubgraph.id, newSubgraph1, gns, curation) - }) - - it('should reject a new version with the same subgraph deployment ID', async function () { - const tx = gns - .connect(me) - .publishNewVersion(subgraph.id, newSubgraph0.subgraphDeploymentID, newSubgraph0.versionMetadata) - await expect(tx).revertedWith('GNS: Cannot publish a new version with the same subgraph deployment ID') - }) - - it('should reject publishing a version to a subgraph that does not exist', async function () { - const tx = gns - .connect(me) - .publishNewVersion(randomHexBytes(32), newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - await expect(tx).revertedWith('ERC721: owner query for nonexistent token') - }) - - it('reject if not the owner', async function () { - const tx = gns - .connect(other) - .publishNewVersion(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - await expect(tx).revertedWith('GNS: Must be authorized') - }) - - it('should fail when upgrade tries to point to a pre-curated', async function () { - // Curate directly to the deployment - await curation.connect(me).mint(newSubgraph1.subgraphDeploymentID, tokens1000, 0) - - // Target a pre-curated subgraph deployment - const tx = gns - .connect(me) - .publishNewVersion(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - await expect(tx).revertedWith('GNS: Owner cannot point to a subgraphID that has been pre-curated') - }) - - it('should upgrade version when there is no signal with no signal migration', async function () { - await burnSignal(me, subgraph.id, gns, curation) - const tx = gns - .connect(me) - .publishNewVersion(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - await expect(tx) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - }) - - it('should fail when subgraph is deprecated', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - const tx = gns - .connect(me) - .publishNewVersion(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - // NOTE: deprecate burns the Subgraph NFT, when someone wants to publish a new version it won't find it - await expect(tx).revertedWith('ERC721: owner query for nonexistent token') - }) - }) - describe('subgraphTokens', function () { - it('should return the correct number of tokens for a subgraph', async function () { - const subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - const taxForMe = (await curation.tokensToSignal(subgraph.subgraphDeploymentID, tokens10000))[1] - await mintSignal(me, subgraph.id, tokens10000, gns, curation) - const taxForOther = (await curation.tokensToSignal(subgraph.subgraphDeploymentID, tokens1000))[1] - await mintSignal(other, subgraph.id, tokens1000, gns, curation) - expect(await gns.subgraphTokens(subgraph.id)).eq(tokens10000.add(tokens1000).sub(taxForMe).sub(taxForOther)) - }) - }) - describe('subgraphSignal', function () { - it('should return the correct amount of signal for a subgraph', async function () { - const subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - const vSignalForMe = (await curation.tokensToSignal(subgraph.subgraphDeploymentID, tokens10000))[0] - await mintSignal(me, subgraph.id, tokens10000, gns, curation) - const vSignalForOther = (await curation.tokensToSignal(subgraph.subgraphDeploymentID, tokens1000))[0] - await mintSignal(other, subgraph.id, tokens1000, gns, curation) - const expectedSignal = await gns.vSignalToNSignal(subgraph.id, vSignalForMe.add(vSignalForOther)) - expect(await gns.subgraphSignal(subgraph.id)).eq(expectedSignal) - }) - }) - describe('deprecateSubgraph', function () { - let subgraph: Subgraph - - beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - await mintSignal(me, subgraph.id, tokens10000, gns, curation) - }) - - it('should deprecate a subgraph', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - }) - - it('should prevent a deprecated subgraph from being republished', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - const tx = gns - .connect(me) - .publishNewVersion(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - // NOTE: deprecate burns the Subgraph NFT, when someone wants to publish a new version it won't find it - await expect(tx).revertedWith('ERC721: owner query for nonexistent token') - }) - - it('reject if the subgraph does not exist', async function () { - const subgraphID = randomHexBytes(32) - const tx = gns.connect(me).deprecateSubgraph(subgraphID) - await expect(tx).revertedWith('ERC721: owner query for nonexistent token') - }) - - it('reject deprecate if not the owner', async function () { - const tx = gns.connect(other).deprecateSubgraph(subgraph.id) - await expect(tx).revertedWith('GNS: Must be authorized') - }) - }) - }) - - describe('Curating on names', function () { - describe('mintSignal()', function () { - it('should deposit into the name signal curve', async function () { - const subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - await mintSignal(other, subgraph.id, tokens10000, gns, curation) - }) - - it('should fail when name signal is disabled', async function () { - const subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - const tx = gns.connect(me).mintSignal(subgraph.id, tokens1000, 0) - await expect(tx).revertedWith('GNS: Must be active') - }) - - it('should fail if you try to deposit on a non existing name', async function () { - const subgraphID = randomHexBytes(32) - const tx = gns.connect(me).mintSignal(subgraphID, tokens1000, 0) - await expect(tx).revertedWith('GNS: Must be active') - }) - - it('reject minting if under slippage', async function () { - // First publish the subgraph - const subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - // Set slippage to be 1 less than expected result to force reverting - const { 1: expectedNSignal } = await gns.tokensToNSignal(subgraph.id, tokens1000) - const tx = gns.connect(me).mintSignal(subgraph.id, tokens1000, expectedNSignal.add(1)) - await expect(tx).revertedWith('GNS: Slippage protection') - }) - }) - - describe('burnSignal()', function () { - let subgraph: Subgraph - - beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - await mintSignal(other, subgraph.id, tokens10000, gns, curation) - }) - - it('should withdraw from the name signal curve', async function () { - await burnSignal(other, subgraph.id, gns, curation) - }) - - it('should fail when name signal is disabled', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - // just test 1 since it will fail - const tx = gns.connect(me).burnSignal(subgraph.id, 1, 0) - await expect(tx).revertedWith('GNS: Must be active') - }) - - it('should fail when the curator tries to withdraw more nSignal than they have', async function () { - const tx = gns.connect(me).burnSignal( - subgraph.id, - // 1000000 * 10^18 nSignal is a lot, and will cause fail - toBN('1000000000000000000000000'), - 0, - ) - await expect(tx).revertedWith('GNS: Curator cannot withdraw more nSignal than they have') - }) - - it('reject burning if under slippage', async function () { - // Get current curator name signal - const curatorNSignal = await gns.getCuratorSignal(subgraph.id, other.address) - - // Withdraw - const { 1: expectedTokens } = await gns.nSignalToTokens(subgraph.id, curatorNSignal) - - // Force a revert by asking 1 more token than the function will return - const tx = gns.connect(other).burnSignal(subgraph.id, curatorNSignal, expectedTokens.add(1)) - await expect(tx).revertedWith('Slippage protection') - }) - }) - - describe('transferSignal()', function () { - let subgraph: Subgraph - let otherNSignal: BigNumber - - beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - await mintSignal(other, subgraph.id, tokens10000, gns, curation) - otherNSignal = await gns.getCuratorSignal(subgraph.id, other.address) - }) - - it('should transfer signal from one curator to another', async function () { - await transferSignal(subgraph.id, other, another, otherNSignal) - }) - it('should fail when transferring to zero address', async function () { - const tx = gns.connect(other).transferSignal(subgraph.id, ethers.constants.AddressZero, otherNSignal) - await expect(tx).revertedWith('GNS: Curator cannot transfer to the zero address') - }) - it('should fail when name signal is disabled', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - const tx = gns.connect(other).transferSignal(subgraph.id, another.address, otherNSignal) - await expect(tx).revertedWith('GNS: Must be active') - }) - it('should fail if you try to transfer on a non existing name', async function () { - const subgraphID = randomHexBytes(32) - const tx = gns.connect(other).transferSignal(subgraphID, another.address, otherNSignal) - await expect(tx).revertedWith('GNS: Must be active') - }) - it('should fail when the curator tries to transfer more signal than they have', async function () { - const tx = gns.connect(other).transferSignal(subgraph.id, another.address, otherNSignal.add(otherNSignal)) - await expect(tx).revertedWith('GNS: Curator transfer amount exceeds balance') - }) - }) - describe('withdraw()', function () { - let subgraph: Subgraph - - beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - await mintSignal(other, subgraph.id, tokens10000, gns, curation) - }) - - it('should withdraw GRT from a disabled name signal', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - await withdraw(other, subgraph.id) - }) - - it('should fail if not disabled', async function () { - const tx = gns.connect(other).withdraw(subgraph.id) - await expect(tx).revertedWith('GNS: Must be disabled first') - }) - - it('should fail when there is no more GRT to withdraw', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - await withdraw(other, subgraph.id) - const tx = gns.connect(other).withdraw(subgraph.id) - await expect(tx).revertedWith('GNS: No more GRT to withdraw') - }) - - it('should fail if the curator has no nSignal', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - const tx = gns.connect(me).withdraw(subgraph.id) - await expect(tx).revertedWith('GNS: No signal to withdraw GRT') - }) - }) - - describe('multiple minting', function () { - it('should mint less signal every time due to the bonding curve', async function () { - const tokensToDepositMany = [ - toGRT('1000'), // should mint if we start with number above minimum deposit - toGRT('1000'), // every time it should mint less GCS due to bonding curve... - toGRT('1.06'), // should mint minimum deposit including tax - toGRT('1000'), - toGRT('1000'), - toGRT('2000'), - toGRT('2000'), - toGRT('123'), - ] - const subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - // State updated - const curationTaxPercentage = await curation.curationTaxPercentage() - - for (const tokensToDeposit of tokensToDepositMany) { - const beforeSubgraph = await gns.subgraphs(subgraph.id) - expect(newSubgraph0.subgraphDeploymentID).eq(beforeSubgraph.subgraphDeploymentID) - - const curationTax = toBN(curationTaxPercentage).mul(tokensToDeposit).div(toBN(1000000)) - const expectedNSignal = await calcGNSBondingCurve( - beforeSubgraph.nSignal, - beforeSubgraph.vSignal, - tokensToDeposit.sub(curationTax), - beforeSubgraph.subgraphDeploymentID, - ) - const tx = await mintSignal(me, subgraph.id, tokensToDeposit, gns, curation) - const receipt = await tx.wait() - const event: Event = receipt.events.pop() - const nSignalCreated = event.args['nSignalCreated'] - expect(toRound(expectedNSignal)).eq(toRound(toFloat(nSignalCreated))) - } - }) - - it('should mint when using the edge case of linear function', async function () { - // Setup edge case like linear function: 1 vSignal = 1 nSignal = 1 token - await curation.connect(governor).setMinimumCurationDeposit(toGRT('1')) - await curation.connect(governor).setDefaultReserveRatio(1000000) - // note - reserve ratio is already set to 1000000 in GNS - - const tokensToDepositMany = [ - toGRT('1000'), // should mint if we start with number above minimum deposit - toGRT('1000'), // every time it should mint less GCS due to bonding curve... - toGRT('1000'), - toGRT('1000'), - toGRT('2000'), - toGRT('2000'), - toGRT('123'), - toGRT('1'), // should mint below minimum deposit - ] - - const subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - // State updated - for (const tokensToDeposit of tokensToDepositMany) { - await mintSignal(me, subgraph.id, tokensToDeposit, gns, curation) - } - }) - }) - }) - - describe('Two named subgraphs point to the same subgraph deployment ID', function () { - it('handle initialization under minimum signal values', async function () { - await curation.connect(governor).setMinimumCurationDeposit(toGRT('1')) - - // Publish a named subgraph-0 -> subgraphDeployment0 - const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - // Curate on the first subgraph - await gns.connect(me).mintSignal(subgraph0.id, toGRT('90000'), 0) - - // Publish a named subgraph-1 -> subgraphDeployment0 - const subgraph1 = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - // Curate on the second subgraph should work - await gns.connect(me).mintSignal(subgraph1.id, toGRT('10'), 0) - }) - }) - - describe('batch calls', function () { - it('should publish new subgraph and mint signal in single transaction', async function () { - // Create a subgraph - const tx1 = await gns.populateTransaction.publishNewSubgraph( - newSubgraph0.subgraphDeploymentID, - newSubgraph0.versionMetadata, - newSubgraph0.subgraphMetadata, - ) - // Curate on the subgraph - const subgraphID = await buildSubgraphId(me.address, await gns.nextAccountSeqID(me.address), graph.chainId) - const tx2 = await gns.populateTransaction.mintSignal(subgraphID, toGRT('90000'), 0) - - // Batch send transaction - await gns.connect(me).multicall([tx1.data, tx2.data]) - }) - - it('should revert if batching a call to non-authorized function', async function () { - // Call a forbidden function - const tx1 = await gns.populateTransaction.setOwnerTaxPercentage(100) - - // Create a subgraph - const tx2 = await gns.populateTransaction.publishNewSubgraph( - newSubgraph0.subgraphDeploymentID, - newSubgraph0.versionMetadata, - newSubgraph0.subgraphMetadata, - ) - - // Batch send transaction - const tx = gns.connect(me).multicall([tx1.data, tx2.data]) - await expect(tx).revertedWith('Only Controller governor') - }) - - it('should revert if batching a call to initialize', async function () { - // Call a forbidden function - const tx1 = await gns.populateTransaction.initialize(me.address, me.address) - - // Create a subgraph - const tx2 = await gns.populateTransaction.publishNewSubgraph( - newSubgraph0.subgraphDeploymentID, - newSubgraph0.versionMetadata, - newSubgraph0.subgraphMetadata, - ) - - // Batch send transaction - const tx = gns.connect(me).multicall([tx1.data, tx2.data]) - await expect(tx).revertedWith('Only implementation') - }) - - it('should revert if trying to call a private function', async function () { - // Craft call a private function - const hash = ethers.utils.id('_setOwnerTaxPercentage(uint32)') - const functionHash = hash.slice(0, 10) - const calldata = ethers.utils.arrayify(ethers.utils.defaultAbiCoder.encode(['uint32'], ['100'])) - const bogusPayload = ethers.utils.concat([functionHash, calldata]) - - // Create a subgraph - const tx2 = await gns.populateTransaction.publishNewSubgraph( - newSubgraph0.subgraphDeploymentID, - newSubgraph0.versionMetadata, - newSubgraph0.subgraphMetadata, - ) - - // Batch send transaction - const tx = gns.connect(me).multicall([bogusPayload, tx2.data]) - - // Under coverage, the error message may be different due to instrumentation - if (isRunningUnderCoverage()) { - await expect(tx).to.be.reverted - } else { - await expect(tx).revertedWith("function selector was not recognized and there's no fallback function") - } - }) - }) - - describe('NFT descriptor', function () { - it('cannot be minted by an account that is not the minter (i.e. GNS)', async function () { - const tx = subgraphNFT.connect(me).mint(me.address, 1) - await expect(tx).revertedWith('Must be a minter') - }) - it('cannot be burned by an account that is not the minter (i.e. GNS)', async function () { - const tx = subgraphNFT.connect(me).burn(1) - await expect(tx).revertedWith('Must be a minter') - }) - it('with token descriptor', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - const tokenURI = await subgraphNFT.connect(me).tokenURI(subgraph0.id) - - const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) - expect(sub.ipfsHash).eq(tokenURI) - }) - - it('with token descriptor and baseURI', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - await subgraphNFT.connect(governor).setBaseURI('ipfs://') - const tokenURI = await subgraphNFT.connect(me).tokenURI(subgraph0.id) - - const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) - expect('ipfs://' + sub.ipfsHash).eq(tokenURI) - }) - - it('without token descriptor', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - await subgraphNFT.connect(governor).setTokenDescriptor(AddressZero) - const tokenURI = await subgraphNFT.connect(me).tokenURI(subgraph0.id) - - const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) - expect(sub.bytes32).eq(tokenURI) - }) - - it('without token descriptor and baseURI', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - await subgraphNFT.connect(governor).setTokenDescriptor(AddressZero) - await subgraphNFT.connect(governor).setBaseURI('ipfs://') - const tokenURI = await subgraphNFT.connect(me).tokenURI(subgraph0.id) - - const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) - expect('ipfs://' + sub.bytes32).eq(tokenURI) - }) - - it('without token descriptor and 0x0 metadata', async function () { - const newSubgraphNoMetadata = buildSubgraph() - newSubgraphNoMetadata.subgraphMetadata = HashZero - const subgraph0 = await publishNewSubgraph(me, newSubgraphNoMetadata, gns, graph.chainId) - - await subgraphNFT.connect(governor).setTokenDescriptor(AddressZero) - await subgraphNFT.connect(governor).setBaseURI('ipfs://') - const tokenURI = await subgraphNFT.connect(me).tokenURI(subgraph0.id) - expect('ipfs://' + subgraph0.id).eq(tokenURI) - }) - }) - describe('Legacy subgraph migration', function () { - it('migrates a legacy subgraph', async function () { - const seqID = toBN('2') - await legacyGNSMock.connect(me).createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) - const tx = legacyGNSMock.connect(me).migrateLegacySubgraph(me.address, seqID, newSubgraph0.subgraphMetadata) - await expect(tx).emit(legacyGNSMock, ' LegacySubgraphClaimed').withArgs(me.address, seqID) - const expectedSubgraphID = buildLegacySubgraphId(me.address, seqID) - const migratedSubgraphDeploymentID = await legacyGNSMock.getSubgraphDeploymentID(expectedSubgraphID) - const migratedNSignal = await legacyGNSMock.getSubgraphNSignal(expectedSubgraphID) - expect(migratedSubgraphDeploymentID).eq(newSubgraph0.subgraphDeploymentID) - expect(migratedNSignal).eq(toBN('1000')) - - const subgraphNFTAddress = await legacyGNSMock.subgraphNFT() - const subgraphNFT = loadContractAt('SubgraphNFT', subgraphNFTAddress) - const tokenURI = await subgraphNFT.connect(me).tokenURI(expectedSubgraphID) - - const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) - expect(sub.ipfsHash).eq(tokenURI) - }) - it('refuses to migrate an already migrated subgraph', async function () { - const seqID = toBN('2') - await legacyGNSMock.connect(me).createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) - let tx = legacyGNSMock.connect(me).migrateLegacySubgraph(me.address, seqID, newSubgraph0.subgraphMetadata) - await expect(tx).emit(legacyGNSMock, ' LegacySubgraphClaimed').withArgs(me.address, seqID) - tx = legacyGNSMock.connect(me).migrateLegacySubgraph(me.address, seqID, newSubgraph0.subgraphMetadata) - await expect(tx).revertedWith('GNS: Subgraph was already claimed') - }) - }) - describe('Legacy subgraph view functions', function () { - it('isLegacySubgraph returns whether a subgraph is legacy or not', async function () { - const seqID = toBN('2') - const subgraphId = buildLegacySubgraphId(me.address, seqID) - await legacyGNSMock.connect(me).createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) - await legacyGNSMock.connect(me).migrateLegacySubgraph(me.address, seqID, newSubgraph0.subgraphMetadata) - - expect(await legacyGNSMock.isLegacySubgraph(subgraphId)).eq(true) - - const subgraph0 = await publishNewSubgraph(me, newSubgraph0, legacyGNSMock, graph.chainId) - expect(await legacyGNSMock.isLegacySubgraph(subgraph0.id)).eq(false) - }) - it('getLegacySubgraphKey returns the account and seqID for a legacy subgraph', async function () { - const seqID = toBN('2') - const subgraphId = buildLegacySubgraphId(me.address, seqID) - await legacyGNSMock.connect(me).createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) - await legacyGNSMock.connect(me).migrateLegacySubgraph(me.address, seqID, newSubgraph0.subgraphMetadata) - const [account, id] = await legacyGNSMock.getLegacySubgraphKey(subgraphId) - expect(account).eq(me.address) - expect(id).eq(seqID) - }) - it('getLegacySubgraphKey returns zero values for a non-legacy subgraph', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0, legacyGNSMock, graph.chainId) - const [account, id] = await legacyGNSMock.getLegacySubgraphKey(subgraph0.id) - expect(account).eq(AddressZero) - expect(id).eq(toBN('0')) - }) - }) - describe('Subgraph transfer to L2', function () { - const publishAndCurateOnSubgraph = async function (): Promise { - // Publish a named subgraph-0 -> subgraphDeployment0 - const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - // Curate on the subgraph - await gns.connect(me).mintSignal(subgraph0.id, toGRT('90000'), 0) - // Add an additional curator that is not the owner - await gns.connect(other).mintSignal(subgraph0.id, toGRT('10000'), 0) - return subgraph0 - } - - const publishCurateAndSendSubgraph = async function ( - beforeTransferCallback?: (_subgraphID: string) => Promise, - ): Promise { - const subgraph0 = await publishAndCurateOnSubgraph() - - if (beforeTransferCallback != null && subgraph0.id) { - await beforeTransferCallback(subgraph0.id) - } - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const subgraphBefore = await gns.subgraphs(subgraph0.id) - const curatedTokens = await gns.subgraphTokens(subgraph0.id) - const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) - const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) - - const tx = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) - return subgraph0 - } - const publishAndCurateOnLegacySubgraph = async function (seqID: BigNumber): Promise { - await legacyGNSMock.connect(me).createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) - // The legacy subgraph must be claimed - const migrateTx = legacyGNSMock - .connect(me) - .migrateLegacySubgraph(me.address, seqID, newSubgraph0.subgraphMetadata) - await expect(migrateTx).emit(legacyGNSMock, ' LegacySubgraphClaimed').withArgs(me.address, seqID) - const subgraphID = buildLegacySubgraphId(me.address, seqID) - - // Curate on the subgraph - await legacyGNSMock.connect(me).mintSignal(subgraphID, toGRT('10000'), 0) - - return subgraphID - } - - describe('sendSubgraphToL2', function () { - it('sends tokens and calldata to L2 through the GRT bridge, for a desired L2 owner', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const curatedTokens = await grt.balanceOf(curation.address) - const subgraphBefore = await gns.subgraphs(subgraph0.id) - - const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns - .connect(me) - .sendSubgraphToL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) - await expect(tx) - .emit(gns, 'SubgraphSentToL2') - .withArgs(subgraph0.id, me.address, other.address, expectedSentToL2) - - const expectedRemainingTokens = curatedTokens.sub(expectedSentToL2) - const subgraphAfter = await gns.subgraphs(subgraph0.id) - expect(subgraphAfter.vSignal).eq(0) - expect(await grt.balanceOf(gns.address)).eq(expectedRemainingTokens) - expect(subgraphAfter.disabled).eq(true) - expect(subgraphAfter.withdrawableGRT).eq(expectedRemainingTokens) - - const transferred = await gns.subgraphTransferredToL2(subgraph0.id) - expect(transferred).eq(true) - - const expectedCallhookData = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(0), subgraph0.id, other.address], // code = 0 means RECEIVE_SUBGRAPH_CODE - ) - const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - gns.address, - l2GNSMock.address, - expectedSentToL2, - expectedCallhookData, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(gns.address, l2GRTGatewayMock.address, toBN(1), expectedL2Data) - }) - it('sends tokens and calldata for a legacy subgraph to L2 through the GRT bridge', async function () { - const seqID = toBN('2') - const subgraphID = await publishAndCurateOnLegacySubgraph(seqID) - - const subgraphBefore = await legacyGNSMock.legacySubgraphData(me.address, seqID) - const curatedTokens = await legacyGNSMock.subgraphTokens(subgraphID) - const beforeOwnerSignal = await legacyGNSMock.getCuratorSignal(subgraphID, me.address) - const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = legacyGNSMock - .connect(me) - .sendSubgraphToL2(subgraphID, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx) - .emit(legacyGNSMock, 'SubgraphSentToL2') - .withArgs(subgraphID, me.address, other.address, expectedSentToL2) - - const expectedRemainingTokens = curatedTokens.sub(expectedSentToL2) - const subgraphAfter = await legacyGNSMock.legacySubgraphData(me.address, seqID) - expect(subgraphAfter.vSignal).eq(0) - expect(await grt.balanceOf(legacyGNSMock.address)).eq(expectedRemainingTokens) - expect(subgraphAfter.disabled).eq(true) - expect(subgraphAfter.withdrawableGRT).eq(expectedRemainingTokens) - - const transferred = await legacyGNSMock.subgraphTransferredToL2(subgraphID) - expect(transferred).eq(true) - - const expectedCallhookData = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(0), subgraphID, other.address], // code = 0 means RECEIVE_SUBGRAPH_CODE - ) - - const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - legacyGNSMock.address, - l2GNSMock.address, - expectedSentToL2, - expectedCallhookData, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(legacyGNSMock.address, l2GRTGatewayMock.address, toBN(1), expectedL2Data) - }) - it('rejects calls from someone who is not the subgraph owner', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns - .connect(other) - .sendSubgraphToL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx).revertedWith('GNS: Must be authorized') - }) - it('rejects calls for a subgraph that was already sent', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const subgraphBefore = await gns.subgraphs(subgraph0.id) - const curatedTokens = await gns.subgraphTokens(subgraph0.id) - const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) - const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) - - const tx2 = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx2).revertedWith('ALREADY_DONE') - }) - it('rejects a call for a subgraph that is deprecated', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - await gns.connect(me).deprecateSubgraph(subgraph0.id) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - await expect(tx).revertedWith('GNS: Must be active') - }) - it('rejects a call for a subgraph that does not exist', async function () { - const subgraphId = await buildSubgraphId(me.address, toBN(100), graph.chainId) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns.connect(me).sendSubgraphToL2(subgraphId, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - await expect(tx).revertedWith('GNS: Must be active') - }) - it('rejects calls with more ETH than maxSubmissionCost + maxGas * gasPriceBid', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)).add(toBN('1')), - }) - await expect(tx).revertedWith('INVALID_ETH_VALUE') - }) - it('does not allow curators to burn signal after sending', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const subgraphBefore = await gns.subgraphs(subgraph0.id) - const curatedTokens = await gns.subgraphTokens(subgraph0.id) - const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) - const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) - - const tx2 = gns.connect(me).burnSignal(subgraph0.id, toBN(1), toGRT('0')) - await expect(tx2).revertedWith('GNS: Must be active') - const tx3 = gns.connect(other).burnSignal(subgraph0.id, toBN(1), toGRT('0')) - await expect(tx3).revertedWith('GNS: Must be active') - }) - it('does not allow curators to transfer signal after sending', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const subgraphBefore = await gns.subgraphs(subgraph0.id) - const curatedTokens = await gns.subgraphTokens(subgraph0.id) - const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) - const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) - - const tx2 = gns.connect(me).transferSignal(subgraph0.id, other.address, toBN(1)) - await expect(tx2).revertedWith('GNS: Must be active') - const tx3 = gns.connect(other).transferSignal(subgraph0.id, me.address, toBN(1)) - await expect(tx3).revertedWith('GNS: Must be active') - }) - it('does not allow the owner to withdraw GRT after sending', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const subgraphBefore = await gns.subgraphs(subgraph0.id) - const curatedTokens = await gns.subgraphTokens(subgraph0.id) - const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) - const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) - - const tx2 = gns.connect(me).withdraw(subgraph0.id) - await expect(tx2).revertedWith('GNS: No signal to withdraw GRT') - }) - it('allows a curator that is not the owner to withdraw GRT after sending', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const subgraphBefore = await gns.subgraphs(subgraph0.id) - const curatedTokens = await gns.subgraphTokens(subgraph0.id) - const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) - const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) - const beforeOtherSignal = await gns.getCuratorSignal(subgraph0.id, other.address) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - const tx = gns.connect(me).sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) - - const remainingTokens = (await gns.subgraphs(subgraph0.id)).withdrawableGRT - const tx2 = gns.connect(other).withdraw(subgraph0.id) - await expect(tx2) - .emit(gns, 'GRTWithdrawn') - .withArgs(subgraph0.id, other.address, beforeOtherSignal, remainingTokens) - }) - }) - describe('sendCuratorBalanceToBeneficiaryOnL2', function () { - it('sends a transaction with a curator balance to the L2GNS using the L1 gateway', async function () { - const subgraph0 = await publishCurateAndSendSubgraph() - const afterSubgraph = await gns.subgraphs(subgraph0.id) - const curatorTokens = afterSubgraph.withdrawableGRT - - const expectedCallhookData = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(1), subgraph0.id, another.address], // code = 1 means RECEIVE_CURATOR_BALANCE_CODE - ) - const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - gns.address, - l2GNSMock.address, - curatorTokens, - expectedCallhookData, - ) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const tx = gns - .connect(other) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, another.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - // seqNum (third argument in the event) is 2, because number 1 was when the subgraph was sent to L2 - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(gns.address, l2GRTGatewayMock.address, toBN('2'), expectedL2Data) - await expect(tx) - .emit(gns, 'CuratorBalanceSentToL2') - .withArgs(subgraph0.id, other.address, another.address, curatorTokens) - }) - it('sets the curator signal to zero so it cannot be called twice', async function () { - const subgraph0 = await publishCurateAndSendSubgraph() - const afterSubgraph = await gns.subgraphs(subgraph0.id) - const curatorTokens = afterSubgraph.withdrawableGRT - - const expectedCallhookData = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(1), subgraph0.id, other.address], // code = 1 means RECEIVE_CURATOR_BALANCE_CODE - ) - const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - gns.address, - l2GNSMock.address, - curatorTokens, - expectedCallhookData, - ) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const tx = gns - .connect(other) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - // seqNum (third argument in the event) is 2, because number 1 was when the subgraph was sent to L2 - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(gns.address, l2GRTGatewayMock.address, toBN('2'), expectedL2Data) - - const tx2 = gns - .connect(other) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx2).revertedWith('NO_SIGNAL') - }) - it('sets the curator signal to zero so they cannot withdraw', async function () { - // Under coverage, skip this test as it has issues with BigNumber values - if (isRunningUnderCoverage()) { - this.skip() - return - } - - const subgraph0 = await publishCurateAndSendSubgraph(async (_subgraphId) => { - // We add another curator before transferring, so the the subgraph doesn't - // run out of withdrawable GRT and we can test that it denies the specific curator - // because they have sent their signal to L2, not because the subgraph is out of GRT. - await gns.connect(another).mintSignal(_subgraphId, toGRT('1000'), toBN(0)) - }) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - await gns - .connect(other) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - const tx = gns.connect(other).withdraw(subgraph0.id) - await expect(tx).revertedWith('GNS: No signal to withdraw GRT') - }) - it('gives each curator an amount of tokens proportional to their nSignal', async function () { - // Under coverage, skip this test as it has issues with BigNumber values - if (isRunningUnderCoverage()) { - this.skip() - return - } - - let beforeOtherNSignal: BigNumber - let beforeAnotherNSignal: BigNumber - const subgraph0 = await publishCurateAndSendSubgraph(async (subgraphID: string) => { - beforeOtherNSignal = await gns.getCuratorSignal(subgraphID, other.address) - await gns.connect(another).mintSignal(subgraphID, toGRT('10000'), 0) - beforeAnotherNSignal = await gns.getCuratorSignal(subgraphID, another.address) - }) - const afterSubgraph = await gns.subgraphs(subgraph0.id) - - // Compute how much is owed to each curator - const curator1Tokens = beforeOtherNSignal.mul(afterSubgraph.withdrawableGRT).div(afterSubgraph.nSignal) - const curator2Tokens = beforeAnotherNSignal.mul(afterSubgraph.withdrawableGRT).div(afterSubgraph.nSignal) - - const expectedCallhookData1 = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(1), subgraph0.id, other.address], // code = 1 means RECEIVE_CURATOR_BALANCE_CODE - ) - const expectedCallhookData2 = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(1), subgraph0.id, another.address], // code = 1 means RECEIVE_CURATOR_BALANCE_CODE - ) - const expectedL2Data1 = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - gns.address, - l2GNSMock.address, - curator1Tokens, - expectedCallhookData1, - ) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const tx = gns - .connect(other) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - // seqNum (third argument in the event) is 2, because number 1 was when the subgraph was sent to L2 - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(gns.address, l2GRTGatewayMock.address, toBN('2'), expectedL2Data1) - - // Accept slight numerical errors given how we compute the amount of tokens to send - const curator2TokensUpdated = (await gns.subgraphs(subgraph0.id)).withdrawableGRT - expect(toRound(toFloat(curator2TokensUpdated))).to.equal(toRound(toFloat(curator2Tokens))) - const expectedL2Data2 = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - gns.address, - l2GNSMock.address, - curator2TokensUpdated, - expectedCallhookData2, - ) - const tx2 = gns - .connect(another) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, another.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - // seqNum (third argument in the event) is 3 now - await expect(tx2) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(gns.address, l2GRTGatewayMock.address, toBN('3'), expectedL2Data2) - }) - it('rejects calls for a subgraph that was not sent to L2', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const tx = gns - .connect(me) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - await expect(tx).revertedWith('!TRANSFERRED') - }) - - it('rejects calls for a subgraph that was deprecated', async function () { - const subgraph0 = await publishAndCurateOnSubgraph() - - await helpers.mine(256) - await gns.connect(me).deprecateSubgraph(subgraph0.id) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const tx = gns - .connect(me) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - await expect(tx).revertedWith('!TRANSFERRED') - }) - it('rejects calls with zero maxSubmissionCost', async function () { - const subgraph0 = await publishCurateAndSendSubgraph() - - const maxSubmissionCost = toBN('0') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const tx = gns - .connect(me) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - await expect(tx).revertedWith('NO_SUBMISSION_COST') - }) - it('rejects calls with more ETH than maxSubmissionCost + maxGas * gasPriceBid', async function () { - const subgraph0 = await publishCurateAndSendSubgraph() - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const tx = gns - .connect(me) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)).add(toBN('1')), - }) - - await expect(tx).revertedWith('INVALID_ETH_VALUE') - }) - it('rejects calls if the curator has withdrawn the GRT', async function () { - const subgraph0 = await publishCurateAndSendSubgraph() - - await gns.connect(other).withdraw(subgraph0.id) - - const maxSubmissionCost = toBN('100') - const maxGas = toBN('10') - const gasPriceBid = toBN('20') - - const tx = gns - .connect(other) - .sendCuratorBalanceToBeneficiaryOnL2(subgraph0.id, another.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - - // seqNum (third argument in the event) is 2, because number 1 was when the subgraph was sent to L2 - await expect(tx).revertedWith('NO_SIGNAL') - }) - }) - }) -}) diff --git a/packages/contracts-test/tests/unit/l2/l2GNS.test.ts b/packages/contracts-test/tests/unit/l2/l2GNS.test.ts deleted file mode 100644 index a8b1b67a9..000000000 --- a/packages/contracts-test/tests/unit/l2/l2GNS.test.ts +++ /dev/null @@ -1,777 +0,0 @@ -import { L2GNS } from '@graphprotocol/contracts' -import { L2GraphTokenGateway } from '@graphprotocol/contracts' -import { L2Curation } from '@graphprotocol/contracts' -import { GraphToken } from '@graphprotocol/contracts' -import { IL2Staking } from '@graphprotocol/contracts' -import { L1GNS, L1GraphTokenGateway } from '@graphprotocol/contracts' -import { - buildSubgraph, - buildSubgraphId, - deriveChannelKey, - GraphNetworkContracts, - helpers, - PublishSubgraph, - randomHexBytes, - Subgraph, - toBN, - toGRT, -} from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { BigNumber, ContractTransaction, ethers } from 'ethers' -import { defaultAbiCoder, parseEther } from 'ethers/lib/utils' -import hre from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' -import { - burnSignal, - DEFAULT_RESERVE_RATIO, - deprecateSubgraph, - mintSignal, - publishNewSubgraph, - publishNewVersion, -} from '../lib/gnsUtils' - -const { HashZero } = ethers.constants - -interface L1SubgraphParams { - l1SubgraphId: string - curatedTokens: BigNumber - subgraphMetadata: string - versionMetadata: string -} - -describe('L2GNS', () => { - const graph = hre.graph() - let me: SignerWithAddress - let attacker: SignerWithAddress - let other: SignerWithAddress - let governor: SignerWithAddress - let fixture: NetworkFixture - - let fixtureContracts: GraphNetworkContracts - let l1MockContracts: GraphNetworkContracts - let l1GRTMock: GraphToken - let l1GNSMock: L1GNS - let l1GRTGatewayMock: L1GraphTokenGateway - let l2GraphTokenGateway: L2GraphTokenGateway - let gns: L2GNS - let curation: L2Curation - let grt: GraphToken - let staking: IL2Staking - - let newSubgraph0: PublishSubgraph - let newSubgraph1: PublishSubgraph - - const tokens1000 = toGRT('1000') - const tokens10000 = toGRT('10000') - const tokens100000 = toGRT('100000') - const curationTaxPercentage = 50000 - - const gatewayFinalizeTransfer = async function ( - from: string, - to: string, - amount: BigNumber, - callhookData: string, - ): Promise { - const l1GRTGatewayMockL2Alias = await helpers.getL2SignerFromL1(l1GRTGatewayMock.address) - // Eth for gas: - await helpers.setBalance(await l1GRTGatewayMockL2Alias.getAddress(), parseEther('1')) - - const tx = l2GraphTokenGateway - .connect(l1GRTGatewayMockL2Alias) - .finalizeInboundTransfer(l1GRTMock.address, from, to, amount, callhookData) - return tx - } - - const defaultL1SubgraphParams = async function (): Promise { - return { - l1SubgraphId: await buildSubgraphId(me.address, toBN('1'), graph.chainId), - curatedTokens: toGRT('1337'), - subgraphMetadata: randomHexBytes(), - versionMetadata: randomHexBytes(), - } - } - const transferMockSubgraphFromL1 = async function ( - l1SubgraphId: string, - curatedTokens: BigNumber, - subgraphMetadata: string, - versionMetadata: string, - ) { - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - await gns - .connect(me) - .finishSubgraphTransferFromL1(l2SubgraphId, newSubgraph0.subgraphDeploymentID, subgraphMetadata, versionMetadata) - } - - before(async function () { - newSubgraph0 = buildSubgraph() - ;[me, attacker, other] = await graph.getTestAccounts() - ;({ governor } = await graph.getNamedAccounts()) - - fixture = new NetworkFixture(graph.provider) - - // Deploy L2 - fixtureContracts = await fixture.load(governor, true) - l2GraphTokenGateway = fixtureContracts.L2GraphTokenGateway as L2GraphTokenGateway - gns = fixtureContracts.L2GNS as L2GNS - curation = fixtureContracts.L2Curation as L2Curation - grt = fixtureContracts.GraphToken as GraphToken - staking = fixtureContracts.L2Staking as unknown as IL2Staking - - // Deploy L1 mock - l1MockContracts = await fixture.loadMock(false) - l1GRTMock = l1MockContracts.GraphToken as GraphToken - l1GNSMock = l1MockContracts.L1GNS as L1GNS - l1GRTGatewayMock = l1MockContracts.L1GraphTokenGateway as L1GraphTokenGateway - - // Deploy L2 arbitrum bridge - await fixture.loadL2ArbitrumBridge(governor) - - // Configure L2 bridge - await fixture.configureL2Bridge(governor, fixtureContracts, l1MockContracts) - - await grt.connect(governor).mint(me.address, toGRT('10000')) - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - // Adapted from the L1 GNS tests but allowing curating to a pre-curated subgraph deployment - describe('publishNewVersion', function () { - let subgraph: Subgraph - - beforeEach(async () => { - newSubgraph0 = buildSubgraph() - newSubgraph1 = buildSubgraph() - // Give some funds to the signers and approve gns contract to use funds on signers behalf - await grt.connect(governor).mint(me.address, tokens100000) - await grt.connect(governor).mint(other.address, tokens100000) - await grt.connect(me).approve(gns.address, tokens100000) - await grt.connect(me).approve(curation.address, tokens100000) - await grt.connect(other).approve(gns.address, tokens100000) - await grt.connect(other).approve(curation.address, tokens100000) - // Update curation tax to test the functionality of it in disableNameSignal() - await curation.connect(governor).setCurationTaxPercentage(curationTaxPercentage) - subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - await mintSignal(me, subgraph.id, tokens10000, gns, curation) - }) - - it('should publish a new version on an existing subgraph', async function () { - await publishNewVersion(me, subgraph.id, newSubgraph1, gns, curation) - }) - - it('should publish a new version on an existing subgraph when owner tax is zero', async function () { - await gns.connect(governor).setOwnerTaxPercentage(0) - await publishNewVersion(me, subgraph.id, newSubgraph1, gns, curation) - }) - - it('should publish a new version on an existing subgraph with no current signal', async function () { - const emptySignalSubgraph = await publishNewSubgraph(me, buildSubgraph(), gns, graph.chainId) - await publishNewVersion(me, emptySignalSubgraph.id, newSubgraph1, gns, curation) - }) - - it('should reject a new version with the same subgraph deployment ID', async function () { - const tx = gns - .connect(me) - [ - 'publishNewVersion(uint256,bytes32,bytes32,uint256)' - ](subgraph.id, newSubgraph0.subgraphDeploymentID, newSubgraph0.versionMetadata, 0) - await expect(tx).revertedWith('GNS: Cannot publish a new version with the same subgraph deployment ID') - }) - - it('should reject publishing a version to a subgraph that does not exist', async function () { - const tx = gns - .connect(me) - [ - 'publishNewVersion(uint256,bytes32,bytes32,uint256)' - ](randomHexBytes(32), newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata, 0) - await expect(tx).revertedWith('ERC721: owner query for nonexistent token') - }) - - it('reject if not the owner', async function () { - const tx = gns - .connect(other) - [ - 'publishNewVersion(uint256,bytes32,bytes32,uint256)' - ](subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata, 0) - await expect(tx).revertedWith('GNS: Must be authorized') - }) - - it('should NOT fail when upgrade tries to point to a pre-curated', async function () { - // Curate directly to the deployment - await curation.connect(me).mint(newSubgraph1.subgraphDeploymentID, tokens1000, 0) - - await publishNewVersion(me, subgraph.id, newSubgraph1, gns, curation) - }) - - it('should upgrade version when there is no signal with no signal migration', async function () { - await burnSignal(me, subgraph.id, gns, curation) - const tx = gns - .connect(me) - [ - 'publishNewVersion(uint256,bytes32,bytes32,uint256)' - ](subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata, 0) - await expect(tx) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - }) - - it('should fail when subgraph is deprecated', async function () { - await deprecateSubgraph(me, subgraph.id, gns, curation, grt) - const tx = gns - .connect(me) - [ - 'publishNewVersion(uint256,bytes32,bytes32,uint256)' - ](subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata, 0) - // NOTE: deprecate burns the Subgraph NFT, when someone wants to publish a new version it won't find it - await expect(tx).revertedWith('ERC721: owner query for nonexistent token') - }) - - it('should protect against front-running with slippage protection', async function () { - // Give attacker massive funds for the attack - await grt.connect(governor).mint(attacker.address, toGRT('1000000')) - await grt.connect(attacker).approve(curation.address, toGRT('1000000')) - await grt.connect(attacker).approve(staking.address, toGRT('1000000')) - - // Step 1: Set minimum curation deposit to 1 wei and disable tax for accurate attack test - await curation.connect(governor).setMinimumCurationDeposit(1) - await curation.connect(governor).setCurationTaxPercentage(0) - await curation.connect(attacker).mint(newSubgraph1.subgraphDeploymentID, 1, 0) - - // Step 2: Attacker stakes and creates allocation on the deployment - await staking.connect(attacker).stake(toGRT('100000')) - const channelKey = deriveChannelKey() - await staking - .connect(attacker) - .allocateFrom( - attacker.address, - newSubgraph1.subgraphDeploymentID, - toGRT('10000'), - channelKey.address, - randomHexBytes(32), - await channelKey.generateProof(attacker.address), - ) - - // Calculate expected signal from burning current subgraph (before attack) - const subgraphData = await gns.subgraphs(subgraph.id) - const tokensFromOldCurve = await curation.signalToTokens(newSubgraph0.subgraphDeploymentID, subgraphData.vSignal) - const expectedSignalWithoutAttack = await curation.tokensToSignalNoTax( - newSubgraph1.subgraphDeploymentID, - tokensFromOldCurve, - ) - - // Step 3: Attacker inflates the curation pool by collecting massive query fees - // Simulate the token collection flow: TokenUtils.pushTokens + curation.collect - const collectTokens = toGRT('50000') // 50k GRT collected as fees - - // Mint tokens to curation contract (simulating TokenUtils.pushTokens from staking) - await grt.connect(governor).mint(curation.address, collectTokens) - - // Only the subgraph service may call collect, so point it at the attacker for this step - await curation.connect(governor).setSubgraphService(attacker.address) - await curation.connect(attacker).collect(newSubgraph1.subgraphDeploymentID, collectTokens) - - // Now calculate signal after the attack - should be much less due to inflated pool - const actualSignalAfterAttack = await curation.tokensToSignalNoTax( - newSubgraph1.subgraphDeploymentID, - tokensFromOldCurve, - ) - - // Verify the attack worked - victim gets much less signal - expect(actualSignalAfterAttack).to.be.lt(expectedSignalWithoutAttack.div(10)) - - // Step 4: With slippage protection, the upgrade should fail when expecting reasonable signal - const tx = gns - .connect(me) - [ - 'publishNewVersion(uint256,bytes32,bytes32,uint256)' - ](subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata, expectedSignalWithoutAttack.div(2)) - await expect(tx).to.be.revertedWith('Slippage protection') - }) - - it('should succeed when slippage protection is satisfied', async function () { - // Give attacker funds and pre-curate the new deployment with some tokens - await grt.connect(governor).mint(attacker.address, toGRT('100')) - await grt.connect(attacker).approve(curation.address, toGRT('100')) - await curation.connect(attacker).mint(newSubgraph1.subgraphDeploymentID, toGRT('100'), 0) - - // Calculate the minimum signal we'll accept (allowing for some slippage) - const subgraphData = await gns.subgraphs(subgraph.id) - const tokensFromOldCurve = await curation.signalToTokens(newSubgraph0.subgraphDeploymentID, subgraphData.vSignal) - const expectedSignal = await curation.tokensToSignalNoTax(newSubgraph1.subgraphDeploymentID, tokensFromOldCurve) - const minSignalWithSlippage = expectedSignal.mul(95).div(100) // Accept 5% slippage - - // Upgrade should succeed with reasonable slippage protection - const tx = gns - .connect(me) - [ - 'publishNewVersion(uint256,bytes32,bytes32,uint256)' - ](subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata, minSignalWithSlippage) - await expect(tx) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - }) - - it('should succeed using L1 version (3-param) when deployment is NOT pre-curated', async function () { - // Use the 3-parameter version (inherited from GNS) - const tx = gns - .connect(me) - [ - 'publishNewVersion(uint256,bytes32,bytes32)' - ](subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - await expect(tx) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - }) - - it('should revert using L1 version (3-param) when deployment IS pre-curated', async function () { - // Pre-curate the new deployment - await curation.connect(me).mint(newSubgraph1.subgraphDeploymentID, tokens1000, 0) - - // Use the 3-parameter version (inherited from GNS) - should revert due to pre-curation check - const tx = gns - .connect(me) - [ - 'publishNewVersion(uint256,bytes32,bytes32)' - ](subgraph.id, newSubgraph1.subgraphDeploymentID, newSubgraph1.versionMetadata) - await expect(tx).revertedWith('GNS: Owner cannot point to a subgraphID that has been pre-curated') - }) - }) - - describe('receiving a subgraph from L1 (onTokenTransfer)', function () { - it('cannot be called by someone other than the L2GraphTokenGateway', async function () { - const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - const tx = gns.connect(me).onTokenTransfer(l1GNSMock.address, curatedTokens, callhookData) - await expect(tx).revertedWith('ONLY_GATEWAY') - }) - it('rejects calls if the L1 sender is not the L1GNS', async function () { - const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - const tx = gatewayFinalizeTransfer(me.address, gns.address, curatedTokens, callhookData) - - await expect(tx).revertedWith('ONLY_L1_GNS_THROUGH_BRIDGE') - }) - it('creates a subgraph in a disabled state', async function () { - const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - const tx = gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - - await expect(tx) - .emit(l2GraphTokenGateway, 'DepositFinalized') - .withArgs(l1GRTMock.address, l1GNSMock.address, gns.address, curatedTokens) - await expect(tx) - .emit(gns, 'SubgraphReceivedFromL1') - .withArgs(l1SubgraphId, l2SubgraphId, me.address, curatedTokens) - - const transferData = await gns.subgraphL2TransferData(l2SubgraphId) - const subgraphData = await gns.subgraphs(l2SubgraphId) - - expect(transferData.tokens).eq(curatedTokens) - expect(transferData.l2Done).eq(false) - expect(transferData.subgraphReceivedOnL2BlockNumber).eq(await helpers.latestBlock()) - - expect(subgraphData.vSignal).eq(0) - expect(subgraphData.nSignal).eq(0) - expect(subgraphData.subgraphDeploymentID).eq(HashZero) - expect(subgraphData.__DEPRECATED_reserveRatio).eq(DEFAULT_RESERVE_RATIO) - expect(subgraphData.disabled).eq(true) - expect(subgraphData.withdrawableGRT).eq(0) // Important so that it's not the same as a deprecated subgraph! - - expect(await gns.ownerOf(l2SubgraphId)).eq(me.address) - }) - it('does not conflict with a locally created subgraph', async function () { - const l2Subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - const tx = gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - - await expect(tx) - .emit(l2GraphTokenGateway, 'DepositFinalized') - .withArgs(l1GRTMock.address, l1GNSMock.address, gns.address, curatedTokens) - await expect(tx) - .emit(gns, 'SubgraphReceivedFromL1') - .withArgs(l1SubgraphId, l2SubgraphId, me.address, curatedTokens) - - const transferData = await gns.subgraphL2TransferData(l2SubgraphId) - const subgraphData = await gns.subgraphs(l2SubgraphId) - - expect(transferData.tokens).eq(curatedTokens) - expect(transferData.l2Done).eq(false) - expect(transferData.subgraphReceivedOnL2BlockNumber).eq(await helpers.latestBlock()) - - expect(subgraphData.vSignal).eq(0) - expect(subgraphData.nSignal).eq(0) - expect(subgraphData.subgraphDeploymentID).eq(HashZero) - expect(subgraphData.__DEPRECATED_reserveRatio).eq(DEFAULT_RESERVE_RATIO) - expect(subgraphData.disabled).eq(true) - expect(subgraphData.withdrawableGRT).eq(0) // Important so that it's not the same as a deprecated subgraph! - - expect(await gns.ownerOf(l2SubgraphId)).eq(me.address) - - expect(l2Subgraph.id).not.eq(l2SubgraphId) - const l2SubgraphData = await gns.subgraphs(l2Subgraph.id) - expect(l2SubgraphData.vSignal).eq(0) - expect(l2SubgraphData.nSignal).eq(0) - expect(l2SubgraphData.subgraphDeploymentID).eq(l2Subgraph.subgraphDeploymentID) - expect(l2SubgraphData.__DEPRECATED_reserveRatio).eq(DEFAULT_RESERVE_RATIO) - expect(l2SubgraphData.disabled).eq(false) - expect(l2SubgraphData.withdrawableGRT).eq(0) - }) - }) - - describe('finishing a subgraph transfer from L1', function () { - it('publishes the transferred subgraph and mints signal with no tax', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - // Calculate expected signal before minting - const expectedSignal = await curation.tokensToSignalNoTax(newSubgraph0.subgraphDeploymentID, curatedTokens) - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - const tx = gns - .connect(me) - .finishSubgraphTransferFromL1( - l2SubgraphId, - newSubgraph0.subgraphDeploymentID, - subgraphMetadata, - versionMetadata, - ) - await expect(tx) - .emit(gns, 'SubgraphPublished') - .withArgs(l2SubgraphId, newSubgraph0.subgraphDeploymentID, DEFAULT_RESERVE_RATIO) - await expect(tx).emit(gns, 'SubgraphMetadataUpdated').withArgs(l2SubgraphId, subgraphMetadata) - await expect(tx) - .emit(gns, 'SubgraphUpgraded') - .withArgs(l2SubgraphId, expectedSignal, curatedTokens, newSubgraph0.subgraphDeploymentID) - await expect(tx) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(l2SubgraphId, newSubgraph0.subgraphDeploymentID, versionMetadata) - await expect(tx).emit(gns, 'SubgraphL2TransferFinalized').withArgs(l2SubgraphId) - - const subgraphAfter = await gns.subgraphs(l2SubgraphId) - const transferDataAfter = await gns.subgraphL2TransferData(l2SubgraphId) - expect(subgraphAfter.vSignal).eq(expectedSignal) - expect(transferDataAfter.l2Done).eq(true) - expect(subgraphAfter.disabled).eq(false) - expect(subgraphAfter.subgraphDeploymentID).eq(newSubgraph0.subgraphDeploymentID) - const expectedNSignal = await gns.vSignalToNSignal(l2SubgraphId, expectedSignal) - expect(await gns.getCuratorSignal(l2SubgraphId, me.address)).eq(expectedNSignal) - await expect(tx) - .emit(gns, 'SignalMinted') - .withArgs(l2SubgraphId, me.address, expectedNSignal, expectedSignal, curatedTokens) - }) - it('cannot be called by someone other than the subgraph owner', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - const tx = gns - .connect(other) - .finishSubgraphTransferFromL1( - l2SubgraphId, - newSubgraph0.subgraphDeploymentID, - subgraphMetadata, - versionMetadata, - ) - await expect(tx).revertedWith('GNS: Must be authorized') - }) - it('rejects calls for a subgraph that does not exist', async function () { - const l1SubgraphId = await buildSubgraphId(me.address, toBN('1'), graph.chainId) - const metadata = randomHexBytes() - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - const tx = gns - .connect(me) - .finishSubgraphTransferFromL1(l2SubgraphId, newSubgraph0.subgraphDeploymentID, metadata, metadata) - await expect(tx).revertedWith('ERC721: owner query for nonexistent token') - }) - it('rejects calls for a subgraph that was not transferred from L1', async function () { - const l2Subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - const metadata = randomHexBytes() - - const tx = gns - .connect(me) - .finishSubgraphTransferFromL1(l2Subgraph.id, newSubgraph0.subgraphDeploymentID, metadata, metadata) - await expect(tx).revertedWith('INVALID_SUBGRAPH') - }) - it('accepts calls to a pre-curated subgraph deployment', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - - // Calculate expected signal before minting - const expectedSignal = await curation.tokensToSignalNoTax(newSubgraph0.subgraphDeploymentID, curatedTokens) - await grt.connect(me).approve(curation.address, toGRT('100')) - await curation.connect(me).mint(newSubgraph0.subgraphDeploymentID, toGRT('100'), toBN('0')) - - expect(await curation.getCurationPoolTokens(newSubgraph0.subgraphDeploymentID)).eq(toGRT('100')) - const tx = gns - .connect(me) - .finishSubgraphTransferFromL1( - l2SubgraphId, - newSubgraph0.subgraphDeploymentID, - subgraphMetadata, - versionMetadata, - ) - await expect(tx) - .emit(gns, 'SubgraphPublished') - .withArgs(l2SubgraphId, newSubgraph0.subgraphDeploymentID, DEFAULT_RESERVE_RATIO) - await expect(tx).emit(gns, 'SubgraphMetadataUpdated').withArgs(l2SubgraphId, subgraphMetadata) - await expect(tx) - .emit(gns, 'SubgraphUpgraded') - .withArgs(l2SubgraphId, expectedSignal, curatedTokens, newSubgraph0.subgraphDeploymentID) - await expect(tx) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(l2SubgraphId, newSubgraph0.subgraphDeploymentID, versionMetadata) - await expect(tx).emit(gns, 'SubgraphL2TransferFinalized').withArgs(l2SubgraphId) - - const subgraphAfter = await gns.subgraphs(l2SubgraphId) - const transferDataAfter = await gns.subgraphL2TransferData(l2SubgraphId) - expect(subgraphAfter.vSignal).eq(expectedSignal) - expect(transferDataAfter.l2Done).eq(true) - expect(subgraphAfter.disabled).eq(false) - expect(subgraphAfter.subgraphDeploymentID).eq(newSubgraph0.subgraphDeploymentID) - expect(await curation.getCurationPoolTokens(newSubgraph0.subgraphDeploymentID)).eq( - toGRT('100').add(curatedTokens), - ) - }) - it('rejects calls if the subgraph deployment ID is zero', async function () { - const metadata = randomHexBytes() - const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - const tx = gns.connect(me).finishSubgraphTransferFromL1(l2SubgraphId, HashZero, metadata, metadata) - await expect(tx).revertedWith('GNS: deploymentID != 0') - }) - it('rejects calls if the subgraph transfer was already finished', async function () { - const metadata = randomHexBytes() - const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - await gns - .connect(me) - .finishSubgraphTransferFromL1(l2SubgraphId, newSubgraph0.subgraphDeploymentID, metadata, metadata) - - const tx = gns - .connect(me) - .finishSubgraphTransferFromL1(l2SubgraphId, newSubgraph0.subgraphDeploymentID, metadata, metadata) - await expect(tx).revertedWith('ALREADY_DONE') - }) - }) - describe('claiming a curator balance with a message from L1 (onTokenTransfer)', function () { - it('assigns a curator balance to a beneficiary', async function () { - const l1GNSMockL2Alias = await helpers.getL2SignerFromL1(l1GNSMock.address) - // Eth for gas: - await helpers.setBalance(await l1GNSMockL2Alias.getAddress(), parseEther('1')) - - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - await transferMockSubgraphFromL1(l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata) - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - const l2OwnerSignalBefore = await gns.getCuratorSignal(l2SubgraphId, me.address) - - const newCuratorTokens = toGRT('10') - const callhookData = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(1), l1SubgraphId, other.address], - ) - const tx = await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, newCuratorTokens, callhookData) - - await expect(tx) - .emit(gns, 'CuratorBalanceReceived') - .withArgs(l1SubgraphId, l2SubgraphId, other.address, newCuratorTokens) - - const l2NewCuratorSignal = await gns.getCuratorSignal(l2SubgraphId, other.address) - const expectedNewCuratorSignal = await gns.vSignalToNSignal( - l2SubgraphId, - await curation.tokensToSignalNoTax(newSubgraph0.subgraphDeploymentID, newCuratorTokens), - ) - const l2OwnerSignalAfter = await gns.getCuratorSignal(l2SubgraphId, me.address) - expect(l2OwnerSignalAfter).eq(l2OwnerSignalBefore) - expect(l2NewCuratorSignal).eq(expectedNewCuratorSignal) - }) - it('adds the signal to any existing signal for the beneficiary', async function () { - const l1GNSMockL2Alias = await helpers.getL2SignerFromL1(l1GNSMock.address) - // Eth for gas: - await helpers.setBalance(await l1GNSMockL2Alias.getAddress(), parseEther('1')) - - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - await transferMockSubgraphFromL1(l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata) - - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - await grt.connect(governor).mint(other.address, toGRT('10')) - await grt.connect(other).approve(gns.address, toGRT('10')) - await gns.connect(other).mintSignal(l2SubgraphId, toGRT('10'), toBN(0)) - const prevSignal = await gns.getCuratorSignal(l2SubgraphId, other.address) - - const newCuratorTokens = toGRT('10') - const callhookData = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(1), l1SubgraphId, other.address], - ) - const tx = await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, newCuratorTokens, callhookData) - - await expect(tx) - .emit(gns, 'CuratorBalanceReceived') - .withArgs(l1SubgraphId, l2SubgraphId, other.address, newCuratorTokens) - - const expectedNewCuratorSignal = await gns.vSignalToNSignal( - l2SubgraphId, - await curation.tokensToSignalNoTax(newSubgraph0.subgraphDeploymentID, newCuratorTokens), - ) - const l2CuratorBalance = await gns.getCuratorSignal(l2SubgraphId, other.address) - expect(l2CuratorBalance).eq(prevSignal.add(expectedNewCuratorSignal)) - }) - it('cannot be called by someone other than the L2GraphTokenGateway', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - await transferMockSubgraphFromL1(l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata) - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(1), l1SubgraphId, me.address]) - const tx = gns.connect(me).onTokenTransfer(l1GNSMock.address, toGRT('1'), callhookData) - await expect(tx).revertedWith('ONLY_GATEWAY') - }) - it('rejects calls if the L1 sender is not the L1GNS', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - await transferMockSubgraphFromL1(l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata) - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(1), l1SubgraphId, me.address]) - const tx = gatewayFinalizeTransfer(me.address, gns.address, toGRT('1'), callhookData) - - await expect(tx).revertedWith('ONLY_L1_GNS_THROUGH_BRIDGE') - }) - it('if a subgraph does not exist, it returns the tokens to the beneficiary', async function () { - const l1GNSMockL2Alias = await helpers.getL2SignerFromL1(l1GNSMock.address) - // Eth for gas: - await helpers.setBalance(await l1GNSMockL2Alias.getAddress(), parseEther('1')) - - const { l1SubgraphId } = await defaultL1SubgraphParams() - - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(1), l1SubgraphId, me.address]) - const curatorTokensBefore = await grt.balanceOf(me.address) - const gnsBalanceBefore = await grt.balanceOf(gns.address) - const tx = gatewayFinalizeTransfer(l1GNSMock.address, gns.address, toGRT('1'), callhookData) - await expect(tx).emit(gns, 'CuratorBalanceReturnedToBeneficiary').withArgs(l1SubgraphId, me.address, toGRT('1')) - const curatorTokensAfter = await grt.balanceOf(me.address) - expect(curatorTokensAfter).eq(curatorTokensBefore.add(toGRT('1'))) - const gnsBalanceAfter = await grt.balanceOf(gns.address) - // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, - // so the GNS balance should be the same - expect(gnsBalanceAfter).eq(gnsBalanceBefore) - }) - it('for an L2-native subgraph, it sends the tokens to the beneficiary', async function () { - // This should never really happen unless there's a clash in subgraph IDs (which should - // also never happen), but we test it anyway to ensure it's a well-defined behavior - const l1GNSMockL2Alias = await helpers.getL2SignerFromL1(l1GNSMock.address) - // Eth for gas: - await helpers.setBalance(await l1GNSMockL2Alias.getAddress(), parseEther('1')) - - const l2Subgraph = await publishNewSubgraph(me, newSubgraph0, gns, graph.chainId) - - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(1), l2Subgraph.id, me.address]) - const curatorTokensBefore = await grt.balanceOf(me.address) - const gnsBalanceBefore = await grt.balanceOf(gns.address) - const tx = gatewayFinalizeTransfer(l1GNSMock.address, gns.address, toGRT('1'), callhookData) - await expect(tx).emit(gns, 'CuratorBalanceReturnedToBeneficiary').withArgs(l2Subgraph.id, me.address, toGRT('1')) - const curatorTokensAfter = await grt.balanceOf(me.address) - expect(curatorTokensAfter).eq(curatorTokensBefore.add(toGRT('1'))) - const gnsBalanceAfter = await grt.balanceOf(gns.address) - // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, - // so the GNS balance should be the same - expect(gnsBalanceAfter).eq(gnsBalanceBefore) - }) - it('if a subgraph transfer was not finished, it returns the tokens to the beneficiary', async function () { - const l1GNSMockL2Alias = await helpers.getL2SignerFromL1(l1GNSMock.address) - // Eth for gas: - await helpers.setBalance(await l1GNSMockL2Alias.getAddress(), parseEther('1')) - - const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() - const callhookDataSG = defaultAbiCoder.encode( - ['uint8', 'uint256', 'address'], - [toBN(0), l1SubgraphId, me.address], - ) - await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookDataSG) - - // At this point the SG exists, but transfer is not finished - - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(1), l1SubgraphId, me.address]) - const curatorTokensBefore = await grt.balanceOf(me.address) - const gnsBalanceBefore = await grt.balanceOf(gns.address) - const tx = gatewayFinalizeTransfer(l1GNSMock.address, gns.address, toGRT('1'), callhookData) - await expect(tx).emit(gns, 'CuratorBalanceReturnedToBeneficiary').withArgs(l1SubgraphId, me.address, toGRT('1')) - const curatorTokensAfter = await grt.balanceOf(me.address) - expect(curatorTokensAfter).eq(curatorTokensBefore.add(toGRT('1'))) - const gnsBalanceAfter = await grt.balanceOf(gns.address) - // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, - // so the GNS balance should be the same - expect(gnsBalanceAfter).eq(gnsBalanceBefore) - }) - - it('if a subgraph was deprecated after transfer, it returns the tokens to the beneficiary', async function () { - const l1GNSMockL2Alias = await helpers.getL2SignerFromL1(l1GNSMock.address) - // Eth for gas: - await helpers.setBalance(await l1GNSMockL2Alias.getAddress(), parseEther('1')) - - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - await transferMockSubgraphFromL1(l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata) - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - await gns.connect(me).deprecateSubgraph(l2SubgraphId) - - // SG was transferred, but is deprecated now! - - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(1), l1SubgraphId, me.address]) - const curatorTokensBefore = await grt.balanceOf(me.address) - const gnsBalanceBefore = await grt.balanceOf(gns.address) - const tx = gatewayFinalizeTransfer(l1GNSMock.address, gns.address, toGRT('1'), callhookData) - await expect(tx).emit(gns, 'CuratorBalanceReturnedToBeneficiary').withArgs(l1SubgraphId, me.address, toGRT('1')) - const curatorTokensAfter = await grt.balanceOf(me.address) - expect(curatorTokensAfter).eq(curatorTokensBefore.add(toGRT('1'))) - const gnsBalanceAfter = await grt.balanceOf(gns.address) - // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, - // so the GNS balance should be the same - expect(gnsBalanceAfter).eq(gnsBalanceBefore) - }) - }) - describe('onTokenTransfer with invalid codes', function () { - it('reverts', async function () { - // This should never really happen unless the Arbitrum bridge is compromised, - // so we test it anyway to ensure it's a well-defined behavior. - // code 2 does not exist: - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(2), toBN(1337), me.address]) - const tx = gatewayFinalizeTransfer(l1GNSMock.address, gns.address, toGRT('1'), callhookData) - await expect(tx).revertedWith('INVALID_CODE') - }) - }) - describe('getAliasedL2SubgraphID', function () { - it('returns the L2 subgraph ID that is the L1 subgraph ID with an offset', async function () { - const l1SubgraphId = ethers.BigNumber.from( - '68799548758199140224151701590582019137924969401915573086349306511960790045480', - ) - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - const offset = ethers.BigNumber.from('0x1111000000000000000000000000000000000000000000000000000000001111') - const base = ethers.constants.MaxUint256.add(1) - const expectedL2SubgraphId = l1SubgraphId.add(offset).mod(base) - expect(l2SubgraphId).eq(expectedL2SubgraphId) - }) - it('wraps around MAX_UINT256 in case of overflow', async function () { - const l1SubgraphId = ethers.constants.MaxUint256 - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - const offset = ethers.BigNumber.from('0x1111000000000000000000000000000000000000000000000000000000001111') - const base = ethers.constants.MaxUint256.add(1) - const expectedL2SubgraphId = l1SubgraphId.add(offset).mod(base) - expect(l2SubgraphId).eq(expectedL2SubgraphId) - }) - }) -}) diff --git a/packages/contracts-test/tests/unit/lib/gnsUtils.ts b/packages/contracts-test/tests/unit/lib/gnsUtils.ts deleted file mode 100644 index 877601886..000000000 --- a/packages/contracts-test/tests/unit/lib/gnsUtils.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Curation } from '@graphprotocol/contracts' -import { L1GNS } from '@graphprotocol/contracts' -import { L2GNS } from '@graphprotocol/contracts' -import { L2Curation } from '@graphprotocol/contracts' -import { GraphToken } from '@graphprotocol/contracts' -import { L2GraphToken } from '@graphprotocol/contracts' -import { buildSubgraphId, PublishSubgraph, Subgraph, toBN } from '@graphprotocol/sdk' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { BigNumber, ContractTransaction } from 'ethers' -import { namehash } from 'ethers/lib/utils' - -// Entities -export interface AccountDefaultName { - name: string - nameIdentifier: string -} - -export const DEFAULT_RESERVE_RATIO = 1000000 - -export const createDefaultName = (name: string): AccountDefaultName => { - return { - name: name, - nameIdentifier: namehash(name), - } -} - -export const getTokensAndVSignal = async ( - subgraphDeploymentID: string, - curation: Curation | L2Curation, -): Promise> => { - const curationPool = await curation.pools(subgraphDeploymentID) - const vSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) - return [curationPool.tokens, vSignal] -} - -export const publishNewSubgraph = async ( - account: SignerWithAddress, - newSubgraph: PublishSubgraph, - gns: L1GNS | L2GNS, - chainId: number, -): Promise => { - const subgraphID = await buildSubgraphId(account.address, await gns.nextAccountSeqID(account.address), chainId) - - // Send tx - const tx = gns - .connect(account) - .publishNewSubgraph(newSubgraph.subgraphDeploymentID, newSubgraph.versionMetadata, newSubgraph.subgraphMetadata) - - // Check events - await expect(tx) - .emit(gns, 'SubgraphPublished') - .withArgs(subgraphID, newSubgraph.subgraphDeploymentID, DEFAULT_RESERVE_RATIO) - .emit(gns, 'SubgraphMetadataUpdated') - .withArgs(subgraphID, newSubgraph.subgraphMetadata) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(subgraphID, newSubgraph.subgraphDeploymentID, newSubgraph.versionMetadata) - - // Check state - const subgraph = await gns.subgraphs(subgraphID) - expect(subgraph.vSignal).eq(0) - expect(subgraph.nSignal).eq(0) - expect(subgraph.subgraphDeploymentID).eq(newSubgraph.subgraphDeploymentID) - expect(subgraph.__DEPRECATED_reserveRatio).eq(DEFAULT_RESERVE_RATIO) - expect(subgraph.disabled).eq(false) - expect(subgraph.withdrawableGRT).eq(0) - - // Check NFT issuance - const owner = await gns.ownerOf(subgraphID) - expect(owner).eq(account.address) - - // TODO: properly type this - return { ...subgraph, id: subgraphID } as unknown as Subgraph -} - -export const publishNewVersion = async ( - account: SignerWithAddress, - subgraphID: string, - newSubgraph: PublishSubgraph, - gns: L1GNS | L2GNS, - curation: Curation | L2Curation, -) => { - // Before state - const ownerTaxPercentage = await gns.ownerTaxPercentage() - const curationTaxPercentage = await curation.curationTaxPercentage() - const beforeSubgraph = await gns.subgraphs(subgraphID) - - // Check what selling all nSignal, which == selling all vSignal, should return for tokens - // NOTE - no tax on burning on nSignal - const tokensReceivedEstimate = beforeSubgraph.nSignal.gt(0) - ? (await gns.nSignalToTokens(subgraphID, beforeSubgraph.nSignal))[1] - : toBN(0) - // Example: - // Deposit 100, 5 is taxed, 95 GRT in curve - // Upgrade - calculate 5% tax on 95 --> 4.75 GRT - // Multiple by ownerPercentage --> 50% * 4.75 = 2.375 GRT - // Owner adds 2.375 to 90.25, we deposit 92.625 GRT into the curve - // Divide this by 0.95 to get exactly 97.5 total tokens to be deposited - - // nSignalToTokens returns the amount of tokens with tax removed - // already. So we must add in the tokens removed - const MAX_PPM = 1000000 - const taxOnOriginal = tokensReceivedEstimate.mul(curationTaxPercentage).div(MAX_PPM) - const totalWithoutOwnerTax = tokensReceivedEstimate.sub(taxOnOriginal) - const ownerTax = taxOnOriginal.mul(ownerTaxPercentage).div(MAX_PPM) - const totalWithOwnerTax = totalWithoutOwnerTax.add(ownerTax) - const totalAdjustedUp = totalWithOwnerTax.mul(MAX_PPM).div(MAX_PPM - curationTaxPercentage) - - // Re-estimate amount of signal to get considering the owner tax paid by the owner - - const { 0: newVSignalEstimate, 1: newCurationTaxEstimate } = beforeSubgraph.nSignal.gt(0) - ? await curation.tokensToSignal(newSubgraph.subgraphDeploymentID, totalAdjustedUp) - : [toBN(0), toBN(0)] - - // Check the vSignal of the new curation curve, and tokens, before upgrading - const [beforeTokensNewCurve, beforeVSignalNewCurve] = await getTokensAndVSignal( - newSubgraph.subgraphDeploymentID, - curation, - ) - // Send tx - // Check if it's L2GNS (which requires slippage parameter) or L1GNS - const isL2 = 'getAliasedL2SubgraphID' in gns - const tx = isL2 - ? (gns as L2GNS) - .connect(account) - [ - 'publishNewVersion(uint256,bytes32,bytes32,uint256)' - ](subgraphID, newSubgraph.subgraphDeploymentID, newSubgraph.versionMetadata, 0) - : (gns as L1GNS) - .connect(account) - .publishNewVersion(subgraphID, newSubgraph.subgraphDeploymentID, newSubgraph.versionMetadata) - const txResult = expect(tx) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(subgraphID, newSubgraph.subgraphDeploymentID, newSubgraph.versionMetadata) - - // Only emits this event if there was actual signal to upgrade - if (beforeSubgraph.nSignal.gt(0)) { - await txResult - .emit(gns, 'SubgraphUpgraded') - .withArgs(subgraphID, newVSignalEstimate, totalAdjustedUp, newSubgraph.subgraphDeploymentID) - } - await txResult - - // Check curation vSignal old are set to zero - const [afterTokensOldCuration, afterVSignalOldCuration] = await getTokensAndVSignal( - beforeSubgraph.subgraphDeploymentID, - curation, - ) - expect(afterTokensOldCuration).eq(0) - expect(afterVSignalOldCuration).eq(0) - - // Check the vSignal of the new curation curve, and tokens - const [afterTokensNewCurve, afterVSignalNewCurve] = await getTokensAndVSignal( - newSubgraph.subgraphDeploymentID, - curation, - ) - expect(afterTokensNewCurve).eq(beforeTokensNewCurve.add(totalAdjustedUp).sub(newCurationTaxEstimate)) - expect(afterVSignalNewCurve).eq(beforeVSignalNewCurve.add(newVSignalEstimate)) - - // Check the nSignal pool - const afterSubgraph = await gns.subgraphs(subgraphID) - expect(afterSubgraph.vSignal).eq(afterVSignalNewCurve.sub(beforeVSignalNewCurve)).eq(newVSignalEstimate) - expect(afterSubgraph.nSignal).eq(beforeSubgraph.nSignal) // should not change - expect(afterSubgraph.subgraphDeploymentID).eq(newSubgraph.subgraphDeploymentID) - - // Check NFT should not change owner - const owner = await gns.ownerOf(subgraphID) - expect(owner).eq(account.address) - - return tx -} - -export const mintSignal = async ( - account: SignerWithAddress, - subgraphID: string, - tokensIn: BigNumber, - gns: L1GNS | L2GNS, - curation: Curation | L2Curation, -): Promise => { - // Before state - const beforeSubgraph = await gns.subgraphs(subgraphID) - const [beforeTokens, beforeVSignal] = await getTokensAndVSignal(beforeSubgraph.subgraphDeploymentID, curation) - - // Deposit - const { 0: vSignalExpected, 1: nSignalExpected, 2: curationTax } = await gns.tokensToNSignal(subgraphID, tokensIn) - const tx = gns.connect(account).mintSignal(subgraphID, tokensIn, 0) - await expect(tx) - .emit(gns, 'SignalMinted') - .withArgs(subgraphID, account.address, nSignalExpected, vSignalExpected, tokensIn) - - // After state - const afterSubgraph = await gns.subgraphs(subgraphID) - const [afterTokens, afterVSignal] = await getTokensAndVSignal(afterSubgraph.subgraphDeploymentID, curation) - - // Check state - expect(afterTokens).eq(beforeTokens.add(tokensIn.sub(curationTax))) - expect(afterVSignal).eq(beforeVSignal.add(vSignalExpected)) - expect(afterSubgraph.nSignal).eq(beforeSubgraph.nSignal.add(nSignalExpected)) - expect(afterSubgraph.vSignal).eq(beforeVSignal.add(vSignalExpected)) - - return tx -} - -export const burnSignal = async ( - account: SignerWithAddress, - subgraphID: string, - gns: L1GNS | L2GNS, - curation: Curation | L2Curation, -): Promise => { - // Before state - const beforeSubgraph = await gns.subgraphs(subgraphID) - const [beforeTokens, beforeVSignal] = await getTokensAndVSignal(beforeSubgraph.subgraphDeploymentID, curation) - const beforeUsersNSignal = await gns.getCuratorSignal(subgraphID, account.address) - - // Withdraw - const { 0: vSignalExpected, 1: tokensExpected } = await gns.nSignalToTokens(subgraphID, beforeUsersNSignal) - - // Send tx - const tx = gns.connect(account).burnSignal(subgraphID, beforeUsersNSignal, 0) - await expect(tx) - .emit(gns, 'SignalBurned') - .withArgs(subgraphID, account.address, beforeUsersNSignal, vSignalExpected, tokensExpected) - - // After state - const afterSubgraph = await gns.subgraphs(subgraphID) - const [afterTokens, afterVSignalCuration] = await getTokensAndVSignal(afterSubgraph.subgraphDeploymentID, curation) - - // Check state - expect(afterTokens).eq(beforeTokens.sub(tokensExpected)) - expect(afterVSignalCuration).eq(beforeVSignal.sub(vSignalExpected)) - expect(afterSubgraph.nSignal).eq(beforeSubgraph.nSignal.sub(beforeUsersNSignal)) - - return tx -} - -export const deprecateSubgraph = async ( - account: SignerWithAddress, - subgraphID: string, - gns: L1GNS | L2GNS, - curation: Curation | L2Curation, - grt: GraphToken | L2GraphToken, -) => { - // Before state - const beforeSubgraph = await gns.subgraphs(subgraphID) - const [beforeTokens] = await getTokensAndVSignal(beforeSubgraph.subgraphDeploymentID, curation) - - // We can use the whole amount, since in this test suite all vSignal is used to be staked on nSignal - const ownerBalanceBefore = await grt.balanceOf(account.address) - - // Send tx - const tx = gns.connect(account).deprecateSubgraph(subgraphID) - await expect(tx).emit(gns, 'SubgraphDeprecated').withArgs(subgraphID, beforeTokens) - - // After state - const afterSubgraph = await gns.subgraphs(subgraphID) - // Check marked as deprecated - expect(afterSubgraph.disabled).eq(true) - // Signal for the deployment must be all burned - expect(afterSubgraph.vSignal.eq(toBN('0'))) - // Cleanup reserve ratio - expect(afterSubgraph.__DEPRECATED_reserveRatio).eq(0) - // Should be equal since owner pays curation tax - expect(afterSubgraph.withdrawableGRT).eq(beforeTokens) - - // Check balance of GNS increased by curation tax from owner being added - const afterGNSBalance = await grt.balanceOf(gns.address) - expect(afterGNSBalance).eq(afterSubgraph.withdrawableGRT) - // Check that the owner balance decreased by the curation tax - const ownerBalanceAfter = await grt.balanceOf(account.address) - expect(ownerBalanceBefore.eq(ownerBalanceAfter)) - - // Check NFT was burned - await expect(gns.ownerOf(subgraphID)).revertedWith('ERC721: owner query for nonexistent token') - - return tx -} diff --git a/packages/subgraph-discovery/test/unit/Setup.t.sol b/packages/subgraph-discovery/test/unit/Setup.t.sol index 4b4f4ca7d..d95930ff7 100644 --- a/packages/subgraph-discovery/test/unit/Setup.t.sol +++ b/packages/subgraph-discovery/test/unit/Setup.t.sol @@ -1,12 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import { DiscoveryTest } from "./shared/DiscoveryTest.t.sol"; +import { GNSHelpers } from "./shared/GNSHelpers.t.sol"; -contract SetupTest is DiscoveryTest { +contract SetupTest is GNSHelpers { function test_Setup_Wired() public view { assertEq(nft.minter(), address(gns)); assertEq(curation.subgraphService(), subgraphService); assertEq(gns.counterpartGNSAddress(), l1GNS); } + + function test_Setup_PublishNewSubgraph() public { + _publishNewSubgraph(me, DEPLOYMENT_ID_0); + } } diff --git a/packages/subgraph-discovery/test/unit/interfaces/IL2GNSLike.sol b/packages/subgraph-discovery/test/unit/interfaces/IL2GNSLike.sol index dff9d4d54..6a3d0b2ee 100644 --- a/packages/subgraph-discovery/test/unit/interfaces/IL2GNSLike.sol +++ b/packages/subgraph-discovery/test/unit/interfaces/IL2GNSLike.sol @@ -122,4 +122,8 @@ interface IL2GNSLike { function nSignalToTokens(uint256 subgraphID, uint256 nSignalIn) external view returns (uint256, uint256); function vSignalToNSignal(uint256 subgraphID, uint256 vSignalIn) external view returns (uint256); + + function nSignalToVSignal(uint256 subgraphID, uint256 nSignalIn) external view returns (uint256); + + function multicall(bytes[] calldata data) external returns (bytes[] memory results); } diff --git a/packages/subgraph-discovery/test/unit/l2-gns/AliasedSubgraphId.t.sol b/packages/subgraph-discovery/test/unit/l2-gns/AliasedSubgraphId.t.sol new file mode 100644 index 000000000..e55643a93 --- /dev/null +++ b/packages/subgraph-discovery/test/unit/l2-gns/AliasedSubgraphId.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { GNSHelpers } from "../shared/GNSHelpers.t.sol"; + +contract AliasedSubgraphIdTest is GNSHelpers { + uint256 internal constant OFFSET = 0x1111000000000000000000000000000000000000000000000000000000001111; + + // -- getAliasedL2SubgraphID -- + + function test_GetAliasedL2SubgraphID_AddsOffset() public view { + uint256 l1SubgraphId = 68799548758199140224151701590582019137924969401915573086349306511960790045480; + uint256 expectedL2SubgraphId; + unchecked { + expectedL2SubgraphId = l1SubgraphId + OFFSET; + } + assertEq(gns.getAliasedL2SubgraphID(l1SubgraphId), expectedL2SubgraphId); + } + + function test_GetAliasedL2SubgraphID_WrapsAroundMaxUint256() public view { + uint256 l1SubgraphId = type(uint256).max; + uint256 expectedL2SubgraphId; + unchecked { + expectedL2SubgraphId = l1SubgraphId + OFFSET; + } + assertEq(gns.getAliasedL2SubgraphID(l1SubgraphId), expectedL2SubgraphId); + assertEq(expectedL2SubgraphId, OFFSET - 1); + } +} diff --git a/packages/subgraph-discovery/test/unit/l2-gns/FinishSubgraphTransfer.t.sol b/packages/subgraph-discovery/test/unit/l2-gns/FinishSubgraphTransfer.t.sol new file mode 100644 index 000000000..1d52d7bdc --- /dev/null +++ b/packages/subgraph-discovery/test/unit/l2-gns/FinishSubgraphTransfer.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { GNSHelpers } from "../shared/GNSHelpers.t.sol"; +import { IL2GNSLike } from "../interfaces/IL2GNSLike.sol"; + +contract FinishSubgraphTransferTest is GNSHelpers { + bytes32 internal constant METADATA = keccak256("metadata"); + + // -- finishing a subgraph transfer from L1 -- + + function test_FinishSubgraphTransfer_PublishesAndMintsSignalWithNoTax() public { + uint256 l1SubgraphId = _l1SubgraphId(); + uint256 l2SubgraphId = _receiveSubgraph(l1SubgraphId); + uint256 expectedSignal = curation.tokensToSignalNoTax(DEPLOYMENT_ID_0, L1_CURATED_TOKENS); + // vSignal is zero before the transfer, so nSignal is minted 1:1 + uint256 expectedNSignalEmitted = gns.vSignalToNSignal(l2SubgraphId, expectedSignal); + + vm.expectEmit(address(gns)); + emit IL2GNSLike.SignalMinted(l2SubgraphId, me, expectedNSignalEmitted, expectedSignal, L1_CURATED_TOKENS); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphUpgraded(l2SubgraphId, expectedSignal, L1_CURATED_TOKENS, DEPLOYMENT_ID_0); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphMetadataUpdated(l2SubgraphId, L1_SUBGRAPH_METADATA); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphPublished(l2SubgraphId, DEPLOYMENT_ID_0, DEFAULT_RESERVE_RATIO); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphVersionUpdated(l2SubgraphId, DEPLOYMENT_ID_0, L1_VERSION_METADATA); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphL2TransferFinalized(l2SubgraphId); + vm.prank(me); + gns.finishSubgraphTransferFromL1(l2SubgraphId, DEPLOYMENT_ID_0, L1_SUBGRAPH_METADATA, L1_VERSION_METADATA); + + _assertFinished(l2SubgraphId, expectedSignal); + uint256 expectedNSignal = gns.vSignalToNSignal(l2SubgraphId, expectedSignal); + assertEq(gns.getCuratorSignal(l2SubgraphId, me), expectedNSignal); + assertEq(expectedNSignal, expectedNSignalEmitted); + } + + function test_FinishSubgraphTransfer_RevertWhen_NotOwner() public { + uint256 l2SubgraphId = _receiveSubgraph(_l1SubgraphId()); + vm.expectRevert(bytes("GNS: Must be authorized")); + vm.prank(other); + gns.finishSubgraphTransferFromL1(l2SubgraphId, DEPLOYMENT_ID_0, L1_SUBGRAPH_METADATA, L1_VERSION_METADATA); + } + + function test_FinishSubgraphTransfer_RevertWhen_SubgraphDoesNotExist() public { + uint256 l2SubgraphId = gns.getAliasedL2SubgraphID(_l1SubgraphId()); + vm.expectRevert(bytes("ERC721: owner query for nonexistent token")); + vm.prank(me); + gns.finishSubgraphTransferFromL1(l2SubgraphId, DEPLOYMENT_ID_0, METADATA, METADATA); + } + + function test_FinishSubgraphTransfer_RevertWhen_NotTransferredFromL1() public { + uint256 l2SubgraphId = _publishNewSubgraph(me, DEPLOYMENT_ID_0); + vm.expectRevert(bytes("INVALID_SUBGRAPH")); + vm.prank(me); + gns.finishSubgraphTransferFromL1(l2SubgraphId, DEPLOYMENT_ID_0, METADATA, METADATA); + } + + function test_FinishSubgraphTransfer_ToPreCuratedDeployment() public { + uint256 l2SubgraphId = _receiveSubgraph(_l1SubgraphId()); + + // Expected signal computed before the pre-curation + uint256 expectedSignal = curation.tokensToSignalNoTax(DEPLOYMENT_ID_0, L1_CURATED_TOKENS); + vm.startPrank(me); + grt.approve(address(curation), 100 ether); + curation.mint(DEPLOYMENT_ID_0, 100 ether, 0); + vm.stopPrank(); + assertEq(curation.getCurationPoolTokens(DEPLOYMENT_ID_0), 100 ether); + + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphUpgraded(l2SubgraphId, expectedSignal, L1_CURATED_TOKENS, DEPLOYMENT_ID_0); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphMetadataUpdated(l2SubgraphId, L1_SUBGRAPH_METADATA); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphPublished(l2SubgraphId, DEPLOYMENT_ID_0, DEFAULT_RESERVE_RATIO); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphVersionUpdated(l2SubgraphId, DEPLOYMENT_ID_0, L1_VERSION_METADATA); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphL2TransferFinalized(l2SubgraphId); + vm.prank(me); + gns.finishSubgraphTransferFromL1(l2SubgraphId, DEPLOYMENT_ID_0, L1_SUBGRAPH_METADATA, L1_VERSION_METADATA); + + _assertFinished(l2SubgraphId, expectedSignal); + assertEq(curation.getCurationPoolTokens(DEPLOYMENT_ID_0), 100 ether + L1_CURATED_TOKENS); + } + + function test_FinishSubgraphTransfer_RevertWhen_DeploymentIDZero() public { + uint256 l2SubgraphId = _receiveSubgraph(_l1SubgraphId()); + vm.expectRevert(bytes("GNS: deploymentID != 0")); + vm.prank(me); + gns.finishSubgraphTransferFromL1(l2SubgraphId, bytes32(0), METADATA, METADATA); + } + + function test_FinishSubgraphTransfer_RevertWhen_AlreadyFinished() public { + uint256 l2SubgraphId = _receiveSubgraph(_l1SubgraphId()); + vm.startPrank(me); + gns.finishSubgraphTransferFromL1(l2SubgraphId, DEPLOYMENT_ID_0, METADATA, METADATA); + + vm.expectRevert(bytes("ALREADY_DONE")); + gns.finishSubgraphTransferFromL1(l2SubgraphId, DEPLOYMENT_ID_0, METADATA, METADATA); + vm.stopPrank(); + } + + function _receiveSubgraph(uint256 l1SubgraphId) internal returns (uint256) { + gatewayMock.deliver( + address(gns), + l1GNS, + L1_CURATED_TOKENS, + _callhookData(RECEIVE_SUBGRAPH_CODE, l1SubgraphId, me) + ); + return gns.getAliasedL2SubgraphID(l1SubgraphId); + } + + function _assertFinished(uint256 l2SubgraphId, uint256 expectedSignal) internal view { + Subgraph memory s = _subgraph(l2SubgraphId); + (, bool l2Done, ) = gns.subgraphL2TransferData(l2SubgraphId); + assertEq(s.vSignal, expectedSignal); + assertTrue(l2Done); + assertFalse(s.disabled); + assertEq(s.subgraphDeploymentID, DEPLOYMENT_ID_0); + } +} diff --git a/packages/subgraph-discovery/test/unit/l2-gns/Multicall.t.sol b/packages/subgraph-discovery/test/unit/l2-gns/Multicall.t.sol new file mode 100644 index 000000000..34c568fae --- /dev/null +++ b/packages/subgraph-discovery/test/unit/l2-gns/Multicall.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { GNSHelpers } from "../shared/GNSHelpers.t.sol"; +import { IL2GNSLike } from "../interfaces/IL2GNSLike.sol"; + +contract MulticallTest is GNSHelpers { + function test_GNS_Multicall() public { + uint256 tokens = 1000 ether; + uint256 subgraphId = uint256(keccak256(abi.encodePacked(me, gns.nextAccountSeqID(me), block.chainid))); + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall( + IL2GNSLike.publishNewSubgraph, + (DEPLOYMENT_ID_0, _versionMetadata(DEPLOYMENT_ID_0), _subgraphMetadata(DEPLOYMENT_ID_0)) + ); + calls[1] = abi.encodeCall(IL2GNSLike.mintSignal, (subgraphId, tokens, 0)); + + vm.startPrank(me); + grt.approve(address(gns), tokens); + bytes[] memory results = gns.multicall(calls); + vm.stopPrank(); + + assertEq(results.length, 2); + assertEq(gns.ownerOf(subgraphId), me); + assertEq(_subgraph(subgraphId).subgraphDeploymentID, DEPLOYMENT_ID_0); + uint256 nSignal = _subgraph(subgraphId).nSignal; + assertGt(nSignal, 0); + assertEq(gns.getCuratorSignal(subgraphId, me), nSignal); + // Curation tax is zero + assertEq(curation.getCurationPoolTokens(DEPLOYMENT_ID_0), tokens); + } +} diff --git a/packages/subgraph-discovery/test/unit/l2-gns/PublishNewVersion.t.sol b/packages/subgraph-discovery/test/unit/l2-gns/PublishNewVersion.t.sol new file mode 100644 index 000000000..523f1dffd --- /dev/null +++ b/packages/subgraph-discovery/test/unit/l2-gns/PublishNewVersion.t.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { GNSHelpers } from "../shared/GNSHelpers.t.sol"; +import { IL2GNSLike } from "../interfaces/IL2GNSLike.sol"; + +contract PublishNewVersionTest is GNSHelpers { + uint256 internal constant TOKENS_1000 = 1000 ether; + uint256 internal constant TOKENS_10000 = 10_000 ether; + uint256 internal constant TOKENS_100000 = 100_000 ether; + uint32 internal constant CURATION_TAX_PERCENTAGE = 50_000; + + uint256 internal subgraphId; + + function setUp() public override { + super.setUp(); + + vm.startPrank(governor); + grt.mint(me, TOKENS_100000); + grt.mint(other, TOKENS_100000); + curation.setCurationTaxPercentage(CURATION_TAX_PERCENTAGE); + vm.stopPrank(); + vm.startPrank(me); + grt.approve(address(gns), TOKENS_100000); + grt.approve(address(curation), TOKENS_100000); + vm.stopPrank(); + vm.startPrank(other); + grt.approve(address(gns), TOKENS_100000); + grt.approve(address(curation), TOKENS_100000); + vm.stopPrank(); + + subgraphId = _publishNewSubgraph(me, DEPLOYMENT_ID_0); + _mintSignal(me, subgraphId, TOKENS_10000); + } + + function test_PublishNewVersion_OnExistingSubgraph() public { + _publishNewVersion(me, subgraphId, DEPLOYMENT_ID_1); + } + + function test_PublishNewVersion_WhenOwnerTaxIsZero() public { + vm.prank(governor); + gns.setOwnerTaxPercentage(0); + _publishNewVersion(me, subgraphId, DEPLOYMENT_ID_1); + } + + function test_PublishNewVersion_WithNoCurrentSignal() public { + uint256 emptySignalSubgraphId = _publishNewSubgraph(me, keccak256("emptySignalDeploymentID")); + _publishNewVersion(me, emptySignalSubgraphId, DEPLOYMENT_ID_1); + } + + function test_PublishNewVersion_RevertWhen_SameDeploymentID() public { + vm.expectRevert(bytes("GNS: Cannot publish a new version with the same subgraph deployment ID")); + vm.prank(me); + gns.publishNewVersion(subgraphId, DEPLOYMENT_ID_0, _versionMetadata(DEPLOYMENT_ID_0), 0); + } + + function test_PublishNewVersion_RevertWhen_SubgraphDoesNotExist() public { + vm.expectRevert(bytes("ERC721: owner query for nonexistent token")); + vm.prank(me); + gns.publishNewVersion( + uint256(keccak256("nonexistentSubgraph")), + DEPLOYMENT_ID_1, + _versionMetadata(DEPLOYMENT_ID_1), + 0 + ); + } + + function test_PublishNewVersion_RevertWhen_NotOwner() public { + vm.expectRevert(bytes("GNS: Must be authorized")); + vm.prank(other); + gns.publishNewVersion(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1), 0); + } + + function test_PublishNewVersion_ToPreCuratedDeployment() public { + // Curate directly to the deployment + vm.prank(me); + curation.mint(DEPLOYMENT_ID_1, TOKENS_1000, 0); + + _publishNewVersion(me, subgraphId, DEPLOYMENT_ID_1); + } + + function test_PublishNewVersion_WithNoSignalNoMigration() public { + _burnSignal(me, subgraphId); + + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphVersionUpdated(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1)); + vm.prank(me); + gns.publishNewVersion(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1), 0); + } + + function test_PublishNewVersion_RevertWhen_Deprecated() public { + _deprecateSubgraph(me, subgraphId); + + // Deprecate burns the subgraph NFT, so the ownership check can't find it + vm.expectRevert(bytes("ERC721: owner query for nonexistent token")); + vm.prank(me); + gns.publishNewVersion(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1), 0); + } + + function test_PublishNewVersion_RevertWhen_FrontRunBeyondSlippage() public { + // Give attacker massive funds for the attack + vm.prank(governor); + grt.mint(attacker, 1_000_000 ether); + vm.prank(attacker); + grt.approve(address(curation), 1_000_000 ether); + + // Set minimum curation deposit to 1 wei and disable tax for an accurate attack test + vm.startPrank(governor); + curation.setMinimumCurationDeposit(1); + curation.setCurationTaxPercentage(0); + vm.stopPrank(); + vm.prank(attacker); + curation.mint(DEPLOYMENT_ID_1, 1, 0); + + // Expected signal from burning the current subgraph (before the attack) + uint256 vSignal = _subgraph(subgraphId).vSignal; + uint256 tokensFromOldCurve = curation.signalToTokens(DEPLOYMENT_ID_0, vSignal); + uint256 expectedSignalWithoutAttack = curation.tokensToSignalNoTax(DEPLOYMENT_ID_1, tokensFromOldCurve); + + // Attacker inflates the curation pool by collecting massive query fees + uint256 collectTokens = 50_000 ether; + vm.prank(governor); + grt.mint(address(curation), collectTokens); + vm.prank(subgraphService); + curation.collect(DEPLOYMENT_ID_1, collectTokens); + + // The victim now gets much less signal + uint256 actualSignalAfterAttack = curation.tokensToSignalNoTax(DEPLOYMENT_ID_1, tokensFromOldCurve); + assertLt(actualSignalAfterAttack, expectedSignalWithoutAttack / 10); + + // With slippage protection the upgrade fails when expecting reasonable signal + vm.expectRevert(bytes("Slippage protection")); + vm.prank(me); + gns.publishNewVersion( + subgraphId, + DEPLOYMENT_ID_1, + _versionMetadata(DEPLOYMENT_ID_1), + expectedSignalWithoutAttack / 2 + ); + } + + function test_PublishNewVersion_WhenSlippageProtectionSatisfied() public { + // Pre-curate the new deployment + vm.prank(governor); + grt.mint(attacker, 100 ether); + vm.startPrank(attacker); + grt.approve(address(curation), 100 ether); + curation.mint(DEPLOYMENT_ID_1, 100 ether, 0); + vm.stopPrank(); + + // Minimum signal we accept (5% slippage) + uint256 vSignal = _subgraph(subgraphId).vSignal; + uint256 tokensFromOldCurve = curation.signalToTokens(DEPLOYMENT_ID_0, vSignal); + uint256 expectedSignal = curation.tokensToSignalNoTax(DEPLOYMENT_ID_1, tokensFromOldCurve); + uint256 minSignalWithSlippage = (expectedSignal * 95) / 100; + + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphVersionUpdated(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1)); + vm.prank(me); + gns.publishNewVersion(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1), minSignalWithSlippage); + } + + function test_PublishNewVersion_L1VersionWhenNotPreCurated() public { + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphVersionUpdated(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1)); + vm.prank(me); + gns.publishNewVersion(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1)); + } + + function test_PublishNewVersion_RevertWhen_L1VersionPreCurated() public { + vm.prank(me); + curation.mint(DEPLOYMENT_ID_1, TOKENS_1000, 0); + + vm.expectRevert(bytes("GNS: Owner cannot point to a subgraphID that has been pre-curated")); + vm.prank(me); + gns.publishNewVersion(subgraphId, DEPLOYMENT_ID_1, _versionMetadata(DEPLOYMENT_ID_1)); + } +} diff --git a/packages/subgraph-discovery/test/unit/l2-gns/ReceiveCuratorBalance.t.sol b/packages/subgraph-discovery/test/unit/l2-gns/ReceiveCuratorBalance.t.sol new file mode 100644 index 000000000..c1352e6cc --- /dev/null +++ b/packages/subgraph-discovery/test/unit/l2-gns/ReceiveCuratorBalance.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { GNSHelpers } from "../shared/GNSHelpers.t.sol"; +import { IL2GNSLike } from "../interfaces/IL2GNSLike.sol"; + +contract ReceiveCuratorBalanceTest is GNSHelpers { + // -- claiming a curator balance with a message from L1 (onTokenTransfer) -- + + function test_ReceiveCuratorBalance_AssignsBalanceToBeneficiary() public { + uint256 l1SubgraphId = _l1SubgraphId(); + _transferMockSubgraphFromL1(l1SubgraphId, L1_CURATED_TOKENS); + uint256 l2SubgraphId = gns.getAliasedL2SubgraphID(l1SubgraphId); + uint256 l2OwnerSignalBefore = gns.getCuratorSignal(l2SubgraphId, me); + + uint256 newCuratorTokens = 10 ether; + vm.expectEmit(address(gns)); + emit IL2GNSLike.CuratorBalanceReceived(l1SubgraphId, l2SubgraphId, other, newCuratorTokens); + gatewayMock.deliver( + address(gns), + l1GNS, + newCuratorTokens, + _callhookData(RECEIVE_CURATOR_BALANCE_CODE, l1SubgraphId, other) + ); + + uint256 expectedNewCuratorSignal = gns.vSignalToNSignal( + l2SubgraphId, + curation.tokensToSignalNoTax(DEPLOYMENT_ID_0, newCuratorTokens) + ); + assertEq(gns.getCuratorSignal(l2SubgraphId, me), l2OwnerSignalBefore); + assertEq(gns.getCuratorSignal(l2SubgraphId, other), expectedNewCuratorSignal); + } + + function test_ReceiveCuratorBalance_AddsToExistingSignal() public { + uint256 l1SubgraphId = _l1SubgraphId(); + _transferMockSubgraphFromL1(l1SubgraphId, L1_CURATED_TOKENS); + uint256 l2SubgraphId = gns.getAliasedL2SubgraphID(l1SubgraphId); + + vm.prank(governor); + grt.mint(other, 10 ether); + vm.startPrank(other); + grt.approve(address(gns), 10 ether); + gns.mintSignal(l2SubgraphId, 10 ether, 0); + vm.stopPrank(); + uint256 prevSignal = gns.getCuratorSignal(l2SubgraphId, other); + + uint256 newCuratorTokens = 10 ether; + vm.expectEmit(address(gns)); + emit IL2GNSLike.CuratorBalanceReceived(l1SubgraphId, l2SubgraphId, other, newCuratorTokens); + gatewayMock.deliver( + address(gns), + l1GNS, + newCuratorTokens, + _callhookData(RECEIVE_CURATOR_BALANCE_CODE, l1SubgraphId, other) + ); + + uint256 expectedNewCuratorSignal = gns.vSignalToNSignal( + l2SubgraphId, + curation.tokensToSignalNoTax(DEPLOYMENT_ID_0, newCuratorTokens) + ); + assertEq(gns.getCuratorSignal(l2SubgraphId, other), prevSignal + expectedNewCuratorSignal); + } + + function test_ReceiveCuratorBalance_RevertWhen_CallerNotGateway() public { + uint256 l1SubgraphId = _l1SubgraphId(); + _transferMockSubgraphFromL1(l1SubgraphId, L1_CURATED_TOKENS); + bytes memory data = _callhookData(RECEIVE_CURATOR_BALANCE_CODE, l1SubgraphId, me); + vm.expectRevert(bytes("ONLY_GATEWAY")); + vm.prank(me); + gns.onTokenTransfer(l1GNS, 1 ether, data); + } + + function test_ReceiveCuratorBalance_RevertWhen_L1SenderNotL1GNS() public { + uint256 l1SubgraphId = _l1SubgraphId(); + _transferMockSubgraphFromL1(l1SubgraphId, L1_CURATED_TOKENS); + bytes memory data = _callhookData(RECEIVE_CURATOR_BALANCE_CODE, l1SubgraphId, me); + vm.expectRevert(bytes("ONLY_L1_GNS_THROUGH_BRIDGE")); + gatewayMock.deliver(address(gns), me, 1 ether, data); + } + + function test_ReceiveCuratorBalance_ReturnsTokensWhenSubgraphDoesNotExist() public { + _assertReturnedToBeneficiary(_l1SubgraphId()); + } + + function test_ReceiveCuratorBalance_ReturnsTokensForL2NativeSubgraph() public { + // Only happens on a subgraph ID clash, but the behaviour must be well defined + uint256 l2SubgraphId = _publishNewSubgraph(me, DEPLOYMENT_ID_0); + _assertReturnedToBeneficiary(l2SubgraphId); + } + + function test_ReceiveCuratorBalance_ReturnsTokensWhenTransferNotFinished() public { + uint256 l1SubgraphId = _l1SubgraphId(); + gatewayMock.deliver( + address(gns), + l1GNS, + L1_CURATED_TOKENS, + _callhookData(RECEIVE_SUBGRAPH_CODE, l1SubgraphId, me) + ); + + // The subgraph exists, but the transfer is not finished + _assertReturnedToBeneficiary(l1SubgraphId); + } + + function test_ReceiveCuratorBalance_ReturnsTokensWhenDeprecatedAfterTransfer() public { + uint256 l1SubgraphId = _l1SubgraphId(); + _transferMockSubgraphFromL1(l1SubgraphId, L1_CURATED_TOKENS); + uint256 l2SubgraphId = gns.getAliasedL2SubgraphID(l1SubgraphId); + vm.prank(me); + gns.deprecateSubgraph(l2SubgraphId); + + // Transferred, but deprecated now + _assertReturnedToBeneficiary(l1SubgraphId); + } + + function _assertReturnedToBeneficiary(uint256 l1SubgraphId) internal { + uint256 curatorTokensBefore = grt.balanceOf(me); + uint256 gnsBalanceBefore = grt.balanceOf(address(gns)); + + vm.expectEmit(address(gns)); + emit IL2GNSLike.CuratorBalanceReturnedToBeneficiary(l1SubgraphId, me, 1 ether); + gatewayMock.deliver( + address(gns), + l1GNS, + 1 ether, + _callhookData(RECEIVE_CURATOR_BALANCE_CODE, l1SubgraphId, me) + ); + + assertEq(grt.balanceOf(me), curatorTokensBefore + 1 ether); + // The gateway mints the tokens that are sent on to the curator + assertEq(grt.balanceOf(address(gns)), gnsBalanceBefore); + } +} diff --git a/packages/subgraph-discovery/test/unit/l2-gns/ReceiveSubgraphFromL1.t.sol b/packages/subgraph-discovery/test/unit/l2-gns/ReceiveSubgraphFromL1.t.sol new file mode 100644 index 000000000..cd5b63b39 --- /dev/null +++ b/packages/subgraph-discovery/test/unit/l2-gns/ReceiveSubgraphFromL1.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { GNSHelpers } from "../shared/GNSHelpers.t.sol"; +import { IL2GNSLike } from "../interfaces/IL2GNSLike.sol"; + +contract ReceiveSubgraphFromL1Test is GNSHelpers { + // -- receiving a subgraph from L1 (onTokenTransfer) -- + + function test_ReceiveSubgraphFromL1_RevertWhen_CallerNotGateway() public { + bytes memory data = _callhookData(RECEIVE_SUBGRAPH_CODE, _l1SubgraphId(), me); + vm.expectRevert(bytes("ONLY_GATEWAY")); + vm.prank(me); + gns.onTokenTransfer(l1GNS, L1_CURATED_TOKENS, data); + } + + function test_ReceiveSubgraphFromL1_RevertWhen_L1SenderNotL1GNS() public { + bytes memory data = _callhookData(RECEIVE_SUBGRAPH_CODE, _l1SubgraphId(), me); + vm.expectRevert(bytes("ONLY_L1_GNS_THROUGH_BRIDGE")); + gatewayMock.deliver(address(gns), me, L1_CURATED_TOKENS, data); + } + + function test_ReceiveSubgraphFromL1_CreatesDisabledSubgraph() public { + uint256 l1SubgraphId = _l1SubgraphId(); + uint256 l2SubgraphId = gns.getAliasedL2SubgraphID(l1SubgraphId); + + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphReceivedFromL1(l1SubgraphId, l2SubgraphId, me, L1_CURATED_TOKENS); + gatewayMock.deliver( + address(gns), + l1GNS, + L1_CURATED_TOKENS, + _callhookData(RECEIVE_SUBGRAPH_CODE, l1SubgraphId, me) + ); + + _assertReceivedSubgraph(l2SubgraphId); + } + + function test_ReceiveSubgraphFromL1_DoesNotConflictWithLocalSubgraph() public { + uint256 localSubgraphId = _publishNewSubgraph(me, DEPLOYMENT_ID_0); + + uint256 l1SubgraphId = _l1SubgraphId(); + uint256 l2SubgraphId = gns.getAliasedL2SubgraphID(l1SubgraphId); + + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphReceivedFromL1(l1SubgraphId, l2SubgraphId, me, L1_CURATED_TOKENS); + gatewayMock.deliver( + address(gns), + l1GNS, + L1_CURATED_TOKENS, + _callhookData(RECEIVE_SUBGRAPH_CODE, l1SubgraphId, me) + ); + + _assertReceivedSubgraph(l2SubgraphId); + + assertNotEq(localSubgraphId, l2SubgraphId); + Subgraph memory local = _subgraph(localSubgraphId); + assertEq(local.vSignal, 0); + assertEq(local.nSignal, 0); + assertEq(local.subgraphDeploymentID, DEPLOYMENT_ID_0); + assertEq(local.reserveRatio, DEFAULT_RESERVE_RATIO); + assertFalse(local.disabled); + assertEq(local.withdrawableGRT, 0); + } + + // -- onTokenTransfer with invalid codes -- + + function test_OnTokenTransferWithInvalidCodes_RevertWhen_InvalidCode() public { + // Code 2 does not exist + bytes memory data = _callhookData(2, 1337, me); + vm.expectRevert(bytes("INVALID_CODE")); + gatewayMock.deliver(address(gns), l1GNS, 1 ether, data); + } + + function _assertReceivedSubgraph(uint256 l2SubgraphId) internal view { + (uint256 tokens, bool l2Done, uint256 receivedBlock) = gns.subgraphL2TransferData(l2SubgraphId); + assertEq(tokens, L1_CURATED_TOKENS); + assertFalse(l2Done); + assertEq(receivedBlock, block.number); + + Subgraph memory s = _subgraph(l2SubgraphId); + assertEq(s.vSignal, 0); + assertEq(s.nSignal, 0); + assertEq(s.subgraphDeploymentID, bytes32(0)); + assertEq(s.reserveRatio, DEFAULT_RESERVE_RATIO); + assertTrue(s.disabled); + // Not the same as a deprecated subgraph + assertEq(s.withdrawableGRT, 0); + + assertEq(gns.ownerOf(l2SubgraphId), me); + } +} diff --git a/packages/subgraph-discovery/test/unit/shared/GNSHelpers.t.sol b/packages/subgraph-discovery/test/unit/shared/GNSHelpers.t.sol new file mode 100644 index 000000000..aeb0bc245 --- /dev/null +++ b/packages/subgraph-discovery/test/unit/shared/GNSHelpers.t.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { DiscoveryTest } from "./DiscoveryTest.t.sol"; +import { IL2GNSLike } from "../interfaces/IL2GNSLike.sol"; + +/// Ports the `lib/gnsUtils.ts` helpers used by the L2GNS tests, assertions included. +abstract contract GNSHelpers is DiscoveryTest { + uint32 internal constant DEFAULT_RESERVE_RATIO = 1_000_000; + uint256 internal constant MAX_PPM = 1_000_000; + + uint8 internal constant RECEIVE_SUBGRAPH_CODE = 0; + uint8 internal constant RECEIVE_CURATOR_BALANCE_CODE = 1; + + bytes32 internal constant DEPLOYMENT_ID_0 = keccak256("subgraphDeploymentID0"); + bytes32 internal constant DEPLOYMENT_ID_1 = keccak256("subgraphDeploymentID1"); + + struct Subgraph { + uint256 vSignal; + uint256 nSignal; + bytes32 subgraphDeploymentID; + uint32 reserveRatio; + bool disabled; + uint256 withdrawableGRT; + } + + function setUp() public virtual override { + super.setUp(); + vm.prank(governor); + grt.mint(me, 10_000 ether); + } + + function _versionMetadata(bytes32 deploymentId) internal pure returns (bytes32) { + return keccak256(abi.encode("versionMetadata", deploymentId)); + } + + function _subgraphMetadata(bytes32 deploymentId) internal pure returns (bytes32) { + return keccak256(abi.encode("subgraphMetadata", deploymentId)); + } + + function _subgraph(uint256 subgraphId) internal view returns (Subgraph memory s) { + (s.vSignal, s.nSignal, s.subgraphDeploymentID, s.reserveRatio, s.disabled, s.withdrawableGRT) = gns.subgraphs( + subgraphId + ); + } + + function _tokensAndVSignal(bytes32 deploymentId) internal view returns (uint256 tokens, uint256 vSignal) { + (tokens, , ) = curation.pools(deploymentId); + vSignal = curation.getCurationPoolSignal(deploymentId); + } + + // -- L1 transfers (from l2GNS.test.ts) -- + + uint256 internal constant L1_CURATED_TOKENS = 1337 ether; + bytes32 internal constant L1_SUBGRAPH_METADATA = keccak256("l1SubgraphMetadata"); + bytes32 internal constant L1_VERSION_METADATA = keccak256("l1VersionMetadata"); + + function _l1SubgraphId() internal view returns (uint256) { + return uint256(keccak256(abi.encodePacked(me, uint256(1), block.chainid))); + } + + function _callhookData(uint8 code, uint256 l1SubgraphId, address beneficiary) internal pure returns (bytes memory) { + return abi.encode(code, l1SubgraphId, beneficiary); + } + + /// Receives the subgraph from L1 and finishes the transfer on DEPLOYMENT_ID_0, as `me` + function _transferMockSubgraphFromL1(uint256 l1SubgraphId, uint256 curatedTokens) internal { + gatewayMock.deliver(address(gns), l1GNS, curatedTokens, _callhookData(RECEIVE_SUBGRAPH_CODE, l1SubgraphId, me)); + uint256 l2SubgraphId = gns.getAliasedL2SubgraphID(l1SubgraphId); + vm.prank(me); + gns.finishSubgraphTransferFromL1(l2SubgraphId, DEPLOYMENT_ID_0, L1_SUBGRAPH_METADATA, L1_VERSION_METADATA); + } + + function _publishNewSubgraph(address account, bytes32 deploymentId) internal returns (uint256 subgraphId) { + subgraphId = uint256(keccak256(abi.encodePacked(account, gns.nextAccountSeqID(account), block.chainid))); + bytes32 versionMetadata = _versionMetadata(deploymentId); + bytes32 subgraphMetadata = _subgraphMetadata(deploymentId); + + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphPublished(subgraphId, deploymentId, DEFAULT_RESERVE_RATIO); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphMetadataUpdated(subgraphId, subgraphMetadata); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphVersionUpdated(subgraphId, deploymentId, versionMetadata); + vm.prank(account); + gns.publishNewSubgraph(deploymentId, versionMetadata, subgraphMetadata); + + Subgraph memory s = _subgraph(subgraphId); + assertEq(s.vSignal, 0); + assertEq(s.nSignal, 0); + assertEq(s.subgraphDeploymentID, deploymentId); + assertEq(s.reserveRatio, DEFAULT_RESERVE_RATIO); + assertFalse(s.disabled); + assertEq(s.withdrawableGRT, 0); + + assertEq(gns.ownerOf(subgraphId), account); + } + + /// Mirrors GNS._chargeOwnerTax (the helper's formula only matches it when the division is exact) + function _ownerTaxAdjusted( + uint256 tokens, + uint32 curationTaxPercentage, + uint32 ownerTaxPercentage + ) internal pure returns (uint256) { + if (curationTaxPercentage == 0 || ownerTaxPercentage == 0) return tokens; + uint256 taxOnOriginal = (tokens * curationTaxPercentage) / MAX_PPM; + uint256 totalWithoutOwnerTax = tokens - taxOnOriginal; + uint256 ownerTax = (taxOnOriginal * ownerTaxPercentage) / MAX_PPM; + uint256 totalWithOwnerTax = totalWithoutOwnerTax + ownerTax; + return (totalWithOwnerTax * MAX_PPM) / (MAX_PPM - curationTaxPercentage); + } + + struct VersionUpgrade { + uint256 totalAdjustedUp; + uint256 newVSignalEstimate; + uint256 newCurationTaxEstimate; + uint256 beforeTokensNewCurve; + uint256 beforeVSignalNewCurve; + } + + function _estimateVersionUpgrade( + uint256 subgraphId, + uint256 nSignal, + bytes32 newDeploymentId + ) internal view returns (VersionUpgrade memory u) { + if (nSignal > 0) { + // No tax on burning nSignal + (, uint256 tokensReceivedEstimate) = gns.nSignalToTokens(subgraphId, nSignal); + u.totalAdjustedUp = _ownerTaxAdjusted( + tokensReceivedEstimate, + curation.curationTaxPercentage(), + gns.ownerTaxPercentage() + ); + (u.newVSignalEstimate, u.newCurationTaxEstimate) = curation.tokensToSignal( + newDeploymentId, + u.totalAdjustedUp + ); + } + (u.beforeTokensNewCurve, u.beforeVSignalNewCurve) = _tokensAndVSignal(newDeploymentId); + } + + function _publishNewVersion(address account, uint256 subgraphId, bytes32 newDeploymentId) internal { + Subgraph memory beforeSubgraph = _subgraph(subgraphId); + VersionUpgrade memory u = _estimateVersionUpgrade(subgraphId, beforeSubgraph.nSignal, newDeploymentId); + + // Only emits SubgraphUpgraded if there was signal to upgrade + if (beforeSubgraph.nSignal > 0) { + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphUpgraded(subgraphId, u.newVSignalEstimate, u.totalAdjustedUp, newDeploymentId); + } + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphVersionUpdated(subgraphId, newDeploymentId, _versionMetadata(newDeploymentId)); + vm.prank(account); + gns.publishNewVersion(subgraphId, newDeploymentId, _versionMetadata(newDeploymentId), 0); + + // Old curve is emptied + (uint256 afterTokensOldCurve, uint256 afterVSignalOldCurve) = _tokensAndVSignal( + beforeSubgraph.subgraphDeploymentID + ); + assertEq(afterTokensOldCurve, 0); + assertEq(afterVSignalOldCurve, 0); + + (uint256 afterTokensNewCurve, uint256 afterVSignalNewCurve) = _tokensAndVSignal(newDeploymentId); + assertEq(afterTokensNewCurve, u.beforeTokensNewCurve + u.totalAdjustedUp - u.newCurationTaxEstimate); + assertEq(afterVSignalNewCurve, u.beforeVSignalNewCurve + u.newVSignalEstimate); + + Subgraph memory afterSubgraph = _subgraph(subgraphId); + assertEq(afterSubgraph.vSignal, afterVSignalNewCurve - u.beforeVSignalNewCurve); + assertEq(afterSubgraph.vSignal, u.newVSignalEstimate); + assertEq(afterSubgraph.nSignal, beforeSubgraph.nSignal); + assertEq(afterSubgraph.subgraphDeploymentID, newDeploymentId); + + assertEq(gns.ownerOf(subgraphId), account); + } + + function _mintSignal(address account, uint256 subgraphId, uint256 tokensIn) internal { + Subgraph memory beforeSubgraph = _subgraph(subgraphId); + (uint256 beforeTokens, uint256 beforeVSignal) = _tokensAndVSignal(beforeSubgraph.subgraphDeploymentID); + + (uint256 vSignalExpected, uint256 nSignalExpected, uint256 curationTax) = gns.tokensToNSignal( + subgraphId, + tokensIn + ); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SignalMinted(subgraphId, account, nSignalExpected, vSignalExpected, tokensIn); + vm.prank(account); + gns.mintSignal(subgraphId, tokensIn, 0); + + Subgraph memory afterSubgraph = _subgraph(subgraphId); + (uint256 afterTokens, uint256 afterVSignal) = _tokensAndVSignal(afterSubgraph.subgraphDeploymentID); + assertEq(afterTokens, beforeTokens + tokensIn - curationTax); + assertEq(afterVSignal, beforeVSignal + vSignalExpected); + assertEq(afterSubgraph.nSignal, beforeSubgraph.nSignal + nSignalExpected); + assertEq(afterSubgraph.vSignal, beforeVSignal + vSignalExpected); + } + + function _burnSignal(address account, uint256 subgraphId) internal { + Subgraph memory beforeSubgraph = _subgraph(subgraphId); + (uint256 beforeTokens, uint256 beforeVSignal) = _tokensAndVSignal(beforeSubgraph.subgraphDeploymentID); + uint256 beforeUsersNSignal = gns.getCuratorSignal(subgraphId, account); + + (uint256 vSignalExpected, uint256 tokensExpected) = gns.nSignalToTokens(subgraphId, beforeUsersNSignal); + assertEq(gns.nSignalToVSignal(subgraphId, beforeUsersNSignal), vSignalExpected); + vm.expectEmit(address(gns)); + emit IL2GNSLike.SignalBurned(subgraphId, account, beforeUsersNSignal, vSignalExpected, tokensExpected); + vm.prank(account); + gns.burnSignal(subgraphId, beforeUsersNSignal, 0); + + Subgraph memory afterSubgraph = _subgraph(subgraphId); + (uint256 afterTokens, uint256 afterVSignal) = _tokensAndVSignal(afterSubgraph.subgraphDeploymentID); + assertEq(afterTokens, beforeTokens - tokensExpected); + assertEq(afterVSignal, beforeVSignal - vSignalExpected); + assertEq(afterSubgraph.nSignal, beforeSubgraph.nSignal - beforeUsersNSignal); + } + + function _deprecateSubgraph(address account, uint256 subgraphId) internal { + Subgraph memory beforeSubgraph = _subgraph(subgraphId); + (uint256 beforeTokens, ) = _tokensAndVSignal(beforeSubgraph.subgraphDeploymentID); + // All vSignal in these tests is held by the subgraph's nSignal, so the whole pool is withdrawable + uint256 ownerBalanceBefore = grt.balanceOf(account); + + vm.expectEmit(address(gns)); + emit IL2GNSLike.SubgraphDeprecated(subgraphId, beforeTokens); + vm.prank(account); + gns.deprecateSubgraph(subgraphId); + + Subgraph memory afterSubgraph = _subgraph(subgraphId); + assertTrue(afterSubgraph.disabled); + assertEq(afterSubgraph.vSignal, 0); + assertEq(afterSubgraph.reserveRatio, 0); + assertEq(afterSubgraph.withdrawableGRT, beforeTokens); + + assertEq(grt.balanceOf(address(gns)), afterSubgraph.withdrawableGRT); + assertEq(grt.balanceOf(account), ownerBalanceBefore); + + // NFT is burned + vm.expectRevert(bytes("ERC721: owner query for nonexistent token")); + gns.ownerOf(subgraphId); + } +} diff --git a/scripts/contracts-migration/test-mapping/t3b.md b/scripts/contracts-migration/test-mapping/t3b.md new file mode 100644 index 000000000..aa8c37cc2 --- /dev/null +++ b/scripts/contracts-migration/test-mapping/t3b.md @@ -0,0 +1,56 @@ +# T3b test mapping: L2GNS + +New tests are in `packages/subgraph-discovery`. Each `describe` of `l2GNS.test.ts` has its own file under `test/unit/l2-gns/` (`onTokenTransfer with invalid codes` lives in `ReceiveSubgraphFromL1.t.sol`). The `lib/gnsUtils.ts` helpers, with their assertions, and the L1 transfer helpers are in `test/unit/shared/GNSHelpers.t.sol`, on top of `DiscoveryTest`. L1 deliveries go through `L2GatewayMock.deliver`, which stands in for `L2GraphTokenGateway.finalizeInboundTransfer`. Random metadata and IDs are fixed values. `test/unit/Setup.t.sol::test_Setup_PublishNewSubgraph` checks the publish helper and has no old test. + +| Old file | Old test (describe › it) | New test (file::function) | Note | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should publish a new version on an existing subgraph | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_OnExistingSubgraph | Owner-tax math follows `GNS._chargeOwnerTax` | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should publish a new version on an existing subgraph when owner tax is zero | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_WhenOwnerTaxIsZero | Expected tokens use the contract's zero-tax branch (tokens unchanged); same value as the TS formula for these numbers | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should publish a new version on an existing subgraph with no current signal | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_WithNoCurrentSignal | Random `buildSubgraph()` becomes a fixed deployment ID | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should reject a new version with the same subgraph deployment ID | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_RevertWhen_SameDeploymentID | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should reject publishing a version to a subgraph that does not exist | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_RevertWhen_SubgraphDoesNotExist | Random ID becomes a fixed ID | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › reject if not the owner | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_RevertWhen_NotOwner | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should NOT fail when upgrade tries to point to a pre-curated | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_ToPreCuratedDeployment | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should upgrade version when there is no signal with no signal migration | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_WithNoSignalNoMigration | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should fail when subgraph is deprecated | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_RevertWhen_Deprecated | `_deprecateSubgraph` also asserts `vSignal == 0` and the unchanged owner balance (no-op `expect(bool)` in the TS helper) | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should protect against front-running with slippage protection | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_RevertWhen_FrontRunBeyondSlippage | Staking stake/allocate lines dropped (never affected the assertions); pool inflated by minting to curation and `collect` as `subgraphService` instead of re-pointing the subgraph service at the attacker | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should succeed when slippage protection is satisfied | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_WhenSlippageProtectionSatisfied | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should succeed using L1 version (3-param) when deployment is NOT pre-curated | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_L1VersionWhenNotPreCurated | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › publishNewVersion › should revert using L1 version (3-param) when deployment IS pre-curated | test/unit/l2-gns/PublishNewVersion.t.sol::test_PublishNewVersion_RevertWhen_L1VersionPreCurated | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › receiving a subgraph from L1 (onTokenTransfer) › cannot be called by someone other than the L2GraphTokenGateway | test/unit/l2-gns/ReceiveSubgraphFromL1.t.sol::test_ReceiveSubgraphFromL1_RevertWhen_CallerNotGateway | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › receiving a subgraph from L1 (onTokenTransfer) › rejects calls if the L1 sender is not the L1GNS | test/unit/l2-gns/ReceiveSubgraphFromL1.t.sol::test_ReceiveSubgraphFromL1_RevertWhen_L1SenderNotL1GNS | Delivered through `L2GatewayMock` | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › receiving a subgraph from L1 (onTokenTransfer) › creates a subgraph in a disabled state | test/unit/l2-gns/ReceiveSubgraphFromL1.t.sol::test_ReceiveSubgraphFromL1_CreatesDisabledSubgraph | `DepositFinalized` assertion covered by t2b L2GraphTokenGatewayTransfers | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › receiving a subgraph from L1 (onTokenTransfer) › does not conflict with a locally created subgraph | test/unit/l2-gns/ReceiveSubgraphFromL1.t.sol::test_ReceiveSubgraphFromL1_DoesNotConflictWithLocalSubgraph | `DepositFinalized` assertion covered by t2b L2GraphTokenGatewayTransfers | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › finishing a subgraph transfer from L1 › publishes the transferred subgraph and mints signal with no tax | test/unit/l2-gns/FinishSubgraphTransfer.t.sol::test_FinishSubgraphTransfer_PublishesAndMintsSignalWithNoTax | Events asserted in emission order; `SignalMinted` nSignal computed before the call (1:1) and checked equal to the post-call `vSignalToNSignal` | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › finishing a subgraph transfer from L1 › cannot be called by someone other than the subgraph owner | test/unit/l2-gns/FinishSubgraphTransfer.t.sol::test_FinishSubgraphTransfer_RevertWhen_NotOwner | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › finishing a subgraph transfer from L1 › rejects calls for a subgraph that does not exist | test/unit/l2-gns/FinishSubgraphTransfer.t.sol::test_FinishSubgraphTransfer_RevertWhen_SubgraphDoesNotExist | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › finishing a subgraph transfer from L1 › rejects calls for a subgraph that was not transferred from L1 | test/unit/l2-gns/FinishSubgraphTransfer.t.sol::test_FinishSubgraphTransfer_RevertWhen_NotTransferredFromL1 | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › finishing a subgraph transfer from L1 › accepts calls to a pre-curated subgraph deployment | test/unit/l2-gns/FinishSubgraphTransfer.t.sol::test_FinishSubgraphTransfer_ToPreCuratedDeployment | Events asserted in emission order | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › finishing a subgraph transfer from L1 › rejects calls if the subgraph deployment ID is zero | test/unit/l2-gns/FinishSubgraphTransfer.t.sol::test_FinishSubgraphTransfer_RevertWhen_DeploymentIDZero | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › finishing a subgraph transfer from L1 › rejects calls if the subgraph transfer was already finished | test/unit/l2-gns/FinishSubgraphTransfer.t.sol::test_FinishSubgraphTransfer_RevertWhen_AlreadyFinished | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › claiming a curator balance with a message from L1 (onTokenTransfer) › assigns a curator balance to a beneficiary | test/unit/l2-gns/ReceiveCuratorBalance.t.sol::test_ReceiveCuratorBalance_AssignsBalanceToBeneficiary | L1GNS alias ETH funding dropped (the mock gateway makes the call) | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › claiming a curator balance with a message from L1 (onTokenTransfer) › adds the signal to any existing signal for the beneficiary | test/unit/l2-gns/ReceiveCuratorBalance.t.sol::test_ReceiveCuratorBalance_AddsToExistingSignal | As above | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › claiming a curator balance with a message from L1 (onTokenTransfer) › cannot be called by someone other than the L2GraphTokenGateway | test/unit/l2-gns/ReceiveCuratorBalance.t.sol::test_ReceiveCuratorBalance_RevertWhen_CallerNotGateway | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › claiming a curator balance with a message from L1 (onTokenTransfer) › rejects calls if the L1 sender is not the L1GNS | test/unit/l2-gns/ReceiveCuratorBalance.t.sol::test_ReceiveCuratorBalance_RevertWhen_L1SenderNotL1GNS | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › claiming a curator balance with a message from L1 (onTokenTransfer) › if a subgraph does not exist, it returns the tokens to the beneficiary | test/unit/l2-gns/ReceiveCuratorBalance.t.sol::test_ReceiveCuratorBalance_ReturnsTokensWhenSubgraphDoesNotExist | L1GNS alias ETH funding dropped | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › claiming a curator balance with a message from L1 (onTokenTransfer) › for an L2-native subgraph, it sends the tokens to the beneficiary | test/unit/l2-gns/ReceiveCuratorBalance.t.sol::test_ReceiveCuratorBalance_ReturnsTokensForL2NativeSubgraph | As above | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › claiming a curator balance with a message from L1 (onTokenTransfer) › if a subgraph transfer was not finished, it returns the tokens to the beneficiary | test/unit/l2-gns/ReceiveCuratorBalance.t.sol::test_ReceiveCuratorBalance_ReturnsTokensWhenTransferNotFinished | As above | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › claiming a curator balance with a message from L1 (onTokenTransfer) › if a subgraph was deprecated after transfer, it returns the tokens to the beneficiary | test/unit/l2-gns/ReceiveCuratorBalance.t.sol::test_ReceiveCuratorBalance_ReturnsTokensWhenDeprecatedAfterTransfer | As above | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › onTokenTransfer with invalid codes › reverts | test/unit/l2-gns/ReceiveSubgraphFromL1.t.sol::test_OnTokenTransferWithInvalidCodes_RevertWhen_InvalidCode | | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › getAliasedL2SubgraphID › returns the L2 subgraph ID that is the L1 subgraph ID with an offset | test/unit/l2-gns/AliasedSubgraphId.t.sol::test_GetAliasedL2SubgraphID_AddsOffset | Same L1 ID | +| packages/contracts-test/tests/unit/l2/l2GNS.test.ts | L2GNS › getAliasedL2SubgraphID › wraps around MAX_UINT256 in case of overflow | test/unit/l2-gns/AliasedSubgraphId.t.sol::test_GetAliasedL2SubgraphID_WrapsAroundMaxUint256 | Also asserts the wrapped value is `offset - 1` | +| packages/contracts-test/tests/unit/gns.test.ts | L1GNS (whole file, `describe.skip`) | — | dropped: 88 cases, L1GNS | + +## Coverage + +Checked against `coverage-baseline.json` for `contracts/l2/discovery/L2GNS.sol`, `contracts/discovery/GNS.sol`, `contracts/discovery/SubgraphNFT.sol`, `contracts/base/Multicall.sol` and `contracts/utils/TokenUtils.sol`. + +MISSING initialize: `GNS.initialize` is called by `DiscoveryTest` through `abi.encodeCall` in the proxy init data (`acceptProxyAndCall`), so every test runs it. + +MISSING setSubgraphMetadata: `SubgraphNFT.setSubgraphMetadata` is `onlyMinter` (the minter is GNS) and is reached through `gns.publishNewSubgraph` and `gns.finishSubgraphTransferFromL1`. Likewise `SubgraphNFT.mint` and `burn` (the grep matches `.mint(`/`.burn(` on GRT and curation) are reached through `publishNewSubgraph` / the L1 subgraph callhook and `deprecateSubgraph`. `SubgraphNFT`'s constructor, `setMinter` and `setTokenDescriptor` run in `DiscoveryTest.setUp`. + +MISSING burnTokens, pullTokens, pushTokens: `TokenUtils` is an internal library. `pullTokens` is reached through `gns.mintSignal`, the owner tax in `gns.publishNewVersion` and `curation.mint`; `burnTokens` through `curation.mint` with a non-zero curation tax (the `PublishNewVersion` setup and pre-curation); `pushTokens` through `curation.burn` (`publishNewVersion`, `burnSignal`, `deprecateSubgraph`, and T3a's L2Curation burn tests). + +GNS functions not in the baseline (`setSubgraphNFT`, `setDefaultName`, `updateSubgraphMetadata`, `transferSignal`, `withdraw`, `migrateLegacySubgraph`, `subgraphSignal`, `subgraphTokens`, `isPublished`, `isLegacySubgraph`, `getLegacySubgraphKey`) were only exercised by the L1GNS suite in `gns.test.ts`, which was `describe.skip` and is dropped. + +Additions with no old test: `test/unit/l2-gns/Multicall.t.sol::test_GNS_Multicall` (`multicall` is in the baseline; publishes a subgraph and mints signal in one batch, as the skipped L1GNS `batch calls` test did).