diff --git a/addresses.json b/addresses.json index fb61bd9f4..cc4f0b887 100644 --- a/addresses.json +++ b/addresses.json @@ -119,7 +119,7 @@ "txHash": "0x2e44da799ad8866ac49aae2e40a16c57784ed4b1e9343daa4f764c39a05e0826" } }, - "GNS": { + "L1GNS": { "address": "0xaDcA0dd4729c8BA3aCf3E99F3A9f471EF37b6825", "initArgs": [ { @@ -425,7 +425,7 @@ "runtimeCodeHash": "0x6a7751298d6ffdbcf421a3b72faab5b7d425884b04757303123758dbcfb21dfa", "txHash": "0x8884b65a236c188e4c61cf9593be2f67b27e4f80785939336d659866cfd97aec" }, - "GNS": { + "L1GNS": { "address": "0x065611D3515325aE6fe14f09AEe5Aa2C0a1f0CA7", "initArgs": [ "0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B", @@ -582,9 +582,9 @@ "proxy": true, "implementation": { "address": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550", - "creationCodeHash": "0x729aca90fcffdeede93bc42a6e089a93085ec04133e965cf0291cf6245880595", - "runtimeCodeHash": "0xce525d338b6ed471eeb36d2927a26608cca2d5cfe52bd0585945eacc55b525cf", - "txHash": "0x376a2ebfe783246d83c3cbbdd95e20daa3cd3695a52d8e38699a7fa487d8ed64" + "creationCodeHash": "0x492b44ca23b4728151292c5a7a731da511619bbf4fc0194cb3158fde2a0794ed", + "runtimeCodeHash": "0x73009e4f97f097e7b5d67e1e1b6dd41ecc8f5363eb15484019b8000a6d0cb95c", + "txHash": "0xa9e5c9e3585bb68dc538062ca4c2dbfb58c3fc80523ca97c7d0d27f4a7ca1a09" } }, "GraphToken": { @@ -603,9 +603,9 @@ "proxy": true, "implementation": { "address": "0x0290FB167208Af455bB137780163b7B7a9a10C16", - "creationCodeHash": "0x45f56a7ad420cd11a8585594fb29121747d87d412161c8779ea36dfd34a48e88", - "runtimeCodeHash": "0x26aceabe83e2b757b2f000e185017528cdde2323c2129fd612180ac3192adfda", - "txHash": "0x567c09fd4920dd8ec2e359bd3b2a77aa69658af1ff515fe6d687007967229bee" + "creationCodeHash": "0xad443a9c9bf6a6049265e253dc99658bf99e4091c939f68972c5298926d7689d", + "runtimeCodeHash": "0x495a9a8de4aed745b0521e8b24661cf26ff12a9993a0ec5ef17728271a6f8629", + "txHash": "0x69a51f8846d42a786314d56ce00b7321a6576cd8bdc0d5898dd6f3ccb1c63c87" } }, "Curation": { @@ -624,12 +624,12 @@ "proxy": true, "implementation": { "address": "0x2612Af3A521c2df9EAF28422Ca335b04AdF3ac66", - "creationCodeHash": "0x022576ab4b739ee17dab126ea7e5a6814bda724aa0e4c6735a051b38a76bd597", - "runtimeCodeHash": "0xc7b1f9bef01ef92779aab0ae9be86376c47584118c508f5b4e612a694a4aab93", - "txHash": "0xc93d39f849b249792924ee973c022aea2445c6662ce26f450d324b1c721c25a7" + "creationCodeHash": "0xe69ca2e0119fb769311ecd3d4de6b12fd0cedfb56eeb4c537bd3defa2adcca43", + "runtimeCodeHash": "0x364e9b3216fa3a571e8be3cdb757fa007ee8a2afe384396e4a7cda3de79ce4d9", + "txHash": "0xc3278c3fae8f2cfab00755537c9a8d6712e1e8027f46a9ef99eb3b9231620ab2" } }, - "GNS": { + "L1GNS": { "address": "0xA94B7f0465E98609391C623d0560C5720a3f2D33", "initArgs": [ "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", @@ -642,9 +642,9 @@ "proxy": true, "implementation": { "address": "0xDb56f2e9369E0D7bD191099125a3f6C370F8ed15", - "creationCodeHash": "0x2e71e4aefc1e678cb9c71882c1da67fc640389337a7d6ae43f78d0f13294594a", - "runtimeCodeHash": "0xde0e02c6a36a90e11c768f40a81430b7e9cda261aa6dada14eaad392d42efc21", - "txHash": "0x4032407d59e4ac88868270bb8d920bfcc8fe6572a22ad4f3be9c64da5a8f926e" + "creationCodeHash": "0xfbdc6caf28aa09493e0e0032aac06cdb2be8c5f62b8c839876d62d2bb2977e3d", + "runtimeCodeHash": "0x106af7614bdb7cdf60a6a93e5c92dbee03e36c799880d9ee8e8e9585fc077f72", + "txHash": "0xb1e63211ea7b036bf35423034bc60490b3b35b199bddc85200ea926b76e16a4e" } }, "Staking": { @@ -668,9 +668,9 @@ "proxy": true, "implementation": { "address": "0xFC628dd79137395F3C9744e33b1c5DE554D94882", - "creationCodeHash": "0x55e99794a19a3fea4152ac8cbbec6ed93e88fa0b09e21ac6fbf00f39bfa928f6", - "runtimeCodeHash": "0xef297f45b62801f615d3271bb40f07a30a9906be0f70f2d57dbf6a44408191d3", - "txHash": "0x149846f986f24a619f1137be908ed2cf82dce52c18bcbeacefdb663b1a6dd765", + "creationCodeHash": "0xa6ad6904fe70424527494f1401d71972967da4e35dea7ca01858063a56550e42", + "runtimeCodeHash": "0x3146935df7968ca2b32b0610ddb25e40148a9c007d3e81b367a10342b6eed13b", + "txHash": "0xb37e221c74a2237c0d63cc61242106c426b1b46041e6e0e27467f90c4e01da88", "libraries": { "LibCobbDouglas": "0xb09bCc172050fBd4562da8b229Cf3E45Dc3045A6" } @@ -678,23 +678,23 @@ }, "RewardsManager": { "address": "0x4bf749ec68270027C5910220CEAB30Cc284c7BA2", - "initArgs": ["0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", "1000000012184945188"], + "initArgs": ["0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B"], "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", "txHash": "0xf4a13ad82b067ffb59e689df3cb828a5cd4eac7e316323dfbb4b05b191127ce5", "proxy": true, "implementation": { "address": "0xD86C8F0327494034F60e25074420BcCF560D5610", - "creationCodeHash": "0xfec6d35d9de8a7234e77484ee4fa5d6867697d607b591ed5a8e01679fa1f0394", - "runtimeCodeHash": "0x4595f2b6c37d65ad1deed2497054b2319fb0c6419439e2e374b29a29aa9fcb81", - "txHash": "0xeea5271a0af6be6cc23e7a98fa84343d6b2c2aefaf1a80be63e945be332b5b0e" + "creationCodeHash": "0x5579914062ff21ef47c68cfab1eddbef1c320ae50e769dc430b73fcb995c5095", + "runtimeCodeHash": "0xae6b7b7a6f02d5964d4a35d66c906dd0fb5a5fb00549e646a586465e97218402", + "txHash": "0x8f411197f5b89b40fd61e2a8d35a9740279cff4fb2a7c2231f3faba1b8d4f581" } }, "DisputeManager": { "address": "0x5017A545b09ab9a30499DE7F431DF0855bCb7275", "initArgs": [ "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", - "0xFD01aa87BeB04D0ac764FC298aCFd05FfC5439cD", + "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", "10000000000000000000000", "500000", "25000", @@ -706,9 +706,9 @@ "proxy": true, "implementation": { "address": "0x7C728214be9A0049e6a86f2137ec61030D0AA964", - "creationCodeHash": "0x5b73c9b910d66426fd965ac3110e9debda1d81134c0354a7af8ec1f2ebd765f6", - "runtimeCodeHash": "0xcaf3547f0d675a1e1d2f887cf4666410bc3b084e65ad283ed3f1ff2b1bccc113", - "txHash": "0x29822affa517965e1995fc4e777cd709daf9df8f16f13e08b3829bba5c50bf90" + "creationCodeHash": "0xbcdd3847552c8819e7b65d77b6929f2b61cd5a7522d1355f2bb1a0c2a099f713", + "runtimeCodeHash": "0x2c0589b92badf53b7cb8a0570e4d28ceefff0add59eb2e75e59e4ae4f76592ff", + "txHash": "0x6773e7db3d0991ad4541cdceb64c035e3c0cc7f5e9ecf7749ba2e699b8793bcf" } }, "EthereumDIDRegistry": { @@ -732,21 +732,33 @@ "SubgraphNFT": { "address": "0x0E696947A06550DEf604e82C26fd9E493e576337", "constructorArgs": ["0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"], - "creationCodeHash": "0x5de044b15df24beb8781d1ebe71f01301a6b8985183f37eb8d599aa4059a1d3e", - "runtimeCodeHash": "0x6a7751298d6ffdbcf421a3b72faab5b7d425884b04757303123758dbcfb21dfa", - "txHash": "0xcb40328bd03b6b25e74203e10f9ce17a131aa514f6ba9156aa8fcb81fe5f8cc2" + "creationCodeHash": "0xc3559f8ffca442b8a3706003d3c89d11bc918398551a197bbbd66ae649cc14c4", + "runtimeCodeHash": "0x16c4bfbb2374879d3f9373178fe14170332e274a3a4e6a07f7ffc5194420584d", + "txHash": "0xa03c6e4755494c8334fa9175941cb5655be943a930950312a6e3572204d6259f" }, "AllocationExchange": { "address": "0xFF6049B87215476aBf744eaA3a476cBAd46fB1cA", "constructorArgs": [ "0xe982E462b094850F12AF94d21D470e21bE9D0E9C", "0x5f8e26fAcC23FA4cbd87b8d9Dbbd33D5047abDE1", - "0xf1135bFF22512FF2A585b8d4489426CE660f204c", - "0x52e498aE9B8A5eE2A5Cd26805F06A9f29A7F489F" + "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9", + "0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d" ], - "creationCodeHash": "0x97714e1a80674ab0af90a10f2c7156cc92794ef81565fe9c7c35ecbe0025cc08", - "runtimeCodeHash": "0x07012b5692ec6cbeb7a6986e061fc5026a2f76545b07bfd9182985de002fa281", - "txHash": "0xe3d870434e38ee37142a86e0fc54063df59c02c3b70135f070c3a1025c5e8246" + "creationCodeHash": "0xe7db7b38369ff61ea6cb2abdaf64f94deb88703faec5fa7a33866d1144a7da5f", + "runtimeCodeHash": "0x0792084bfc42580dc14eff231a75eab772eca117894dca8f1544cf0d38df219c", + "txHash": "0xeb2ac7e11256e10591b396fff48d0526c6bab20f9d45036ba07b8e32238d8397" + }, + "L1GraphTokenGateway": { + "address": "0xA586074FA4Fe3E546A132a16238abe37951D41fE", + "creationCodeHash": "0x506b750ce67ef926070c8918e372003d0cd9d21f8198a1e5447ff65a8ca8759e", + "runtimeCodeHash": "0x6cc716875c9de6a3bdc8b53366cb7adf83f96f2254b1f3171c996ac99449bc8c", + "txHash": "0x5f49cd4389f3c59b18bf1bcc7f5bf6feaa4a5e1e3f08b66805b4e1b7329a991c" + }, + "BridgeEscrow": { + "address": "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A", + "creationCodeHash": "0x09b0de6d1f3afb28f3008befbc5c6303a8d510c31a4364483c009f3446082175", + "runtimeCodeHash": "0x7c242c472191805935e451bae6aaf0417ff7192d0b2a76422bc1c93b2284e2d4", + "txHash": "0x1881f59227e8f77a4b28c1877d5a4b08df576e1a22785800e35aeacfb3f6958e" } }, "42161": { diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 335b6fa32..4c9c03768 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -29,7 +29,7 @@ let allContracts = [ 'Curation', 'SubgraphNFTDescriptor', 'SubgraphNFT', - 'GNS', + 'L1GNS', 'Staking', 'RewardsManager', 'DisputeManager', @@ -40,16 +40,15 @@ let allContracts = [ const l2Contracts = [ 'GraphProxyAdmin', - 'BancorFormula', 'Controller', 'EpochManager', 'L2GraphToken', 'GraphCurationToken', 'ServiceRegistry', - 'Curation', + 'L2Curation', 'SubgraphNFTDescriptor', 'SubgraphNFT', - 'GNS', + 'L2GNS', 'Staking', 'RewardsManager', 'DisputeManager', diff --git a/cli/contracts.ts b/cli/contracts.ts index c36e83567..3a20bc727 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -23,6 +23,8 @@ import { ServiceRegistry } from '../build/types/ServiceRegistry' import { Curation } from '../build/types/Curation' import { RewardsManager } from '../build/types/RewardsManager' import { GNS } from '../build/types/GNS' +import { L1GNS } from '../build/types/L1GNS' +import { L2GNS } from '../build/types/L2GNS' import { GraphProxyAdmin } from '../build/types/GraphProxyAdmin' import { GraphToken } from '../build/types/GraphToken' import { Controller } from '../build/types/Controller' @@ -37,15 +39,17 @@ import { L1GraphTokenGateway } from '../build/types/L1GraphTokenGateway' import { L2GraphToken } from '../build/types/L2GraphToken' import { L2GraphTokenGateway } from '../build/types/L2GraphTokenGateway' import { BridgeEscrow } from '../build/types/BridgeEscrow' +import { L2Curation } from '../build/types/L2Curation' export interface NetworkContracts { EpochManager: EpochManager DisputeManager: DisputeManager Staking: Staking ServiceRegistry: ServiceRegistry - Curation: Curation + Curation: Curation | L2Curation + L2Curation: L2Curation RewardsManager: RewardsManager - GNS: GNS + GNS: GNS | L1GNS | L2GNS GraphProxyAdmin: GraphProxyAdmin GraphToken: GraphToken Controller: Controller @@ -60,6 +64,8 @@ export interface NetworkContracts { BridgeEscrow: BridgeEscrow L2GraphToken: L2GraphToken L2GraphTokenGateway: L2GraphTokenGateway + L1GNS: L1GNS + L2GNS: L2GNS } export const loadAddressBookContract = ( @@ -101,6 +107,15 @@ export const loadContracts = ( if (chainIdIsL2(chainId) && contractName == 'L2GraphToken') { contracts['GraphToken'] = contracts[contractName] } + if (signerOrProvider && chainIdIsL2(chainId) && contractName == 'L2GNS') { + contracts['GNS'] = contracts[contractName] + } + if (signerOrProvider && chainIdIsL2(chainId) && contractName == 'L2Curation') { + contracts['Curation'] = contracts[contractName] + } + if (signerOrProvider && !chainIdIsL2(chainId) && contractName == 'L1GNS') { + contracts['GNS'] = contracts[contractName] + } } catch (err) { logger.warn(`Could not load contract ${contractName} - ${err.message}`) } diff --git a/config/graph.arbitrum-goerli.yml b/config/graph.arbitrum-goerli.yml index 234a41f44..0f2bd8642 100644 --- a/config/graph.arbitrum-goerli.yml +++ b/config/graph.arbitrum-goerli.yml @@ -11,10 +11,10 @@ contracts: calls: - fn: "setContractProxy" id: "0xe6876326c1291dfcbbd3864a6816d698cd591defc7aa2153d7f9c4c04016c89f" # keccak256('Curation') - contractAddress: "${{Curation.address}}" + contractAddress: "${{L2Curation.address}}" - fn: "setContractProxy" id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') - contractAddress: "${{GNS.address}}" + contractAddress: "${{L2GNS.address}}" - fn: "setContractProxy" id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') contractAddress: "${{DisputeManager.address}}" @@ -62,15 +62,13 @@ contracts: - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor - Curation: + L2Curation: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" curationTokenMaster: "${{GraphCurationToken.address}}" - reserveRatio: 1000000 # in parts per million curationTaxPercentage: 10000 # in parts per million - minimumCurationDeposit: "1000000000000000000" # in wei + minimumCurationDeposit: "1" # in wei calls: - fn: "syncAllContracts" DisputeManager: @@ -84,11 +82,10 @@ contracts: qrySlashingPercentage: 25000 # in parts per million calls: - fn: "syncAllContracts" - GNS: + L2GNS: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" subgraphNFT: "${{SubgraphNFT.address}}" calls: - fn: "approveAll" @@ -100,7 +97,7 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L2GNS.address}}" - fn: "transferOwnership" owner: *governor Staking: diff --git a/config/graph.arbitrum-localhost.yml b/config/graph.arbitrum-localhost.yml index 153d9c533..521ccc7c3 100644 --- a/config/graph.arbitrum-localhost.yml +++ b/config/graph.arbitrum-localhost.yml @@ -11,10 +11,10 @@ contracts: calls: - fn: "setContractProxy" id: "0xe6876326c1291dfcbbd3864a6816d698cd591defc7aa2153d7f9c4c04016c89f" # keccak256('Curation') - contractAddress: "${{Curation.address}}" + contractAddress: "${{L2Curation.address}}" - fn: "setContractProxy" id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') - contractAddress: "${{GNS.address}}" + contractAddress: "${{L2GNS.address}}" - fn: "setContractProxy" id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') contractAddress: "${{DisputeManager.address}}" @@ -62,15 +62,13 @@ contracts: - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor - Curation: + L2Curation: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" curationTokenMaster: "${{GraphCurationToken.address}}" - reserveRatio: 1000000 # in parts per million curationTaxPercentage: 10000 # in parts per million - minimumCurationDeposit: "1000000000000000000" # in wei + minimumCurationDeposit: "1" # in wei calls: - fn: "syncAllContracts" DisputeManager: @@ -84,11 +82,10 @@ contracts: qrySlashingPercentage: 25000 # in parts per million calls: - fn: "syncAllContracts" - GNS: + L2GNS: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" subgraphNFT: "${{SubgraphNFT.address}}" calls: - fn: "approveAll" @@ -100,7 +97,7 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L2GNS.address}}" - fn: "transferOwnership" owner: *governor Staking: diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index bd368311f..4135a5032 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -11,10 +11,10 @@ contracts: calls: - fn: "setContractProxy" id: "0xe6876326c1291dfcbbd3864a6816d698cd591defc7aa2153d7f9c4c04016c89f" # keccak256('Curation') - contractAddress: "${{Curation.address}}" + contractAddress: "${{L2Curation.address}}" - fn: "setContractProxy" id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') - contractAddress: "${{GNS.address}}" + contractAddress: "${{L2GNS.address}}" - fn: "setContractProxy" id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') contractAddress: "${{DisputeManager.address}}" @@ -62,15 +62,13 @@ contracts: - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor - Curation: + L2Curation: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" curationTokenMaster: "${{GraphCurationToken.address}}" - reserveRatio: 1000000 # in parts per million curationTaxPercentage: 10000 # in parts per million - minimumCurationDeposit: "1000000000000000000" # in wei + minimumCurationDeposit: "1" # in wei calls: - fn: "syncAllContracts" DisputeManager: @@ -84,11 +82,10 @@ contracts: qrySlashingPercentage: 25000 # in parts per million calls: - fn: "syncAllContracts" - GNS: + L2GNS: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" subgraphNFT: "${{SubgraphNFT.address}}" calls: - fn: "approveAll" @@ -100,7 +97,7 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L2GNS.address}}" - fn: "transferOwnership" owner: *governor Staking: diff --git a/config/graph.goerli.yml b/config/graph.goerli.yml index f78be6e4e..c6cd8adc7 100644 --- a/config/graph.goerli.yml +++ b/config/graph.goerli.yml @@ -14,7 +14,7 @@ contracts: contractAddress: "${{Curation.address}}" - fn: "setContractProxy" id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') - contractAddress: "${{GNS.address}}" + contractAddress: "${{L1GNS.address}}" - fn: "setContractProxy" id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') contractAddress: "${{DisputeManager.address}}" @@ -85,11 +85,10 @@ contracts: qrySlashingPercentage: 25000 # in parts per million calls: - fn: "syncAllContracts" - GNS: + L1GNS: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" subgraphNFT: "${{SubgraphNFT.address}}" calls: - fn: "approveAll" @@ -101,7 +100,7 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L1GNS.address}}" - fn: "transferOwnership" owner: *governor Staking: diff --git a/config/graph.localhost.yml b/config/graph.localhost.yml index 984e97b16..176f3080b 100644 --- a/config/graph.localhost.yml +++ b/config/graph.localhost.yml @@ -14,7 +14,7 @@ contracts: contractAddress: "${{Curation.address}}" - fn: "setContractProxy" id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') - contractAddress: "${{GNS.address}}" + contractAddress: "${{L1GNS.address}}" - fn: "setContractProxy" id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') contractAddress: "${{DisputeManager.address}}" @@ -85,11 +85,10 @@ contracts: qrySlashingPercentage: 25000 # in parts per million calls: - fn: "syncAllContracts" - GNS: + L1GNS: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" subgraphNFT: "${{SubgraphNFT.address}}" calls: - fn: "approveAll" @@ -101,7 +100,7 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L1GNS.address}}" - fn: "transferOwnership" owner: *governor Staking: diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index ec826fffe..c42ac90c8 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -14,7 +14,7 @@ contracts: contractAddress: "${{Curation.address}}" - fn: "setContractProxy" id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') - contractAddress: "${{GNS.address}}" + contractAddress: "${{L1GNS.address}}" - fn: "setContractProxy" id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') contractAddress: "${{DisputeManager.address}}" @@ -85,11 +85,10 @@ contracts: qrySlashingPercentage: 25000 # in parts per million calls: - fn: "syncAllContracts" - GNS: + L1GNS: proxy: true init: controller: "${{Controller.address}}" - bondingCurve: "${{BancorFormula.address}}" subgraphNFT: "${{SubgraphNFT.address}}" calls: - fn: "approveAll" @@ -101,7 +100,7 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L1GNS.address}}" - fn: "transferOwnership" owner: *governor Staking: diff --git a/contracts/curation/Curation.sol b/contracts/curation/Curation.sol index 565a51a89..04c3a6217 100644 --- a/contracts/curation/Curation.sol +++ b/contracts/curation/Curation.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6; +pragma abicoder v2; -import { Address } from "@openzeppelin/contracts/utils/Address.sol"; -import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; +import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import { ClonesUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol"; import { BancorFormula } from "../bancor/BancorFormula.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; @@ -12,10 +13,9 @@ import { TokenUtils } from "../utils/TokenUtils.sol"; import { IRewardsManager } from "../rewards/IRewardsManager.sol"; import { Managed } from "../governance/Managed.sol"; import { IGraphToken } from "../token/IGraphToken.sol"; -import { CurationV1Storage } from "./CurationStorage.sol"; +import { CurationV2Storage } from "./CurationStorage.sol"; import { ICuration } from "./ICuration.sol"; import { IGraphCurationToken } from "./IGraphCurationToken.sol"; -import { GraphCurationToken } from "./GraphCurationToken.sol"; /** * @title Curation contract @@ -29,13 +29,13 @@ import { GraphCurationToken } from "./GraphCurationToken.sol"; * Holders can burn GCS using this contract to get GRT tokens back according to the * bonding curve. */ -contract Curation is CurationV1Storage, GraphUpgradeable { - using SafeMath for uint256; +contract Curation is CurationV2Storage, GraphUpgradeable { + using SafeMathUpgradeable for uint256; - // 100% in parts per million + /// @dev 100% in parts per million uint32 private constant MAX_PPM = 1000000; - // Amount of signal you get with your minimum token deposit + /// @dev Amount of signal you get with your minimum token deposit uint256 private constant SIGNAL_PER_MINIMUM_DEPOSIT = 1e18; // 1 signal as 18 decimal number // -- Events -- @@ -71,7 +71,13 @@ contract Curation is CurationV1Storage, GraphUpgradeable { event Collected(bytes32 indexed subgraphDeploymentID, uint256 tokens); /** - * @dev Initialize this contract. + * @notice Initialize this contract. + * @param _controller Address of the controller contract that manages this contract + * @param _bondingCurve Address of the bonding curve contract (e.g. a BancorFormula) + * @param _curationTokenMaster Address of the master copy to use for curation tokens + * @param _defaultReserveRatio Default reserve ratio for a curation pool in PPM + * @param _curationTaxPercentage Percentage of curation tax to charge when depositing GRT tokens + * @param _minimumCurationDeposit Minimum amount of tokens that can be deposited on a new subgraph deployment */ function initialize( address _controller, @@ -80,7 +86,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable { uint32 _defaultReserveRatio, uint32 _curationTaxPercentage, uint256 _minimumCurationDeposit - ) external onlyImpl { + ) external onlyImpl initializer { Managed._initialize(_controller); require(_bondingCurve != address(0), "Bonding curve must be set"); @@ -95,30 +101,13 @@ contract Curation is CurationV1Storage, GraphUpgradeable { /** * @dev Set the default reserve ratio percentage for a curation pool. - * @notice Update the default reserver ratio to `_defaultReserveRatio` + * @notice Update the default reserve ratio to `_defaultReserveRatio` * @param _defaultReserveRatio Reserve ratio (in PPM) */ function setDefaultReserveRatio(uint32 _defaultReserveRatio) external override onlyGovernor { _setDefaultReserveRatio(_defaultReserveRatio); } - /** - * @dev Internal: Set the default reserve ratio percentage for a curation pool. - * @notice Update the default reserver ratio to `_defaultReserveRatio` - * @param _defaultReserveRatio Reserve ratio (in PPM) - */ - function _setDefaultReserveRatio(uint32 _defaultReserveRatio) private { - // Reserve Ratio must be within 0% to 100% (inclusive, in PPM) - require(_defaultReserveRatio > 0, "Default reserve ratio must be > 0"); - require( - _defaultReserveRatio <= MAX_PPM, - "Default reserve ratio cannot be higher than MAX_PPM" - ); - - defaultReserveRatio = _defaultReserveRatio; - emit ParameterUpdated("defaultReserveRatio"); - } - /** * @dev Set the minimum deposit amount for curators. * @notice Update the minimum deposit amount to `_minimumCurationDeposit` @@ -133,19 +122,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable { } /** - * @dev Internal: Set the minimum deposit amount for curators. - * @notice Update the minimum deposit amount to `_minimumCurationDeposit` - * @param _minimumCurationDeposit Minimum amount of tokens required deposit - */ - function _setMinimumCurationDeposit(uint256 _minimumCurationDeposit) private { - require(_minimumCurationDeposit > 0, "Minimum curation deposit cannot be 0"); - - minimumCurationDeposit = _minimumCurationDeposit; - emit ParameterUpdated("minimumCurationDeposit"); - } - - /** - * @dev Set the curation tax percentage to charge when a curator deposits GRT tokens. + * @notice Set the curation tax percentage to charge when a curator deposits GRT tokens. * @param _percentage Curation tax percentage charged when depositing GRT tokens */ function setCurationTaxPercentage(uint32 _percentage) external override onlyGovernor { @@ -153,21 +130,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable { } /** - * @dev Internal: Set the curation tax percentage to charge when a curator deposits GRT tokens. - * @param _percentage Curation tax percentage charged when depositing GRT tokens - */ - function _setCurationTaxPercentage(uint32 _percentage) private { - require( - _percentage <= MAX_PPM, - "Curation tax percentage must be below or equal to MAX_PPM" - ); - - curationTaxPercentage = _percentage; - emit ParameterUpdated("curationTaxPercentage"); - } - - /** - * @dev Set the master copy to use as clones for the curation token. + * @notice Set the master copy to use as clones for the curation token. * @param _curationTokenMaster Address of implementation contract to use for curation tokens */ function setCurationTokenMaster(address _curationTokenMaster) external override onlyGovernor { @@ -175,20 +138,8 @@ contract Curation is CurationV1Storage, GraphUpgradeable { } /** - * @dev Internal: Set the master copy to use as clones for the curation token. - * @param _curationTokenMaster Address of implementation contract to use for curation tokens - */ - function _setCurationTokenMaster(address _curationTokenMaster) private { - require(_curationTokenMaster != address(0), "Token master must be non-empty"); - require(Address.isContract(_curationTokenMaster), "Token master must be a contract"); - - curationTokenMaster = _curationTokenMaster; - emit ParameterUpdated("curationTokenMaster"); - } - - /** - * @dev Assign Graph Tokens collected as curation fees to the curation pool reserve. - * This function can only be called by the Staking contract and will do the bookeeping of + * @notice Assign Graph Tokens collected as curation fees to the curation pool reserve. + * @dev This function can only be called by the Staking contract and will do the bookkeeping of * transferred tokens into this contract. * @param _subgraphDeploymentID SubgraphDeployment where funds should be allocated as reserves * @param _tokens Amount of Graph Tokens to add to reserves @@ -211,11 +162,12 @@ contract Curation is CurationV1Storage, GraphUpgradeable { } /** - * @dev Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. + * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal * @param _tokensIn Amount of Graph Tokens to deposit * @param _signalOutMin Expected minimum amount of signal to receive - * @return Signal minted and deposit tax + * @return Amount of signal minted + * @return Amount of curation tax burned */ function mint( bytes32 _subgraphDeploymentID, @@ -223,7 +175,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable { uint256 _signalOutMin ) external override notPartialPaused returns (uint256, uint256) { // Need to deposit some funds - require(_tokensIn > 0, "Cannot deposit zero tokens"); + require(_tokensIn != 0, "Cannot deposit zero tokens"); // Exchange GRT tokens for GCS of the subgraph pool (uint256 signalOut, uint256 curationTax) = tokensToSignal(_subgraphDeploymentID, _tokensIn); @@ -241,7 +193,9 @@ contract Curation is CurationV1Storage, GraphUpgradeable { // If no signal token for the pool - create one if (address(curationPool.gcs) == address(0)) { // Use a minimal proxy to reduce gas cost - IGraphCurationToken gcs = IGraphCurationToken(Clones.clone(curationTokenMaster)); + IGraphCurationToken gcs = IGraphCurationToken( + ClonesUpgradeable.clone(curationTokenMaster) + ); gcs.initialize(address(this)); curationPool.gcs = gcs; } @@ -283,7 +237,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable { address curator = msg.sender; // Validations - require(_signalIn > 0, "Cannot burn zero signal"); + require(_signalIn != 0, "Cannot burn zero signal"); require( getCuratorSignal(curator, _subgraphDeploymentID) >= _signalIn, "Cannot burn more signal than you own" @@ -319,16 +273,30 @@ contract Curation is CurationV1Storage, GraphUpgradeable { } /** - * @dev Check if any GRT tokens are deposited for a SubgraphDeployment. + * @notice Get the amount of token reserves in a curation pool. + * @param _subgraphDeploymentID Subgraph deployment curation poool + * @return Amount of token reserves in the curation pool + */ + function getCurationPoolTokens(bytes32 _subgraphDeploymentID) + external + view + override + returns (uint256) + { + return pools[_subgraphDeploymentID].tokens; + } + + /** + * @notice Check if any GRT tokens are deposited for a SubgraphDeployment. * @param _subgraphDeploymentID SubgraphDeployment to check if curated - * @return True if curated + * @return True if curated, false otherwise */ function isCurated(bytes32 _subgraphDeploymentID) public view override returns (bool) { - return pools[_subgraphDeploymentID].tokens > 0; + return pools[_subgraphDeploymentID].tokens != 0; } /** - * @dev Get the amount of signal a curator has in a curation pool. + * @notice Get the amount of signal a curator has in a curation pool. * @param _curator Curator owning the signal tokens * @param _subgraphDeploymentID Subgraph deployment curation pool * @return Amount of signal owned by a curator for the subgraph deployment @@ -344,7 +312,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable { } /** - * @dev Get the amount of signal in a curation pool. + * @notice Get the amount of signal in a curation pool. * @param _subgraphDeploymentID Subgraph deployment curation poool * @return Amount of signal minted for the subgraph deployment */ @@ -359,25 +327,12 @@ contract Curation is CurationV1Storage, GraphUpgradeable { } /** - * @dev Get the amount of token reserves in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation poool - * @return Amount of token reserves in the curation pool - */ - function getCurationPoolTokens(bytes32 _subgraphDeploymentID) - external - view - override - returns (uint256) - { - return pools[_subgraphDeploymentID].tokens; - } - - /** - * @dev Calculate amount of signal that can be bought with tokens in a curation pool. + * @notice Calculate amount of signal that can be bought with tokens in a curation pool. * This function considers and excludes the deposit tax. * @param _subgraphDeploymentID Subgraph deployment to mint signal * @param _tokensIn Amount of tokens used to mint signal - * @return Amount of signal that can be bought and tokens subtracted for the tax + * @return Amount of signal that can be bought + * @return Amount of tokens that will be burned as curation tax */ function tokensToSignal(bytes32 _subgraphDeploymentID, uint256 _tokensIn) public @@ -431,10 +386,10 @@ contract Curation is CurationV1Storage, GraphUpgradeable { } /** - * @dev Calculate number of tokens to get when burning signal from a curation pool. + * @notice Calculate number of tokens to get when burning signal from a curation pool. * @param _subgraphDeploymentID Subgraph deployment to burn signal * @param _signalIn Amount of signal to burn - * @return Amount of tokens to get for an amount of signal + * @return Amount of tokens to get for the specified amount of signal */ function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signalIn) public @@ -445,7 +400,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable { CurationPool memory curationPool = pools[_subgraphDeploymentID]; uint256 curationPoolSignal = getCurationPoolSignal(_subgraphDeploymentID); require( - curationPool.tokens > 0, + curationPool.tokens != 0, "Subgraph deployment must be curated to perform calculations" ); require( @@ -462,6 +417,64 @@ contract Curation is CurationV1Storage, GraphUpgradeable { ); } + /** + * @dev Internal: Set the default reserve ratio percentage for a curation pool. + * @notice Update the default reserver ratio to `_defaultReserveRatio` + * @param _defaultReserveRatio Reserve ratio (in PPM) + */ + function _setDefaultReserveRatio(uint32 _defaultReserveRatio) private { + // Reserve Ratio must be within 0% to 100% (inclusive, in PPM) + require(_defaultReserveRatio != 0, "Default reserve ratio must be > 0"); + require( + _defaultReserveRatio <= MAX_PPM, + "Default reserve ratio cannot be higher than MAX_PPM" + ); + + defaultReserveRatio = _defaultReserveRatio; + emit ParameterUpdated("defaultReserveRatio"); + } + + /** + * @dev Internal: Set the minimum deposit amount for curators. + * @notice Update the minimum deposit amount to `_minimumCurationDeposit` + * @param _minimumCurationDeposit Minimum amount of tokens required deposit + */ + function _setMinimumCurationDeposit(uint256 _minimumCurationDeposit) private { + require(_minimumCurationDeposit != 0, "Minimum curation deposit cannot be 0"); + + minimumCurationDeposit = _minimumCurationDeposit; + emit ParameterUpdated("minimumCurationDeposit"); + } + + /** + * @dev Internal: Set the curation tax percentage (in PPM) to charge when a curator deposits GRT tokens. + * @param _percentage Curation tax charged when depositing GRT tokens in PPM + */ + function _setCurationTaxPercentage(uint32 _percentage) private { + require( + _percentage <= MAX_PPM, + "Curation tax percentage must be below or equal to MAX_PPM" + ); + + curationTaxPercentage = _percentage; + emit ParameterUpdated("curationTaxPercentage"); + } + + /** + * @dev Internal: Set the master copy to use as clones for the curation token. + * @param _curationTokenMaster Address of implementation contract to use for curation tokens + */ + function _setCurationTokenMaster(address _curationTokenMaster) private { + require(_curationTokenMaster != address(0), "Token master must be non-empty"); + require( + AddressUpgradeable.isContract(_curationTokenMaster), + "Token master must be a contract" + ); + + curationTokenMaster = _curationTokenMaster; + emit ParameterUpdated("curationTokenMaster"); + } + /** * @dev Triggers an update of rewards due to a change in signal. * @param _subgraphDeploymentID Subgraph deployment updated diff --git a/contracts/curation/CurationStorage.sol b/contracts/curation/CurationStorage.sol index a530d9199..bcbf0df6b 100644 --- a/contracts/curation/CurationStorage.sol +++ b/contracts/curation/CurationStorage.sol @@ -2,41 +2,69 @@ pragma solidity ^0.7.6; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; + import { ICuration } from "./ICuration.sol"; import { IGraphCurationToken } from "./IGraphCurationToken.sol"; import { Managed } from "../governance/Managed.sol"; +/** + * @title Curation Storage version 1 + * @dev This contract holds the first version of the storage variables + * for the Curation and L2Curation contracts. + * When adding new variables, create a new version that inherits this and update + * the contracts to use the new version instead. + */ abstract contract CurationV1Storage is Managed, ICuration { // -- Pool -- + /** + * @dev CurationPool structure that holds the pool's state + * for a particular subgraph deployment. + */ struct CurationPool { uint256 tokens; // GRT Tokens stored as reserves for the subgraph deployment - uint32 reserveRatio; // Ratio for the bonding curve + uint32 reserveRatio; // Ratio for the bonding curve, unused and deprecated in L2 where it will always be 100% but appear as 0 IGraphCurationToken gcs; // Curation token contract for this curation pool } // -- State -- - // Tax charged when curator deposit funds - // Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + /// Tax charged when curators deposit funds. + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) uint32 public override curationTaxPercentage; - // Default reserve ratio to configure curator shares bonding curve - // Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + /// Default reserve ratio to configure curator shares bonding curve + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%). + /// Unused in L2. uint32 public defaultReserveRatio; - // Master copy address that holds implementation of curation token - // This is used as the target for GraphCurationToken clones + /// Master copy address that holds implementation of curation token. + /// @dev This is used as the target for GraphCurationToken clones. address public curationTokenMaster; - // Minimum amount allowed to be deposited by curators to initialize a pool - // This is the `startPoolBalance` for the bonding curve + /// Minimum amount allowed to be deposited by curators to initialize a pool + /// @dev This is the `startPoolBalance` for the bonding curve uint256 public minimumCurationDeposit; - // Bonding curve library + /// Bonding curve library + /// Unused in L2. address public bondingCurve; - // Mapping of subgraphDeploymentID => CurationPool - // There is only one CurationPool per SubgraphDeploymentID + /// @dev Mapping of subgraphDeploymentID => CurationPool + /// There is only one CurationPool per SubgraphDeploymentID mapping(bytes32 => CurationPool) public pools; } + +/** + * @title Curation Storage version 2 + * @dev This contract holds the second version of the storage variables + * for the Curation and L2Curation contracts. + * It doesn't add new variables at this contract's level, but adds the Initializable + * contract to the inheritance chain, which includes storage variables. + * When adding new variables, create a new version that inherits this and update + * the contracts to use the new version instead. + */ +abstract contract CurationV2Storage is CurationV1Storage, Initializable { + // Nothing here, just adding Initializable +} diff --git a/contracts/curation/ICuration.sol b/contracts/curation/ICuration.sol index 9e1701aaf..dffff46cd 100644 --- a/contracts/curation/ICuration.sol +++ b/contracts/curation/ICuration.sol @@ -2,57 +2,135 @@ pragma solidity ^0.7.6; -import "./IGraphCurationToken.sol"; - +/** + * @title Curation Interface + * @dev Interface for the Curation contract (and L2Curation too) + */ interface ICuration { // -- Configuration -- + /** + * @notice Update the default reserve ratio to `_defaultReserveRatio` + * @param _defaultReserveRatio Reserve ratio (in PPM) + */ function setDefaultReserveRatio(uint32 _defaultReserveRatio) external; + /** + * @notice Update the minimum deposit amount needed to intialize a new subgraph + * @param _minimumCurationDeposit Minimum amount of tokens required deposit + */ function setMinimumCurationDeposit(uint256 _minimumCurationDeposit) external; + /** + * @notice Set the curation tax percentage to charge when a curator deposits GRT tokens. + * @param _percentage Curation tax percentage charged when depositing GRT tokens + */ function setCurationTaxPercentage(uint32 _percentage) external; + /** + * @notice Set the master copy to use as clones for the curation token. + * @param _curationTokenMaster Address of implementation contract to use for curation tokens + */ function setCurationTokenMaster(address _curationTokenMaster) external; // -- Curation -- + /** + * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. + * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal + * @param _tokensIn Amount of Graph Tokens to deposit + * @param _signalOutMin Expected minimum amount of signal to receive + * @return Amount of signal minted + * @return Amount of curation tax burned + */ function mint( bytes32 _subgraphDeploymentID, uint256 _tokensIn, uint256 _signalOutMin ) external returns (uint256, uint256); + /** + * @notice Burn _signal from the SubgraphDeployment curation pool + * @param _subgraphDeploymentID SubgraphDeployment the curator is returning signal + * @param _signalIn Amount of signal to return + * @param _tokensOutMin Expected minimum amount of tokens to receive + * @return Tokens returned + */ function burn( bytes32 _subgraphDeploymentID, uint256 _signalIn, uint256 _tokensOutMin ) external returns (uint256); + /** + * @notice Assign Graph Tokens collected as curation fees to the curation pool reserve. + * @param _subgraphDeploymentID SubgraphDeployment where funds should be allocated as reserves + * @param _tokens Amount of Graph Tokens to add to reserves + */ function collect(bytes32 _subgraphDeploymentID, uint256 _tokens) external; // -- Getters -- + /** + * @notice Check if any GRT tokens are deposited for a SubgraphDeployment. + * @param _subgraphDeploymentID SubgraphDeployment to check if curated + * @return True if curated, false otherwise + */ function isCurated(bytes32 _subgraphDeploymentID) external view returns (bool); + /** + * @notice Get the amount of signal a curator has in a curation pool. + * @param _curator Curator owning the signal tokens + * @param _subgraphDeploymentID Subgraph deployment curation pool + * @return Amount of signal owned by a curator for the subgraph deployment + */ function getCuratorSignal(address _curator, bytes32 _subgraphDeploymentID) external view returns (uint256); + /** + * @notice Get the amount of signal in a curation pool. + * @param _subgraphDeploymentID Subgraph deployment curation poool + * @return Amount of signal minted for the subgraph deployment + */ function getCurationPoolSignal(bytes32 _subgraphDeploymentID) external view returns (uint256); + /** + * @notice Get the amount of token reserves in a curation pool. + * @param _subgraphDeploymentID Subgraph deployment curation poool + * @return Amount of token reserves in the curation pool + */ function getCurationPoolTokens(bytes32 _subgraphDeploymentID) external view returns (uint256); + /** + * @notice Calculate amount of signal that can be bought with tokens in a curation pool. + * This function considers and excludes the deposit tax. + * @param _subgraphDeploymentID Subgraph deployment to mint signal + * @param _tokensIn Amount of tokens used to mint signal + * @return Amount of signal that can be bought + * @return Amount of tokens that will be burned as curation tax + */ function tokensToSignal(bytes32 _subgraphDeploymentID, uint256 _tokensIn) external view returns (uint256, uint256); + /** + * @notice Calculate number of tokens to get when burning signal from a curation pool. + * @param _subgraphDeploymentID Subgraph deployment to burn signal + * @param _signalIn Amount of signal to burn + * @return Amount of tokens to get for the specified amount of signal + */ function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signalIn) external view returns (uint256); + /** + * @notice Tax charged when curators deposit funds. + * Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + * @return Curation tax percentage expressed in PPM + */ function curationTaxPercentage() external view returns (uint32); } diff --git a/contracts/discovery/GNS.sol b/contracts/discovery/GNS.sol index fc8802df2..9af60ba2f 100644 --- a/contracts/discovery/GNS.sol +++ b/contracts/discovery/GNS.sol @@ -3,16 +3,18 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; +import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import "../base/Multicall.sol"; -import "../bancor/BancorFormula.sol"; -import "../upgrades/GraphUpgradeable.sol"; -import "../utils/TokenUtils.sol"; +import { Multicall } from "../base/Multicall.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; +import { TokenUtils } from "../utils/TokenUtils.sol"; +import { ICuration } from "../curation/ICuration.sol"; +import { Managed } from "../governance/Managed.sol"; +import { ISubgraphNFT } from "./ISubgraphNFT.sol"; -import "./IGNS.sol"; -import "./GNSStorage.sol"; +import { IGNS } from "./IGNS.sol"; +import { GNSV3Storage } from "./GNSStorage.sol"; /** * @title GNS @@ -23,21 +25,20 @@ import "./GNSStorage.sol"; * The contract implements a multicall behaviour to support batching multiple calls in a single * transaction. */ -contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { - using SafeMath for uint256; +abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { + using SafeMathUpgradeable for uint256; // -- Constants -- - uint256 private constant MAX_UINT256 = 2**256 - 1; - - // 100% in parts per million + /// @dev 100% in parts per million uint32 private constant MAX_PPM = 1000000; - // Equates to Connector weight on bancor formula to be CW = 1 - uint32 private constant defaultReserveRatio = 1000000; + /// @dev Equates to Connector weight on bancor formula to be CW = 1 + uint32 internal immutable fixedReserveRatio = MAX_PPM; // -- Events -- + /// @dev Emitted when the subgraph NFT contract is updated event SubgraphNFTUpdated(address subgraphNFT); /** @@ -132,6 +133,11 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { uint256 withdrawnGRT ); + /** + * @dev Emitted when the counterpart (L1/L2) GNS address is updated + */ + event CounterpartGNSAddressUpdated(address _counterpart); + // -- Modifiers -- /** @@ -150,34 +156,29 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // -- Functions -- /** - * @dev Initialize this contract. + * @notice Initialize the GNS contract. + * @param _controller Address of the Controller contract that manages this contract + * @param _subgraphNFT Address of the Subgraph NFT contract */ - function initialize( - address _controller, - address _bondingCurve, - address _subgraphNFT - ) external onlyImpl { + function initialize(address _controller, address _subgraphNFT) external onlyImpl initializer { Managed._initialize(_controller); - // Dependencies - bondingCurve = _bondingCurve; - // Settings _setOwnerTaxPercentage(500000); _setSubgraphNFT(_subgraphNFT); } /** - * @dev Approve curation contract to pull funds. + * @notice Approve curation contract to pull funds. */ function approveAll() external override { - graphToken().approve(address(curation()), MAX_UINT256); + graphToken().approve(address(curation()), type(uint256).max); } // -- Config -- /** - * @dev Set the owner fee percentage. This is used to prevent a subgraph owner to drain all + * @notice Set the owner fee percentage. This is used to prevent a subgraph owner to drain all * the name curators tokens while upgrading or deprecating and is configurable in parts per million. * @param _ownerTaxPercentage Owner tax percentage */ @@ -186,43 +187,29 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Internal: Set the owner tax percentage. This is used to prevent a subgraph owner to drain all - * the name curators tokens while upgrading or deprecating and is configurable in parts per million. - * @param _ownerTaxPercentage Owner tax percentage - */ - function _setOwnerTaxPercentage(uint32 _ownerTaxPercentage) private { - require(_ownerTaxPercentage <= MAX_PPM, "Owner tax must be MAX_PPM or less"); - ownerTaxPercentage = _ownerTaxPercentage; - emit ParameterUpdated("ownerTaxPercentage"); - } - - /** - * @dev Set the NFT registry contract + * @notice Set the NFT registry contract * NOTE: Calling this function will break the ownership model unless * it is replaced with a fully migrated version of the NFT contract state * Use with care. * @param _subgraphNFT Address of the ERC721 contract */ - function setSubgraphNFT(address _subgraphNFT) public onlyGovernor { + function setSubgraphNFT(address _subgraphNFT) external onlyGovernor { _setSubgraphNFT(_subgraphNFT); } /** - * @dev Internal: Set the NFT registry contract - * @param _subgraphNFT Address of the ERC721 contract + * @notice Set the counterpart (L1/L2) GNS address + * @param _counterpart Owner tax percentage */ - function _setSubgraphNFT(address _subgraphNFT) private { - require(_subgraphNFT != address(0), "NFT address cant be zero"); - require(Address.isContract(_subgraphNFT), "NFT must be valid"); - - subgraphNFT = ISubgraphNFT(_subgraphNFT); - emit SubgraphNFTUpdated(_subgraphNFT); + function setCounterpartGNSAddress(address _counterpart) external onlyGovernor { + counterpartGNSAddress = _counterpart; + emit CounterpartGNSAddressUpdated(_counterpart); } // -- Actions -- /** - * @dev Allows a graph account to set a default name + * @notice Allows a graph account to set a default name * @param _graphAccount Account that is setting its name * @param _nameSystem Name system account already has ownership of a name in * @param _nameIdentifier The unique identifier that is used to identify the name in the system @@ -239,12 +226,12 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Allows a subgraph owner to update the metadata of a subgraph they have published + * @notice Allows a subgraph owner to update the metadata of a subgraph they have published * @param _subgraphID Subgraph ID * @param _subgraphMetadata IPFS hash for the subgraph metadata */ function updateSubgraphMetadata(uint256 _subgraphID, bytes32 _subgraphMetadata) - public + external override onlySubgraphAuth(_subgraphID) { @@ -252,7 +239,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Publish a new subgraph. + * @notice Publish a new subgraph. * @param _subgraphDeploymentID Subgraph deployment for the subgraph * @param _versionMetadata IPFS hash for the subgraph version metadata * @param _subgraphMetadata IPFS hash for the subgraph metadata @@ -270,12 +257,12 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { uint256 subgraphID = _nextSubgraphID(subgraphOwner); SubgraphData storage subgraphData = _getSubgraphData(subgraphID); subgraphData.subgraphDeploymentID = _subgraphDeploymentID; - subgraphData.reserveRatio = defaultReserveRatio; + subgraphData.reserveRatioDeprecated = fixedReserveRatio; // Mint the NFT. Use the subgraphID as tokenID. // This function will check the if tokenID already exists. _mintNFT(subgraphOwner, subgraphID); - emit SubgraphPublished(subgraphID, _subgraphDeploymentID, defaultReserveRatio); + emit SubgraphPublished(subgraphID, _subgraphDeploymentID, fixedReserveRatio); // Set the token metadata _setSubgraphMetadata(subgraphID, _subgraphMetadata); @@ -284,7 +271,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Publish a new version of an existing subgraph. + * @notice Publish a new version of an existing subgraph. * @param _subgraphID Subgraph ID * @param _subgraphDeploymentID Subgraph deployment ID of the new version * @param _versionMetadata IPFS hash for the subgraph version metadata @@ -293,7 +280,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { uint256 _subgraphID, bytes32 _subgraphDeploymentID, bytes32 _versionMetadata - ) external override notPaused onlySubgraphAuth(_subgraphID) { + ) external virtual override notPaused onlySubgraphAuth(_subgraphID) { // Perform the upgrade from the current subgraph deployment to the new one. // This involves burning all signal from the old deployment and using the funds to buy // from the new deployment. @@ -321,7 +308,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // Move all signal from previous version to new version // NOTE: We will only do this as long as there is signal on the subgraph - if (subgraphData.nSignal > 0) { + if (subgraphData.nSignal != 0) { // Burn all version signal in the name pool for tokens (w/no slippage protection) // Sell all signal from the old deployment uint256 tokens = curation.burn( @@ -358,7 +345,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Deprecate a subgraph. The bonding curve is destroyed, the vSignal is burned, and the GNS + * @notice Deprecate a subgraph. The bonding curve is destroyed, the vSignal is burned, and the GNS * contract holds the GRT from burning the vSignal, which all curators can withdraw manually. * Can only be done by the subgraph owner. * @param _subgraphID Subgraph ID @@ -373,7 +360,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { SubgraphData storage subgraphData = _getSubgraphOrRevert(_subgraphID); // Burn signal only if it has any available - if (subgraphData.nSignal > 0) { + if (subgraphData.nSignal != 0) { subgraphData.withdrawableGRT = curation().burn( subgraphData.subgraphDeploymentID, subgraphData.vSignal, @@ -384,7 +371,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // Deprecate the subgraph and do cleanup subgraphData.disabled = true; subgraphData.vSignal = 0; - subgraphData.reserveRatio = 0; + subgraphData.reserveRatioDeprecated = 0; // NOTE: We don't reset the following variable as we use it to test if the Subgraph was ever created // subgraphData.subgraphDeploymentID = 0; @@ -395,7 +382,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Deposit GRT into a subgraph and mint signal. + * @notice Deposit GRT into a subgraph and mint signal. * @param _subgraphID Subgraph ID * @param _tokensIn The amount of tokens the nameCurator wants to deposit * @param _nSignalOutMin Expected minimum amount of name signal to receive @@ -428,7 +415,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Burn signal for a subgraph and return the GRT. + * @notice Burn signal for a subgraph and return the GRT. * @param _subgraphID Subgraph ID * @param _nSignal The amount of nSignal the nameCurator wants to burn * @param _tokensOutMin Expected minimum amount of tokens to receive @@ -465,7 +452,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Move subgraph signal from sender to `_recipient` + * @notice Move subgraph signal from sender to `_recipient` * @param _subgraphID Subgraph ID * @param _recipient Address to send the signal to * @param _amount The amount of nSignal to transfer @@ -495,7 +482,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Withdraw tokens from a deprecated subgraph. + * @notice Withdraw tokens from a deprecated subgraph. * When the subgraph is deprecated, any curator can call this function and * withdraw the GRT they are entitled for its original deposit * @param _subgraphID Subgraph ID @@ -504,12 +491,12 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // Subgraph validations SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); require(subgraphData.disabled == true, "GNS: Must be disabled first"); - require(subgraphData.withdrawableGRT > 0, "GNS: No more GRT to withdraw"); + require(subgraphData.withdrawableGRT != 0, "GNS: No more GRT to withdraw"); // Curator validations address curator = msg.sender; uint256 curatorNSignal = subgraphData.curatorNSignal[curator]; - require(curatorNSignal > 0, "GNS: No signal to withdraw GRT"); + require(curatorNSignal != 0, "GNS: No signal to withdraw GRT"); // Get curator share of tokens to be withdrawn uint256 tokensOut = curatorNSignal.mul(subgraphData.withdrawableGRT).div( @@ -526,49 +513,81 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Calculate tax that owner will have to cover for upgrading or deprecating. - * @param _tokens Tokens that were received from deprecating the old subgraph - * @param _owner Subgraph owner - * @param _curationTaxPercentage Tax percentage on curation deposits from Curation contract - * @return Total tokens that will be sent to curation, _tokens + ownerTax + * @notice Create subgraphID for legacy subgraph and mint ownership NFT. + * @param _graphAccount Account that created the subgraph + * @param _subgraphNumber The sequence number of the created subgraph + * @param _subgraphMetadata IPFS hash for the subgraph metadata */ - function _chargeOwnerTax( - uint256 _tokens, - address _owner, - uint32 _curationTaxPercentage - ) private returns (uint256) { - if (_curationTaxPercentage == 0 || ownerTaxPercentage == 0) { - return 0; - } + function migrateLegacySubgraph( + address _graphAccount, + uint256 _subgraphNumber, + bytes32 _subgraphMetadata + ) external { + // Must be an existing legacy subgraph + bool legacySubgraphExists = legacySubgraphData[_graphAccount][_subgraphNumber] + .subgraphDeploymentID != 0; + require(legacySubgraphExists == true, "GNS: Subgraph does not exist"); - // Tax on the total bonding curve funds - uint256 taxOnOriginal = _tokens.mul(_curationTaxPercentage).div(MAX_PPM); - // Total after the tax - uint256 totalWithoutOwnerTax = _tokens.sub(taxOnOriginal); - // The portion of tax that the owner will pay - uint256 ownerTax = taxOnOriginal.mul(ownerTaxPercentage).div(MAX_PPM); + // Must not be a claimed subgraph + uint256 subgraphID = _buildLegacySubgraphID(_graphAccount, _subgraphNumber); + require( + legacySubgraphKeys[subgraphID].account == address(0), + "GNS: Subgraph was already claimed" + ); - uint256 totalWithOwnerTax = totalWithoutOwnerTax.add(ownerTax); + // Store a reference for a legacy subgraph + legacySubgraphKeys[subgraphID] = IGNS.LegacySubgraphKey({ + account: _graphAccount, + accountSeqID: _subgraphNumber + }); - // The total after tax, plus owner partial repay, divided by - // the tax, to adjust it slightly upwards. ex: - // 100 GRT, 5 GRT Tax, owner pays 100% --> 5 GRT - // To get 100 in the protocol after tax, Owner deposits - // ~5.26, as ~105.26 * .95 = 100 - uint256 totalAdjustedUp = totalWithOwnerTax.mul(MAX_PPM).div( - uint256(MAX_PPM).sub(uint256(_curationTaxPercentage)) - ); + // Delete state for legacy subgraph + legacySubgraphs[_graphAccount][_subgraphNumber] = 0; - uint256 ownerTaxAdjustedUp = totalAdjustedUp.sub(_tokens); + // Mint the NFT and send to owner + // The subgraph owner is the graph account that created it + _mintNFT(_graphAccount, subgraphID); + emit LegacySubgraphClaimed(_graphAccount, _subgraphNumber); - // Get the owner of the subgraph to reimburse the curation tax - TokenUtils.pullTokens(graphToken(), _owner, ownerTaxAdjustedUp); + // Set the token metadata + _setSubgraphMetadata(subgraphID, _subgraphMetadata); + } - return totalAdjustedUp; + /** + * @notice Return the total signal on the subgraph. + * @param _subgraphID Subgraph ID + * @return Total signal on the subgraph + */ + function subgraphSignal(uint256 _subgraphID) external view override returns (uint256) { + return _getSubgraphData(_subgraphID).nSignal; } /** - * @dev Calculate subgraph signal to be returned for an amount of tokens. + * @notice Return the total tokens on the subgraph at current value. + * @param _subgraphID Subgraph ID + * @return Total tokens on the subgraph + */ + function subgraphTokens(uint256 _subgraphID) external view override returns (uint256) { + uint256 signal = _getSubgraphData(_subgraphID).nSignal; + if (signal != 0) { + (, uint256 tokens) = nSignalToTokens(_subgraphID, signal); + return tokens; + } + return 0; + } + + /** + * @notice Return whether a subgraph is a legacy subgraph (created before subgraph NFTs). + * @param _subgraphID Subgraph ID + * @return Return true if subgraph is a legacy subgraph + */ + function isLegacySubgraph(uint256 _subgraphID) external view override returns (bool) { + (address account, ) = getLegacySubgraphKey(_subgraphID); + return account != address(0); + } + + /** + * @notice Calculate subgraph signal to be returned for an amount of tokens. * @param _subgraphID Subgraph ID * @param _tokensIn Tokens being exchanged for subgraph signal * @return Amount of subgraph signal and curation tax @@ -593,7 +612,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Calculate tokens returned for an amount of subgraph signal. + * @notice Calculate tokens returned for an amount of subgraph signal. * @param _subgraphID Subgraph ID * @param _nSignalIn Subgraph signal being exchanged for tokens * @return Amount of tokens returned for an amount of subgraph signal @@ -613,7 +632,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Calculate subgraph signal to be returned for an amount of subgraph deployment signal. + * @notice Calculate subgraph signal to be returned for an amount of subgraph deployment signal. * @param _subgraphID Subgraph ID * @param _vSignalIn Amount of subgraph deployment signal to exchange for subgraph signal * @return Amount of subgraph signal that can be bought @@ -631,17 +650,11 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { return _vSignalIn; } - return - BancorFormula(bondingCurve).calculatePurchaseReturn( - subgraphData.nSignal, - subgraphData.vSignal, - subgraphData.reserveRatio, - _vSignalIn - ); + return subgraphData.nSignal.mul(_vSignalIn).div(subgraphData.vSignal); } /** - * @dev Calculate subgraph deployment signal to be returned for an amount of subgraph signal. + * @notice Calculate subgraph deployment signal to be returned for an amount of subgraph signal. * @param _subgraphID Subgraph ID * @param _nSignalIn Subgraph signal being exchanged for subgraph deployment signal * @return Amount of subgraph deployment signal that can be returned @@ -653,17 +666,11 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { returns (uint256) { SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); - return - BancorFormula(bondingCurve).calculateSaleReturn( - subgraphData.nSignal, - subgraphData.vSignal, - subgraphData.reserveRatio, - _nSignalIn - ); + return subgraphData.vSignal.mul(_nSignalIn).div(subgraphData.nSignal); } /** - * @dev Get the amount of subgraph signal a curator has. + * @notice Get the amount of subgraph signal a curator has. * @param _subgraphID Subgraph ID * @param _curator Curator address * @return Amount of subgraph signal owned by a curator @@ -678,85 +685,80 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Return the total signal on the subgraph. + * @notice Return whether a subgraph is published. * @param _subgraphID Subgraph ID - * @return Total signal on the subgraph + * @return Return true if subgraph is currently published */ - function subgraphSignal(uint256 _subgraphID) external view override returns (uint256) { - return _getSubgraphData(_subgraphID).nSignal; + function isPublished(uint256 _subgraphID) public view override returns (bool) { + return _isPublished(_getSubgraphData(_subgraphID)); } /** - * @dev Return the total tokens on the subgraph at current value. + * @notice Returns account and sequence ID for a legacy subgraph (created before subgraph NFTs). * @param _subgraphID Subgraph ID - * @return Total tokens on the subgraph + * @return account Account that created the subgraph (or 0 if it's not a legacy subgraph) + * @return seqID Sequence number for the subgraph */ - function subgraphTokens(uint256 _subgraphID) external view override returns (uint256) { - uint256 signal = _getSubgraphData(_subgraphID).nSignal; - if (signal > 0) { - (, uint256 tokens) = nSignalToTokens(_subgraphID, signal); - return tokens; - } - return 0; + function getLegacySubgraphKey(uint256 _subgraphID) + public + view + override + returns (address account, uint256 seqID) + { + LegacySubgraphKey storage legacySubgraphKey = legacySubgraphKeys[_subgraphID]; + account = legacySubgraphKey.account; + seqID = legacySubgraphKey.accountSeqID; } /** - * @dev Create subgraphID for legacy subgraph and mint ownership NFT. - * @param _graphAccount Account that created the subgraph - * @param _subgraphNumber The sequence number of the created subgraph - * @param _subgraphMetadata IPFS hash for the subgraph metadata + * @notice Return the owner of a subgraph. + * @param _tokenID Subgraph ID + * @return Owner address */ - function migrateLegacySubgraph( - address _graphAccount, - uint256 _subgraphNumber, - bytes32 _subgraphMetadata - ) external { - // Must be an existing legacy subgraph - bool legacySubgraphExists = legacySubgraphData[_graphAccount][_subgraphNumber] - .subgraphDeploymentID != 0; - require(legacySubgraphExists == true, "GNS: Subgraph does not exist"); + function ownerOf(uint256 _tokenID) public view override returns (address) { + return subgraphNFT.ownerOf(_tokenID); + } - // Must not be a claimed subgraph - uint256 subgraphID = _buildSubgraphID(_graphAccount, _subgraphNumber); - require( - legacySubgraphKeys[subgraphID].account == address(0), - "GNS: Subgraph was already claimed" - ); + /** + * @dev Calculate tax that owner will have to cover for upgrading or deprecating. + * @param _tokens Tokens that were received from deprecating the old subgraph + * @param _owner Subgraph owner + * @param _curationTaxPercentage Tax percentage on curation deposits from Curation contract + * @return Total tokens that will be sent to curation, _tokens + ownerTax + */ + function _chargeOwnerTax( + uint256 _tokens, + address _owner, + uint32 _curationTaxPercentage + ) internal returns (uint256) { + if (_curationTaxPercentage == 0 || ownerTaxPercentage == 0) { + return 0; + } - // Store a reference for a legacy subgraph - legacySubgraphKeys[subgraphID] = IGNS.LegacySubgraphKey({ - account: _graphAccount, - accountSeqID: _subgraphNumber - }); + // Tax on the total bonding curve funds + uint256 taxOnOriginal = _tokens.mul(_curationTaxPercentage).div(MAX_PPM); + // Total after the tax + uint256 totalWithoutOwnerTax = _tokens.sub(taxOnOriginal); + // The portion of tax that the owner will pay + uint256 ownerTax = taxOnOriginal.mul(ownerTaxPercentage).div(MAX_PPM); - // Delete state for legacy subgraph - legacySubgraphs[_graphAccount][_subgraphNumber] = 0; + uint256 totalWithOwnerTax = totalWithoutOwnerTax.add(ownerTax); - // Mint the NFT and send to owner - // The subgraph owner is the graph account that created it - _mintNFT(_graphAccount, subgraphID); - emit LegacySubgraphClaimed(_graphAccount, _subgraphNumber); + // The total after tax, plus owner partial repay, divided by + // the tax, to adjust it slightly upwards. ex: + // 100 GRT, 5 GRT Tax, owner pays 100% --> 5 GRT + // To get 100 in the protocol after tax, Owner deposits + // ~5.26, as ~105.26 * .95 = 100 + uint256 totalAdjustedUp = totalWithOwnerTax.mul(MAX_PPM).div( + uint256(MAX_PPM).sub(uint256(_curationTaxPercentage)) + ); - // Set the token metadata - _setSubgraphMetadata(subgraphID, _subgraphMetadata); - } + uint256 ownerTaxAdjustedUp = totalAdjustedUp.sub(_tokens); - /** - * @dev Return whether a subgraph is published. - * @param _subgraphID Subgraph ID - * @return Return true if subgraph is currently published - */ - function isPublished(uint256 _subgraphID) public view override returns (bool) { - return _isPublished(_getSubgraphData(_subgraphID)); - } + // Get the owner of the subgraph to reimburse the curation tax + TokenUtils.pullTokens(graphToken(), _owner, ownerTaxAdjustedUp); - /** - * @dev Build a subgraph ID based on the account creating it and a sequence number for that account. - * Subgraph ID is the keccak hash of account+seqID - * @return Subgraph ID - */ - function _buildSubgraphID(address _account, uint256 _seqID) internal pure returns (uint256) { - return uint256(keccak256(abi.encodePacked(_account, _seqID))); + return totalAdjustedUp; } /** @@ -779,13 +781,48 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { return seqID; } + /** + * @dev Mint the NFT for the subgraph. + * @param _owner Owner address + * @param _tokenID Subgraph ID + */ + function _mintNFT(address _owner, uint256 _tokenID) internal { + subgraphNFT.mint(_owner, _tokenID); + } + + /** + * @dev Burn the NFT for the subgraph. + * @param _tokenID Subgraph ID + */ + function _burnNFT(uint256 _tokenID) internal { + subgraphNFT.burn(_tokenID); + } + + /** + * @dev Set the subgraph metadata. + * @param _tokenID Subgraph ID + * @param _subgraphMetadata IPFS hash of the subgraph metadata + */ + function _setSubgraphMetadata(uint256 _tokenID, bytes32 _subgraphMetadata) internal { + subgraphNFT.setSubgraphMetadata(_tokenID, _subgraphMetadata); + + // Even if the following event is emitted in the NFT we emit it here to facilitate + // subgraph indexing + emit SubgraphMetadataUpdated(_tokenID, _subgraphMetadata); + } + /** * @dev Get subgraph data. * This function will first look for a v1 subgraph and return it if found. * @param _subgraphID Subgraph ID * @return Subgraph Data */ - function _getSubgraphData(uint256 _subgraphID) private view returns (SubgraphData storage) { + function _getSubgraphData(uint256 _subgraphID) + internal + view + virtual + returns (SubgraphData storage) + { // If there is a legacy subgraph created return it LegacySubgraphKey storage legacySubgraphKey = legacySubgraphKeys[_subgraphID]; if (legacySubgraphKey.account != address(0)) { @@ -819,44 +856,55 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { return subgraphData; } - // -- NFT -- - /** - * @dev Return the owner of a subgraph. - * @param _tokenID Subgraph ID - * @return Owner address + * @dev Build a subgraph ID based on the account creating it and a sequence number for that account. + * Only used for legacy subgraphs being migrated, as new ones will also use the chainid. + * Subgraph ID is the keccak hash of account+seqID + * @return Subgraph ID */ - function ownerOf(uint256 _tokenID) public view override returns (address) { - return subgraphNFT.ownerOf(_tokenID); + function _buildLegacySubgraphID(address _account, uint256 _seqID) + internal + pure + returns (uint256) + { + return uint256(keccak256(abi.encodePacked(_account, _seqID))); } /** - * @dev Mint the NFT for the subgraph. - * @param _owner Owner address - * @param _tokenID Subgraph ID + * @dev Build a subgraph ID based on the account creating it and a sequence number for that account. + * Subgraph ID is the keccak hash of account+seqID + * @return Subgraph ID */ - function _mintNFT(address _owner, uint256 _tokenID) internal { - subgraphNFT.mint(_owner, _tokenID); + function _buildSubgraphID(address _account, uint256 _seqID) internal pure returns (uint256) { + uint256 chainId; + // Too bad solidity 0.7.6 still doesn't have block.chainid + // solhint-disable-next-line no-inline-assembly + assembly { + chainId := chainid() + } + return uint256(keccak256(abi.encodePacked(_account, _seqID, chainId))); } /** - * @dev Burn the NFT for the subgraph. - * @param _tokenID Subgraph ID + * @dev Internal: Set the owner tax percentage. This is used to prevent a subgraph owner to drain all + * the name curators tokens while upgrading or deprecating and is configurable in parts per million. + * @param _ownerTaxPercentage Owner tax percentage */ - function _burnNFT(uint256 _tokenID) internal { - subgraphNFT.burn(_tokenID); + function _setOwnerTaxPercentage(uint32 _ownerTaxPercentage) private { + require(_ownerTaxPercentage <= MAX_PPM, "Owner tax must be MAX_PPM or less"); + ownerTaxPercentage = _ownerTaxPercentage; + emit ParameterUpdated("ownerTaxPercentage"); } /** - * @dev Set the subgraph metadata. - * @param _tokenID Subgraph ID - * @param _subgraphMetadata IPFS hash of the subgraph metadata + * @dev Internal: Set the NFT registry contract + * @param _subgraphNFT Address of the ERC721 contract */ - function _setSubgraphMetadata(uint256 _tokenID, bytes32 _subgraphMetadata) internal { - subgraphNFT.setSubgraphMetadata(_tokenID, _subgraphMetadata); + function _setSubgraphNFT(address _subgraphNFT) private { + require(_subgraphNFT != address(0), "NFT address cant be zero"); + require(AddressUpgradeable.isContract(_subgraphNFT), "NFT must be valid"); - // Even if the following event is emitted in the NFT we emit it here to facilitate - // subgraph indexing - emit SubgraphMetadataUpdated(_tokenID, _subgraphMetadata); + subgraphNFT = ISubgraphNFT(_subgraphNFT); + emit SubgraphNFTUpdated(_subgraphNFT); } } diff --git a/contracts/discovery/GNSStorage.sol b/contracts/discovery/GNSStorage.sol index 50a480777..b8480c8e6 100644 --- a/contracts/discovery/GNSStorage.sol +++ b/contracts/discovery/GNSStorage.sol @@ -3,49 +3,73 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "../governance/Managed.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; +import { Managed } from "../governance/Managed.sol"; -import "./erc1056/IEthereumDIDRegistry.sol"; -import "./IGNS.sol"; -import "./ISubgraphNFT.sol"; +import { IEthereumDIDRegistry } from "./erc1056/IEthereumDIDRegistry.sol"; +import { IGNS } from "./IGNS.sol"; +import { ISubgraphNFT } from "./ISubgraphNFT.sol"; +/** + * @title GNSV1Storage + * @notice This contract holds all the storage variables for the GNS contract, version 1 + */ abstract contract GNSV1Storage is Managed { // -- State -- - // In parts per hundred + /// Percentage of curation tax that must be paid by the owner, in parts per million. uint32 public ownerTaxPercentage; - // Bonding curve formula - address public bondingCurve; + /// [DEPRECATED] Bonding curve formula. + address public __DEPRECATED_bondingCurve; // solhint-disable-line var-name-mixedcase - // Stores what subgraph deployment a particular legacy subgraph targets - // A subgraph is defined by (graphAccountID, subgraphNumber) - // A subgraph can target one subgraph deployment (bytes32 hash) - // (graphAccountID, subgraphNumber) => subgraphDeploymentID + /// @dev Stores what subgraph deployment a particular legacy subgraph targets. + /// A subgraph is defined by (graphAccountID, subgraphNumber). + /// A subgraph can target one subgraph deployment (bytes32 hash). + /// (graphAccountID, subgraphNumber) => subgraphDeploymentID mapping(address => mapping(uint256 => bytes32)) internal legacySubgraphs; - // Every time an account creates a subgraph it increases a per-account sequence ID - // account => seqID + /// Every time an account creates a subgraph it increases a per-account sequence ID. + /// account => seqID mapping(address => uint256) public nextAccountSeqID; - // Stores all the signal deposited on a legacy subgraph - // (graphAccountID, subgraphNumber) => SubgraphData + /// Stores all the signal deposited on a legacy subgraph. + /// (graphAccountID, subgraphNumber) => SubgraphData mapping(address => mapping(uint256 => IGNS.SubgraphData)) public legacySubgraphData; - // [DEPRECATED] ERC-1056 contract reference - // This contract is used for managing identities - IEthereumDIDRegistry private __DEPRECATED_erc1056Registry; + /// @dev [DEPRECATED] ERC-1056 contract reference. + /// This contract was used for managing identities. + IEthereumDIDRegistry private __DEPRECATED_erc1056Registry; // solhint-disable-line var-name-mixedcase } +/** + * @title GNSV2Storage + * @notice This contract holds all the storage variables for the GNS contract, version 2 + */ abstract contract GNSV2Storage is GNSV1Storage { - // Use it whenever a legacy (v1) subgraph NFT was claimed to maintain compatibility - // Keep a reference from subgraphID => (graphAccount, subgraphNumber) + /// Stores the account and seqID for a legacy subgraph that has been migrated. + /// Use it whenever a legacy (v1) subgraph NFT was claimed to maintain compatibility. + /// Keep a reference from subgraphID => (graphAccount, subgraphNumber) mapping(uint256 => IGNS.LegacySubgraphKey) public legacySubgraphKeys; - // Store data for all NFT-based (v2) subgraphs - // subgraphID => SubgraphData + /// Store data for all NFT-based (v2) subgraphs. + /// subgraphID => SubgraphData mapping(uint256 => IGNS.SubgraphData) public subgraphs; - // Contract that represents subgraph ownership through an NFT + /// Contract that represents subgraph ownership through an NFT ISubgraphNFT public subgraphNFT; } + +/** + * @title GNSV3Storage + * @notice This contract holds all the storage variables for the base GNS contract, version 3. + * @dev Note that this is the first version that includes a storage gap - if adding + * future versions, make sure to move the gap to the new version and + * reduce the size of the gap accordingly. + */ +abstract contract GNSV3Storage is GNSV2Storage, Initializable { + /// Address of the counterpart GNS contract (L1GNS/L2GNS) + address public counterpartGNSAddress; + /// @dev Gap to allow adding variables in future upgrades (since L1GNS and L2GNS have their own storage as well) + uint256[50] private __gap; +} diff --git a/contracts/discovery/IGNS.sol b/contracts/discovery/IGNS.sol index 13efa1b9d..80cc99820 100644 --- a/contracts/discovery/IGNS.sol +++ b/contracts/discovery/IGNS.sol @@ -2,19 +2,31 @@ pragma solidity ^0.7.6; +/** + * @title Interface for GNS + */ interface IGNS { // -- Pool -- + /** + * @dev The SubgraphData struct holds information about subgraphs + * and their signal; both nSignal (i.e. name signal at the GNS level) + * and vSignal (i.e. version signal at the Curation contract level) + */ struct SubgraphData { uint256 vSignal; // The token of the subgraph-deployment bonding curve uint256 nSignal; // The token of the subgraph bonding curve mapping(address => uint256) curatorNSignal; bytes32 subgraphDeploymentID; - uint32 reserveRatio; + uint32 reserveRatioDeprecated; // Ratio for the bonding curve, always 1 in PPM, deprecated. bool disabled; uint256 withdrawableGRT; } + /** + * @dev The LegacySubgraphKey struct holds the account and sequence ID + * used to generate subgraph IDs in legacy subgraphs. + */ struct LegacySubgraphKey { address account; uint256 accountSeqID; @@ -22,12 +34,27 @@ interface IGNS { // -- Configuration -- + /** + * @notice Approve curation contract to pull funds. + */ function approveAll() external; + /** + * @notice Set the owner fee percentage. This is used to prevent a subgraph owner to drain all + * the name curators tokens while upgrading or deprecating and is configurable in parts per million. + * @param _ownerTaxPercentage Owner tax percentage + */ function setOwnerTaxPercentage(uint32 _ownerTaxPercentage) external; // -- Publishing -- + /** + * @notice Allows a graph account to set a default name + * @param _graphAccount Account that is setting its name + * @param _nameSystem Name system account already has ownership of a name in + * @param _nameIdentifier The unique identifier that is used to identify the name in the system + * @param _name The name being set as default + */ function setDefaultName( address _graphAccount, uint8 _nameSystem, @@ -35,52 +62,120 @@ interface IGNS { string calldata _name ) external; + /** + * @notice Allows a subgraph owner to update the metadata of a subgraph they have published + * @param _subgraphID Subgraph ID + * @param _subgraphMetadata IPFS hash for the subgraph metadata + */ function updateSubgraphMetadata(uint256 _subgraphID, bytes32 _subgraphMetadata) external; + /** + * @notice Publish a new subgraph. + * @param _subgraphDeploymentID Subgraph deployment for the subgraph + * @param _versionMetadata IPFS hash for the subgraph version metadata + * @param _subgraphMetadata IPFS hash for the subgraph metadata + */ function publishNewSubgraph( bytes32 _subgraphDeploymentID, bytes32 _versionMetadata, bytes32 _subgraphMetadata ) external; + /** + * @notice Publish a new version of an existing subgraph. + * @param _subgraphID Subgraph ID + * @param _subgraphDeploymentID Subgraph deployment ID of the new version + * @param _versionMetadata IPFS hash for the subgraph version metadata + */ function publishNewVersion( uint256 _subgraphID, bytes32 _subgraphDeploymentID, bytes32 _versionMetadata ) external; + /** + * @notice Deprecate a subgraph. The bonding curve is destroyed, the vSignal is burned, and the GNS + * contract holds the GRT from burning the vSignal, which all curators can withdraw manually. + * Can only be done by the subgraph owner. + * @param _subgraphID Subgraph ID + */ function deprecateSubgraph(uint256 _subgraphID) external; // -- Curation -- + /** + * @notice Deposit GRT into a subgraph and mint signal. + * @param _subgraphID Subgraph ID + * @param _tokensIn The amount of tokens the nameCurator wants to deposit + * @param _nSignalOutMin Expected minimum amount of name signal to receive + */ function mintSignal( uint256 _subgraphID, uint256 _tokensIn, uint256 _nSignalOutMin ) external; + /** + * @notice Burn signal for a subgraph and return the GRT. + * @param _subgraphID Subgraph ID + * @param _nSignal The amount of nSignal the nameCurator wants to burn + * @param _tokensOutMin Expected minimum amount of tokens to receive + */ function burnSignal( uint256 _subgraphID, uint256 _nSignal, uint256 _tokensOutMin ) external; + /** + * @notice Move subgraph signal from sender to `_recipient` + * @param _subgraphID Subgraph ID + * @param _recipient Address to send the signal to + * @param _amount The amount of nSignal to transfer + */ function transferSignal( uint256 _subgraphID, address _recipient, uint256 _amount ) external; + /** + * @notice Withdraw tokens from a deprecated subgraph. + * When the subgraph is deprecated, any curator can call this function and + * withdraw the GRT they are entitled for its original deposit + * @param _subgraphID Subgraph ID + */ function withdraw(uint256 _subgraphID) external; // -- Getters -- + /** + * @notice Return the owner of a subgraph. + * @param _tokenID Subgraph ID + * @return Owner address + */ function ownerOf(uint256 _tokenID) external view returns (address); + /** + * @notice Return the total signal on the subgraph. + * @param _subgraphID Subgraph ID + * @return Total signal on the subgraph + */ function subgraphSignal(uint256 _subgraphID) external view returns (uint256); + /** + * @notice Return the total tokens on the subgraph at current value. + * @param _subgraphID Subgraph ID + * @return Total tokens on the subgraph + */ function subgraphTokens(uint256 _subgraphID) external view returns (uint256); + /** + * @notice Calculate subgraph signal to be returned for an amount of tokens. + * @param _subgraphID Subgraph ID + * @param _tokensIn Tokens being exchanged for subgraph signal + * @return Amount of subgraph signal and curation tax + */ function tokensToNSignal(uint256 _subgraphID, uint256 _tokensIn) external view @@ -90,25 +185,72 @@ interface IGNS { uint256 ); + /** + * @notice Calculate tokens returned for an amount of subgraph signal. + * @param _subgraphID Subgraph ID + * @param _nSignalIn Subgraph signal being exchanged for tokens + * @return Amount of tokens returned for an amount of subgraph signal + */ function nSignalToTokens(uint256 _subgraphID, uint256 _nSignalIn) external view returns (uint256, uint256); + /** + * @notice Calculate subgraph signal to be returned for an amount of subgraph deployment signal. + * @param _subgraphID Subgraph ID + * @param _vSignalIn Amount of subgraph deployment signal to exchange for subgraph signal + * @return Amount of subgraph signal that can be bought + */ function vSignalToNSignal(uint256 _subgraphID, uint256 _vSignalIn) external view returns (uint256); + /** + * @notice Calculate subgraph deployment signal to be returned for an amount of subgraph signal. + * @param _subgraphID Subgraph ID + * @param _nSignalIn Subgraph signal being exchanged for subgraph deployment signal + * @return Amount of subgraph deployment signal that can be returned + */ function nSignalToVSignal(uint256 _subgraphID, uint256 _nSignalIn) external view returns (uint256); + /** + * @notice Get the amount of subgraph signal a curator has. + * @param _subgraphID Subgraph ID + * @param _curator Curator address + * @return Amount of subgraph signal owned by a curator + */ function getCuratorSignal(uint256 _subgraphID, address _curator) external view returns (uint256); + /** + * @notice Return whether a subgraph is published. + * @param _subgraphID Subgraph ID + * @return Return true if subgraph is currently published + */ function isPublished(uint256 _subgraphID) external view returns (bool); + + /** + * @notice Return whether a subgraph is a legacy subgraph (created before subgraph NFTs). + * @param _subgraphID Subgraph ID + * @return Return true if subgraph is a legacy subgraph + */ + function isLegacySubgraph(uint256 _subgraphID) external view returns (bool); + + /** + * @notice Returns account and sequence ID for a legacy subgraph (created before subgraph NFTs). + * @param _subgraphID Subgraph ID + * @return account Account that created the subgraph (or 0 if it's not a legacy subgraph) + * @return seqID Sequence number for the subgraph + */ + function getLegacySubgraphKey(uint256 _subgraphID) + external + view + returns (address account, uint256 seqID); } diff --git a/contracts/discovery/L1GNS.sol b/contracts/discovery/L1GNS.sol new file mode 100644 index 000000000..01dce4967 --- /dev/null +++ b/contracts/discovery/L1GNS.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +import { GNS } from "./GNS.sol"; + +import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; +import { IL2GNS } from "../l2/discovery/IL2GNS.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { L1GNSV1Storage } from "./L1GNSStorage.sol"; + +/** + * @title L1GNS + * @dev The Graph Name System contract provides a decentralized naming system for subgraphs + * used in the scope of the Graph Network. It translates Subgraphs into Subgraph Versions. + * Each version is associated with a Subgraph Deployment. The contract has no knowledge of + * human-readable names. All human readable names emitted in events. + * The contract implements a multicall behaviour to support batching multiple calls in a single + * transaction. + * This L1GNS variant includes some functions to allow transferring subgraphs to L2. + */ +contract L1GNS is GNS, L1GNSV1Storage { + using SafeMathUpgradeable for uint256; + + /// @dev Emitted when a subgraph was sent to L2 through the bridge + event SubgraphSentToL2( + uint256 indexed _subgraphID, + address indexed _l1Owner, + address indexed _l2Owner, + uint256 _tokens + ); + + /// @dev Emitted when a curator's balance for a subgraph was sent to L2 + event CuratorBalanceSentToL2( + uint256 indexed _subgraphID, + address indexed _l1Curator, + address indexed _l2Beneficiary, + uint256 _tokens + ); + + /** + * @notice Send a subgraph's data and tokens to L2. + * Use the Arbitrum SDK to estimate the L2 retryable ticket parameters. + * Note that any L2 gas/fee refunds will be lost, so the function only accepts + * the exact amount of ETH to cover _maxSubmissionCost + _maxGas * _gasPriceBid. + * @param _subgraphID Subgraph ID + * @param _l2Owner Address that will own the subgraph in L2 (could be the L1 owner, but could be different if the L1 owner is an L1 contract) + * @param _maxGas Max gas to use for the L2 retryable ticket + * @param _gasPriceBid Gas price bid for the L2 retryable ticket + * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket + */ + function sendSubgraphToL2( + uint256 _subgraphID, + address _l2Owner, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external payable notPartialPaused { + require(!subgraphTransferredToL2[_subgraphID], "ALREADY_DONE"); + require( + msg.value == _maxSubmissionCost.add(_maxGas.mul(_gasPriceBid)), + "INVALID_ETH_VALUE" + ); + + SubgraphData storage subgraphData = _getSubgraphOrRevert(_subgraphID); + // This is just like onlySubgraphAuth, but we want it to run after the subgraphTransferredToL2 check + // to revert with a nicer message in that case: + require(ownerOf(_subgraphID) == msg.sender, "GNS: Must be authorized"); + subgraphTransferredToL2[_subgraphID] = true; + + uint256 curationTokens = curation().burn( + subgraphData.subgraphDeploymentID, + subgraphData.vSignal, + 0 + ); + subgraphData.disabled = true; + subgraphData.vSignal = 0; + + // We send only the subgraph owner's tokens and nsignal to L2, + // and for everyone else we set the withdrawableGRT so that they can choose + // to withdraw or transfer their signal. + uint256 ownerNSignal = subgraphData.curatorNSignal[msg.sender]; + uint256 totalSignal = subgraphData.nSignal; + + // Get owner share of tokens to be sent to L2 + uint256 tokensForL2 = ownerNSignal.mul(curationTokens).div(totalSignal); + // This leaves the subgraph as if it was deprecated, + // so other curators can withdraw: + subgraphData.curatorNSignal[msg.sender] = 0; + subgraphData.nSignal = totalSignal.sub(ownerNSignal); + subgraphData.withdrawableGRT = curationTokens.sub(tokensForL2); + + bytes memory extraData = abi.encode( + uint8(IL2GNS.L1MessageCodes.RECEIVE_SUBGRAPH_CODE), + _subgraphID, + _l2Owner + ); + + _sendTokensAndMessageToL2GNS( + tokensForL2, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + extraData + ); + + subgraphData.reserveRatioDeprecated = 0; + _burnNFT(_subgraphID); + emit SubgraphSentToL2(_subgraphID, msg.sender, _l2Owner, tokensForL2); + } + + /** + * @notice Send the balance for a curator's signal in a subgraph that was + * transferred to L2, using the L1GraphTokenGateway. + * The balance will be claimed for a beneficiary address, as this method can be + * used by curators that use a contract address in L1 that may not exist in L2. + * This will set the curator's signal on L1 to zero, so the caller must ensure + * that the retryable ticket is redeemed before expiration, or the signal will be lost. + * It is up to the caller to verify that the subgraph transfer was finished in L2, + * but if it wasn't, the tokens will be sent to the beneficiary in L2. + * Note that any L2 gas/fee refunds will be lost, so the function only accepts + * the exact amount of ETH to cover _maxSubmissionCost + _maxGas * _gasPriceBid. + * @dev Use the Arbitrum SDK to estimate the L2 retryable ticket parameters. + * @param _subgraphID Subgraph ID + * @param _beneficiary Address that will receive the tokens in L2 + * @param _maxGas Max gas to use for the L2 retryable ticket + * @param _gasPriceBid Gas price bid for the L2 retryable ticket + * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket + */ + function sendCuratorBalanceToBeneficiaryOnL2( + uint256 _subgraphID, + address _beneficiary, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external payable notPartialPaused { + require(subgraphTransferredToL2[_subgraphID], "!TRANSFERRED"); + require( + msg.value == _maxSubmissionCost.add(_maxGas.mul(_gasPriceBid)), + "INVALID_ETH_VALUE" + ); + // The Arbitrum bridge will check this too, we just check here for an early exit + require(_maxSubmissionCost != 0, "NO_SUBMISSION_COST"); + + SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); + uint256 curatorNSignal = subgraphData.curatorNSignal[msg.sender]; + require(curatorNSignal != 0, "NO_SIGNAL"); + uint256 subgraphNSignal = subgraphData.nSignal; + require(subgraphNSignal != 0, "NO_SUBGRAPH_SIGNAL"); + + uint256 withdrawableGRT = subgraphData.withdrawableGRT; + uint256 tokensForL2 = curatorNSignal.mul(withdrawableGRT).div(subgraphNSignal); + bytes memory extraData = abi.encode( + uint8(IL2GNS.L1MessageCodes.RECEIVE_CURATOR_BALANCE_CODE), + _subgraphID, + _beneficiary + ); + + // Set the subgraph as if the curator had withdrawn their tokens + subgraphData.curatorNSignal[msg.sender] = 0; + subgraphData.nSignal = subgraphNSignal.sub(curatorNSignal); + subgraphData.withdrawableGRT = withdrawableGRT.sub(tokensForL2); + + // Send the tokens and data to L2 using the L1GraphTokenGateway + _sendTokensAndMessageToL2GNS( + tokensForL2, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + extraData + ); + emit CuratorBalanceSentToL2(_subgraphID, msg.sender, _beneficiary, tokensForL2); + } + + /** + * @notice Sends a message to the L2GNS with some extra data, + * also sending some tokens, using the L1GraphTokenGateway. + * @param _tokens Amount of tokens to send to L2 + * @param _maxGas Max gas to use for the L2 retryable ticket + * @param _gasPriceBid Gas price bid for the L2 retryable ticket + * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket + * @param _extraData Extra data for the callhook on L2GNS + */ + function _sendTokensAndMessageToL2GNS( + uint256 _tokens, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + bytes memory _extraData + ) internal { + bytes memory data = abi.encode(_maxSubmissionCost, _extraData); + IGraphToken grt = graphToken(); + ITokenGateway gateway = graphTokenGateway(); + grt.approve(address(gateway), _tokens); + gateway.outboundTransfer{ value: msg.value }( + address(grt), + counterpartGNSAddress, + _tokens, + _maxGas, + _gasPriceBid, + data + ); + } +} diff --git a/contracts/discovery/L1GNSStorage.sol b/contracts/discovery/L1GNSStorage.sol new file mode 100644 index 000000000..557814513 --- /dev/null +++ b/contracts/discovery/L1GNSStorage.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +/** + * @title L1GNSV1Storage + * @notice This contract holds all the L1-specific storage variables for the L1GNS contract, version 1 + * @dev When adding new versions, make sure to move the gap to the new version and + * reduce the size of the gap accordingly. + */ +abstract contract L1GNSV1Storage { + /// True for subgraph IDs that have been transferred to L2 + mapping(uint256 => bool) public subgraphTransferredToL2; + /// @dev Storage gap to keep storage slots fixed in future versions + uint256[50] private __gap; +} diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index d946b78a7..cfa71e6a0 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -417,7 +417,7 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @notice Decodes calldata required for migration of tokens + * @notice Decodes calldata required for transfer of tokens to L2 * @dev Data must include maxSubmissionCost, extraData can be left empty. When the router * sends an outbound message, data also contains the from address. * @param _data encoded callhook data diff --git a/contracts/governance/IManaged.sol b/contracts/governance/IManaged.sol index 1a458a460..76f05e0fb 100644 --- a/contracts/governance/IManaged.sol +++ b/contracts/governance/IManaged.sol @@ -2,6 +2,31 @@ pragma solidity ^0.7.6; +import { IController } from "./IController.sol"; + +/** + * @title Managed Interface + * @dev Interface for contracts that can be managed by a controller. + */ interface IManaged { + /** + * @notice Set the controller that manages this contract + * @dev Only the current controller can set a new controller + * @param _controller Address of the new controller + */ function setController(address _controller) external; + + /** + * @notice Sync protocol contract addresses from the Controller registry + * @dev This function will cache all the contracts using the latest addresses. + * Anyone can call the function whenever a Proxy contract change in the + * controller to ensure the protocol is using the latest version. + */ + function syncAllContracts() external; + + /** + * @notice Get the Controller that manages this contract + * @return The Controller as an IController interface + */ + function controller() external view returns (IController); } diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index 6b8fe624e..9b0ea29c8 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -10,6 +10,7 @@ import { IRewardsManager } from "../rewards/IRewardsManager.sol"; import { IStaking } from "../staking/IStaking.sol"; import { IGraphToken } from "../token/IGraphToken.sol"; import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; +import { IGNS } from "../discovery/IGNS.sol"; import { IManaged } from "./IManaged.sol"; @@ -25,8 +26,8 @@ import { IManaged } from "./IManaged.sol"; abstract contract Managed is IManaged { // -- State -- - /// Controller that contract is registered with - IController public controller; + /// Controller that manages this contract + IController public override controller; /// @dev Cache for the addresses of the contracts retrieved from the controller mapping(bytes32 => address) private _addressCache; /// @dev Gap for future storage variables @@ -39,6 +40,7 @@ abstract contract Managed is IManaged { bytes32 private immutable STAKING = keccak256("Staking"); bytes32 private immutable GRAPH_TOKEN = keccak256("GraphToken"); bytes32 private immutable GRAPH_TOKEN_GATEWAY = keccak256("GraphTokenGateway"); + bytes32 private immutable GNS = keccak256("GNS"); // -- Events -- @@ -190,7 +192,15 @@ abstract contract Managed is IManaged { } /** - * @dev Resolve a contract address from the cache or the Controller if not found + * @dev Return GNS (L1 or L2) interface. + * @return Address of the GNS contract registered with Controller, as an IGNS interface. + */ + function gns() internal view returns (IGNS) { + return IGNS(_resolveContract(GNS)); + } + + /** + * @dev Resolve a contract address from the cache or the Controller if not found. * @param _nameHash keccak256 hash of the contract name * @return Address of the contract */ @@ -204,14 +214,13 @@ abstract contract Managed is IManaged { /** * @dev Cache a contract address from the Controller registry. - * @param _name Name of the contract to sync into the cache - */ - function _syncContract(string memory _name) internal { - bytes32 nameHash = keccak256(abi.encodePacked(_name)); - address contractAddress = controller.getContractProxy(nameHash); - if (_addressCache[nameHash] != contractAddress) { - _addressCache[nameHash] = contractAddress; - emit ContractSynced(nameHash, contractAddress); + * @param _nameHash keccak256 hash of the name of the contract to sync into the cache + */ + function _syncContract(bytes32 _nameHash) internal { + address contractAddress = controller.getContractProxy(_nameHash); + if (_addressCache[_nameHash] != contractAddress) { + _addressCache[_nameHash] = contractAddress; + emit ContractSynced(_nameHash, contractAddress); } } @@ -221,12 +230,13 @@ abstract contract Managed is IManaged { * Anyone can call the function whenever a Proxy contract change in the * controller to ensure the protocol is using the latest version */ - function syncAllContracts() external { - _syncContract("Curation"); - _syncContract("EpochManager"); - _syncContract("RewardsManager"); - _syncContract("Staking"); - _syncContract("GraphToken"); - _syncContract("GraphTokenGateway"); + function syncAllContracts() external override { + _syncContract(CURATION); + _syncContract(EPOCH_MANAGER); + _syncContract(REWARDS_MANAGER); + _syncContract(STAKING); + _syncContract(GRAPH_TOKEN); + _syncContract(GRAPH_TOKEN_GATEWAY); + _syncContract(GNS); } } diff --git a/contracts/l2/curation/IL2Curation.sol b/contracts/l2/curation/IL2Curation.sol new file mode 100644 index 000000000..f3d6a0927 --- /dev/null +++ b/contracts/l2/curation/IL2Curation.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @title Interface of the L2 Curation contract. + */ +interface IL2Curation { + /** + * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. + * @dev This function charges no tax and can only be called by GNS in specific scenarios (for now + * only during an L1-L2 transfer). + * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal + * @param _tokensIn Amount of Graph Tokens to deposit + * @return Signal minted + */ + function mintTaxFree(bytes32 _subgraphDeploymentID, uint256 _tokensIn) + external + returns (uint256); + + /** + * @notice Calculate amount of signal that can be bought with tokens in a curation pool, + * without accounting for curation tax. + * @param _subgraphDeploymentID Subgraph deployment for which to mint signal + * @param _tokensIn Amount of tokens used to mint signal + * @return Amount of signal that can be bought + */ + function tokensToSignalNoTax(bytes32 _subgraphDeploymentID, uint256 _tokensIn) + external + view + returns (uint256); +} diff --git a/contracts/l2/curation/L2Curation.sol b/contracts/l2/curation/L2Curation.sol new file mode 100644 index 000000000..6e32f2112 --- /dev/null +++ b/contracts/l2/curation/L2Curation.sol @@ -0,0 +1,532 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import { ClonesUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol"; + +import { GraphUpgradeable } from "../../upgrades/GraphUpgradeable.sol"; +import { TokenUtils } from "../../utils/TokenUtils.sol"; +import { IRewardsManager } from "../../rewards/IRewardsManager.sol"; +import { Managed } from "../../governance/Managed.sol"; +import { IGraphToken } from "../../token/IGraphToken.sol"; +import { CurationV2Storage } from "../../curation/CurationStorage.sol"; +import { IGraphCurationToken } from "../../curation/IGraphCurationToken.sol"; +import { IL2Curation } from "./IL2Curation.sol"; + +/** + * @title L2Curation contract + * @dev Allows curators to signal on subgraph deployments that might be relevant to indexers by + * staking Graph Tokens (GRT). Additionally, curators earn fees from the Query Market related to the + * subgraph deployment they curate. + * A curators deposit goes to a curation pool along with the deposits of other curators, + * only one such pool exists for each subgraph deployment. + * The contract mints Graph Curation Shares (GCS) according to a (flat) bonding curve for each individual + * curation pool where GRT is deposited. + * Holders can burn GCS using this contract to get GRT tokens back according to the + * bonding curve. + */ +contract L2Curation is CurationV2Storage, GraphUpgradeable, IL2Curation { + using SafeMathUpgradeable for uint256; + + /// @dev 100% in parts per million + uint32 private constant MAX_PPM = 1000000; + + /// @dev Amount of signal you get with your minimum token deposit + uint256 private constant SIGNAL_PER_MINIMUM_DEPOSIT = 1; // 1e-18 signal as 18 decimal number + + /// @dev Reserve ratio for all subgraphs set to 100% for a flat bonding curve + uint32 private immutable fixedReserveRatio = MAX_PPM; + + // -- Events -- + + /** + * @dev Emitted when `curator` deposited `tokens` on `subgraphDeploymentID` as curation signal. + * The `curator` receives `signal` amount according to the curation pool bonding curve. + * An amount of `curationTax` will be collected and burned. + */ + event Signalled( + address indexed curator, + bytes32 indexed subgraphDeploymentID, + uint256 tokens, + uint256 signal, + uint256 curationTax + ); + + /** + * @dev Emitted when `curator` burned `signal` for a `subgraphDeploymentID`. + * The curator will receive `tokens` according to the value of the bonding curve. + */ + event Burned( + address indexed curator, + bytes32 indexed subgraphDeploymentID, + uint256 tokens, + uint256 signal + ); + + /** + * @dev Emitted when `tokens` amount were collected for `subgraphDeploymentID` as part of fees + * distributed by an indexer from query fees received from state channels. + */ + event Collected(bytes32 indexed subgraphDeploymentID, uint256 tokens); + + /** + * @dev Modifier for functions that can only be called by the GNS contract + */ + modifier onlyGNS() { + require(msg.sender == address(gns()), "Only the GNS can call this"); + _; + } + + /** + * @notice Initialize the L2Curation contract + * @param _controller Controller contract that manages this contract + * @param _curationTokenMaster Address of the GraphCurationToken master copy + * @param _curationTaxPercentage Percentage of curation tax to be collected + * @param _minimumCurationDeposit Minimum amount of tokens that can be deposited as curation signal + */ + function initialize( + address _controller, + address _curationTokenMaster, + uint32 _curationTaxPercentage, + uint256 _minimumCurationDeposit + ) external onlyImpl initializer { + Managed._initialize(_controller); + + // For backwards compatibility: + defaultReserveRatio = fixedReserveRatio; + emit ParameterUpdated("defaultReserveRatio"); + _setCurationTaxPercentage(_curationTaxPercentage); + _setMinimumCurationDeposit(_minimumCurationDeposit); + _setCurationTokenMaster(_curationTokenMaster); + } + + /** + * @notice Set the default reserve ratio - not implemented in L2 + * @dev We only keep this for compatibility with ICuration + */ + function setDefaultReserveRatio(uint32) external view override onlyGovernor { + revert("Not implemented in L2"); + } + + /** + * @dev Set the minimum deposit amount for curators. + * @notice Update the minimum deposit amount to `_minimumCurationDeposit` + * @param _minimumCurationDeposit Minimum amount of tokens required deposit + */ + function setMinimumCurationDeposit(uint256 _minimumCurationDeposit) + external + override + onlyGovernor + { + _setMinimumCurationDeposit(_minimumCurationDeposit); + } + + /** + * @notice Set the curation tax percentage to charge when a curator deposits GRT tokens. + * @param _percentage Curation tax percentage charged when depositing GRT tokens + */ + function setCurationTaxPercentage(uint32 _percentage) external override onlyGovernor { + _setCurationTaxPercentage(_percentage); + } + + /** + * @notice Set the master copy to use as clones for the curation token. + * @param _curationTokenMaster Address of implementation contract to use for curation tokens + */ + function setCurationTokenMaster(address _curationTokenMaster) external override onlyGovernor { + _setCurationTokenMaster(_curationTokenMaster); + } + + /** + * @notice Assign Graph Tokens collected as curation fees to the curation pool reserve. + * @dev This function can only be called by the Staking contract and will do the bookeeping of + * transferred tokens into this contract. + * @param _subgraphDeploymentID SubgraphDeployment where funds should be allocated as reserves + * @param _tokens Amount of Graph Tokens to add to reserves + */ + function collect(bytes32 _subgraphDeploymentID, uint256 _tokens) external override { + // Only Staking contract is authorized as caller + require(msg.sender == address(staking()), "Caller must be the staking contract"); + + // Must be curated to accept tokens + require( + isCurated(_subgraphDeploymentID), + "Subgraph deployment must be curated to collect fees" + ); + + // Collect new funds into reserve + CurationPool storage curationPool = pools[_subgraphDeploymentID]; + curationPool.tokens = curationPool.tokens.add(_tokens); + + emit Collected(_subgraphDeploymentID, _tokens); + } + + /** + * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. + * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal + * @param _tokensIn Amount of Graph Tokens to deposit + * @param _signalOutMin Expected minimum amount of signal to receive + * @return Signal minted and deposit tax + */ + function mint( + bytes32 _subgraphDeploymentID, + uint256 _tokensIn, + uint256 _signalOutMin + ) external override notPartialPaused returns (uint256, uint256) { + // Need to deposit some funds + require(_tokensIn != 0, "Cannot deposit zero tokens"); + + // Exchange GRT tokens for GCS of the subgraph pool + (uint256 signalOut, uint256 curationTax) = tokensToSignal(_subgraphDeploymentID, _tokensIn); + + // Slippage protection + require(signalOut >= _signalOutMin, "Slippage protection"); + + address curator = msg.sender; + CurationPool storage curationPool = pools[_subgraphDeploymentID]; + + // If it hasn't been curated before then initialize the curve + if (!isCurated(_subgraphDeploymentID)) { + // Note we don't set the reserveRatio to save the gas + // cost, but in the pools() getter we'll inject the value. + + // If no signal token for the pool - create one + if (address(curationPool.gcs) == address(0)) { + // Use a minimal proxy to reduce gas cost + IGraphCurationToken gcs = IGraphCurationToken( + ClonesUpgradeable.clone(curationTokenMaster) + ); + gcs.initialize(address(this)); + curationPool.gcs = gcs; + } + } + + // Trigger update rewards calculation snapshot + _updateRewards(_subgraphDeploymentID); + + // Transfer tokens from the curator to this contract + // Burn the curation tax + // NOTE: This needs to happen after _updateRewards snapshot as that function + // is using balanceOf(curation) + IGraphToken _graphToken = graphToken(); + TokenUtils.pullTokens(_graphToken, curator, _tokensIn); + TokenUtils.burnTokens(_graphToken, curationTax); + + // Update curation pool + curationPool.tokens = curationPool.tokens.add(_tokensIn.sub(curationTax)); + curationPool.gcs.mint(curator, signalOut); + + emit Signalled(curator, _subgraphDeploymentID, _tokensIn, signalOut, curationTax); + + return (signalOut, curationTax); + } + + /** + * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. + * @dev This function charges no tax and can only be called by GNS in specific scenarios (for now + * only during an L1-L2 transfer). + * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal + * @param _tokensIn Amount of Graph Tokens to deposit + * @return Signal minted + */ + function mintTaxFree(bytes32 _subgraphDeploymentID, uint256 _tokensIn) + external + override + notPartialPaused + onlyGNS + returns (uint256) + { + // Need to deposit some funds + require(_tokensIn != 0, "Cannot deposit zero tokens"); + + // Exchange GRT tokens for GCS of the subgraph pool (no tax) + uint256 signalOut = _tokensToSignal(_subgraphDeploymentID, _tokensIn); + + address curator = msg.sender; + CurationPool storage curationPool = pools[_subgraphDeploymentID]; + + // If it hasn't been curated before then initialize the curve + if (!isCurated(_subgraphDeploymentID)) { + // Note we don't set the reserveRatio to save the gas + // cost, but in the pools() getter we'll inject the value. + + // If no signal token for the pool - create one + if (address(curationPool.gcs) == address(0)) { + // Use a minimal proxy to reduce gas cost + IGraphCurationToken gcs = IGraphCurationToken( + ClonesUpgradeable.clone(curationTokenMaster) + ); + gcs.initialize(address(this)); + curationPool.gcs = gcs; + } + } + + // Trigger update rewards calculation snapshot + _updateRewards(_subgraphDeploymentID); + + // Transfer tokens from the curator to this contract + // NOTE: This needs to happen after _updateRewards snapshot as that function + // is using balanceOf(curation) + IGraphToken _graphToken = graphToken(); + TokenUtils.pullTokens(_graphToken, curator, _tokensIn); + + // Update curation pool + curationPool.tokens = curationPool.tokens.add(_tokensIn); + curationPool.gcs.mint(curator, signalOut); + + emit Signalled(curator, _subgraphDeploymentID, _tokensIn, signalOut, 0); + + return signalOut; + } + + /** + * @dev Return an amount of signal to get tokens back. + * @notice Burn _signalIn from the SubgraphDeployment curation pool + * @param _subgraphDeploymentID SubgraphDeployment for which the curator is returning signal + * @param _signalIn Amount of signal to return + * @param _tokensOutMin Expected minimum amount of tokens to receive + * @return Amount of tokens returned to the sender + */ + function burn( + bytes32 _subgraphDeploymentID, + uint256 _signalIn, + uint256 _tokensOutMin + ) external override notPartialPaused returns (uint256) { + address curator = msg.sender; + + // Validations + require(_signalIn != 0, "Cannot burn zero signal"); + require( + getCuratorSignal(curator, _subgraphDeploymentID) >= _signalIn, + "Cannot burn more signal than you own" + ); + + // Get the amount of tokens to refund based on returned signal + uint256 tokensOut = signalToTokens(_subgraphDeploymentID, _signalIn); + + // Slippage protection + require(tokensOut >= _tokensOutMin, "Slippage protection"); + + // Trigger update rewards calculation + _updateRewards(_subgraphDeploymentID); + + // Update curation pool + CurationPool storage curationPool = pools[_subgraphDeploymentID]; + curationPool.tokens = curationPool.tokens.sub(tokensOut); + curationPool.gcs.burnFrom(curator, _signalIn); + + // If all signal burnt delete the curation pool except for the + // curation token contract to avoid recreating it on a new mint + if (getCurationPoolSignal(_subgraphDeploymentID) == 0) { + curationPool.tokens = 0; + } + + // Return the tokens to the curator + TokenUtils.pushTokens(graphToken(), curator, tokensOut); + + emit Burned(curator, _subgraphDeploymentID, tokensOut, _signalIn); + + return tokensOut; + } + + /** + * @notice Get the amount of token reserves in a curation pool. + * @param _subgraphDeploymentID Subgraph deployment curation poool + * @return Amount of token reserves in the curation pool + */ + function getCurationPoolTokens(bytes32 _subgraphDeploymentID) + external + view + override + returns (uint256) + { + return pools[_subgraphDeploymentID].tokens; + } + + /** + * @notice Check if any GRT tokens are deposited for a SubgraphDeployment. + * @param _subgraphDeploymentID SubgraphDeployment to check if curated + * @return True if curated + */ + function isCurated(bytes32 _subgraphDeploymentID) public view override returns (bool) { + return pools[_subgraphDeploymentID].tokens != 0; + } + + /** + * @notice Get the amount of signal a curator has in a curation pool. + * @param _curator Curator owning the signal tokens + * @param _subgraphDeploymentID Subgraph deployment curation pool + * @return Amount of signal owned by a curator for the subgraph deployment + */ + function getCuratorSignal(address _curator, bytes32 _subgraphDeploymentID) + public + view + override + returns (uint256) + { + IGraphCurationToken gcs = pools[_subgraphDeploymentID].gcs; + return (address(gcs) == address(0)) ? 0 : gcs.balanceOf(_curator); + } + + /** + * @notice Get the amount of signal in a curation pool. + * @param _subgraphDeploymentID Subgraph deployment curation poool + * @return Amount of signal minted for the subgraph deployment + */ + function getCurationPoolSignal(bytes32 _subgraphDeploymentID) + public + view + override + returns (uint256) + { + IGraphCurationToken gcs = pools[_subgraphDeploymentID].gcs; + return (address(gcs) == address(0)) ? 0 : gcs.totalSupply(); + } + + /** + * @notice Calculate amount of signal that can be bought with tokens in a curation pool. + * This function considers and excludes the deposit tax. + * @param _subgraphDeploymentID Subgraph deployment to mint signal + * @param _tokensIn Amount of tokens used to mint signal + * @return Amount of signal that can be bought + * @return Amount of GRT that would be subtracted as curation tax + */ + function tokensToSignal(bytes32 _subgraphDeploymentID, uint256 _tokensIn) + public + view + override + returns (uint256, uint256) + { + uint256 curationTax = _tokensIn.mul(uint256(curationTaxPercentage)).div(MAX_PPM); + uint256 signalOut = _tokensToSignal(_subgraphDeploymentID, _tokensIn.sub(curationTax)); + return (signalOut, curationTax); + } + + /** + * @notice Calculate amount of signal that can be bought with tokens in a curation pool, + * without accounting for curation tax. + * @param _subgraphDeploymentID Subgraph deployment to mint signal + * @param _tokensIn Amount of tokens used to mint signal + * @return Amount of signal that can be bought + */ + function tokensToSignalNoTax(bytes32 _subgraphDeploymentID, uint256 _tokensIn) + public + view + override + returns (uint256) + { + return _tokensToSignal(_subgraphDeploymentID, _tokensIn); + } + + /** + * @notice Calculate number of tokens to get when burning signal from a curation pool. + * @param _subgraphDeploymentID Subgraph deployment for which to burn signal + * @param _signalIn Amount of signal to burn + * @return Amount of tokens to get for an amount of signal + */ + function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signalIn) + public + view + override + returns (uint256) + { + CurationPool memory curationPool = pools[_subgraphDeploymentID]; + uint256 curationPoolSignal = getCurationPoolSignal(_subgraphDeploymentID); + require( + curationPool.tokens != 0, + "Subgraph deployment must be curated to perform calculations" + ); + require( + curationPoolSignal >= _signalIn, + "Signal must be above or equal to signal issued in the curation pool" + ); + + return curationPool.tokens.mul(_signalIn).div(curationPoolSignal); + } + + /** + * @dev Internal: Set the minimum deposit amount for curators. + * Update the minimum deposit amount to `_minimumCurationDeposit` + * @param _minimumCurationDeposit Minimum amount of tokens required deposit + */ + function _setMinimumCurationDeposit(uint256 _minimumCurationDeposit) private { + require(_minimumCurationDeposit != 0, "Minimum curation deposit cannot be 0"); + + minimumCurationDeposit = _minimumCurationDeposit; + emit ParameterUpdated("minimumCurationDeposit"); + } + + /** + * @dev Internal: Set the curation tax percentage to charge when a curator deposits GRT tokens. + * @param _percentage Curation tax percentage charged when depositing GRT tokens + */ + function _setCurationTaxPercentage(uint32 _percentage) private { + require( + _percentage <= MAX_PPM, + "Curation tax percentage must be below or equal to MAX_PPM" + ); + + curationTaxPercentage = _percentage; + emit ParameterUpdated("curationTaxPercentage"); + } + + /** + * @dev Internal: Set the master copy to use as clones for the curation token. + * @param _curationTokenMaster Address of implementation contract to use for curation tokens + */ + function _setCurationTokenMaster(address _curationTokenMaster) private { + require(_curationTokenMaster != address(0), "Token master must be non-empty"); + require( + AddressUpgradeable.isContract(_curationTokenMaster), + "Token master must be a contract" + ); + + curationTokenMaster = _curationTokenMaster; + emit ParameterUpdated("curationTokenMaster"); + } + + /** + * @dev Triggers an update of rewards due to a change in signal. + * @param _subgraphDeploymentID Subgraph deployment updated + */ + function _updateRewards(bytes32 _subgraphDeploymentID) private { + IRewardsManager rewardsManager = rewardsManager(); + if (address(rewardsManager) != address(0)) { + rewardsManager.onSubgraphSignalUpdate(_subgraphDeploymentID); + } + } + + /** + * @dev Calculate amount of signal that can be bought with tokens in a curation pool. + * @param _subgraphDeploymentID Subgraph deployment to mint signal + * @param _tokensIn Amount of tokens used to mint signal + * @return Amount of signal that can be bought with tokens + */ + function _tokensToSignal(bytes32 _subgraphDeploymentID, uint256 _tokensIn) + private + view + returns (uint256) + { + // Get curation pool tokens and signal + CurationPool memory curationPool = pools[_subgraphDeploymentID]; + + // Init curation pool + if (curationPool.tokens == 0) { + require( + _tokensIn >= minimumCurationDeposit, + "Curation deposit is below minimum required" + ); + return + SIGNAL_PER_MINIMUM_DEPOSIT.add( + SIGNAL_PER_MINIMUM_DEPOSIT.mul(_tokensIn.sub(minimumCurationDeposit)).div( + minimumCurationDeposit + ) + ); + } + + return getCurationPoolSignal(_subgraphDeploymentID).mul(_tokensIn).div(curationPool.tokens); + } +} diff --git a/contracts/l2/discovery/IL2GNS.sol b/contracts/l2/discovery/IL2GNS.sol new file mode 100644 index 000000000..3a8763274 --- /dev/null +++ b/contracts/l2/discovery/IL2GNS.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; + +/** + * @title Interface for the L2GNS contract. + */ +interface IL2GNS is ICallhookReceiver { + enum L1MessageCodes { + RECEIVE_SUBGRAPH_CODE, + RECEIVE_CURATOR_BALANCE_CODE + } + + /** + * @dev The SubgraphL2TransferData struct holds information + * about a subgraph related to its transfer from L1 to L2. + */ + struct SubgraphL2TransferData { + uint256 tokens; // GRT that will be sent to L2 to mint signal + mapping(address => bool) curatorBalanceClaimed; // True for curators whose balance has been claimed in L2 + bool l2Done; // Transfer finished on L2 side + uint256 subgraphReceivedOnL2BlockNumber; // Block number when the subgraph was received on L2 + } + + /** + * @notice Finish a subgraph transfer from L1. + * The subgraph must have been previously sent through the bridge + * using the sendSubgraphToL2 function on L1GNS. + * @param _l2SubgraphID Subgraph ID in L2 (aliased from the L1 subgraph ID) + * @param _subgraphDeploymentID Latest subgraph deployment to assign to the subgraph + * @param _subgraphMetadata IPFS hash of the subgraph metadata + * @param _versionMetadata IPFS hash of the version metadata + */ + function finishSubgraphTransferFromL1( + uint256 _l2SubgraphID, + bytes32 _subgraphDeploymentID, + bytes32 _subgraphMetadata, + bytes32 _versionMetadata + ) external; + + /** + * @notice Return the aliased L2 subgraph ID from a transferred L1 subgraph ID + * @param _l1SubgraphID L1 subgraph ID + * @return L2 subgraph ID + */ + function getAliasedL2SubgraphID(uint256 _l1SubgraphID) external pure returns (uint256); +} diff --git a/contracts/l2/discovery/L2GNS.sol b/contracts/l2/discovery/L2GNS.sol new file mode 100644 index 000000000..a32263975 --- /dev/null +++ b/contracts/l2/discovery/L2GNS.sol @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +import { GNS } from "../../discovery/GNS.sol"; +import { ICuration } from "../../curation/ICuration.sol"; +import { IL2GNS } from "./IL2GNS.sol"; +import { L2GNSV1Storage } from "./L2GNSStorage.sol"; + +import { IL2Curation } from "../curation/IL2Curation.sol"; + +/** + * @title L2GNS + * @dev The Graph Name System contract provides a decentralized naming system for subgraphs + * used in the scope of the Graph Network. It translates Subgraphs into Subgraph Versions. + * Each version is associated with a Subgraph Deployment. The contract has no knowledge of + * human-readable names. All human readable names emitted in events. + * The contract implements a multicall behaviour to support batching multiple calls in a single + * transaction. + * This particular contract is meant to be deployed in L2, and includes helper functions to + * receive subgraphs that are transferred from L1. + */ +contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { + using SafeMathUpgradeable for uint256; + + uint256 public constant SUBGRAPH_ID_ALIAS_OFFSET = + uint256(0x1111000000000000000000000000000000000000000000000000000000001111); + + /// @dev Emitted when a subgraph is received from L1 through the bridge + event SubgraphReceivedFromL1( + uint256 indexed _l1SubgraphID, + uint256 indexed _l2SubgraphID, + address indexed _owner, + uint256 _tokens + ); + /// @dev Emitted when a subgraph transfer from L1 is finalized, so the subgraph is published on L2 + event SubgraphL2TransferFinalized(uint256 indexed _l2SubgraphID); + /// @dev Emitted when the L1 balance for a curator has been claimed + event CuratorBalanceReceived( + uint256 indexed _l1SubgraphId, + uint256 indexed _l2SubgraphID, + address indexed _l2Curator, + uint256 _tokens + ); + /// @dev Emitted when the L1 balance for a curator has been returned to the beneficiary. + /// This can happen if the subgraph transfer was not finished when the curator's tokens arrived. + event CuratorBalanceReturnedToBeneficiary( + uint256 indexed _l1SubgraphID, + address indexed _l2Curator, + uint256 _tokens + ); + + /** + * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. + */ + modifier onlyL2Gateway() { + require(msg.sender == address(graphTokenGateway()), "ONLY_GATEWAY"); + _; + } + + /** + * @notice Receive tokens with a callhook from the bridge. + * The callhook will receive a subgraph or a curator's balance from L1. The _data parameter + * must contain the ABI encoding of: + * (uint8 code, uint256 subgraphId, address beneficiary) + * Where `code` is one of the codes defined in IL2GNS.L1MessageCodes. + * If the code is RECEIVE_SUBGRAPH_CODE, the beneficiary is the address of the + * owner of the subgraph on L2. + * If the code is RECEIVE_CURATOR_BALANCE_CODE, then the beneficiary is the + * address of the curator in L2. In this case, If the subgraph transfer was never finished + * (or the subgraph doesn't exist), the tokens will be sent to the curator. + * @dev This function is called by the L2GraphTokenGateway contract. + * @param _from Token sender in L1 (must be the L1GNS) + * @param _amount Amount of tokens that were transferred + * @param _data ABI-encoded callhook data + */ + function onTokenTransfer( + address _from, + uint256 _amount, + bytes calldata _data + ) external override notPartialPaused onlyL2Gateway { + require(_from == counterpartGNSAddress, "ONLY_L1_GNS_THROUGH_BRIDGE"); + (uint8 code, uint256 l1SubgraphID, address beneficiary) = abi.decode( + _data, + (uint8, uint256, address) + ); + + if (code == uint8(L1MessageCodes.RECEIVE_SUBGRAPH_CODE)) { + _receiveSubgraphFromL1(l1SubgraphID, beneficiary, _amount); + } else if (code == uint8(L1MessageCodes.RECEIVE_CURATOR_BALANCE_CODE)) { + _mintSignalFromL1(l1SubgraphID, beneficiary, _amount); + } else { + revert("INVALID_CODE"); + } + } + + /** + * @notice Finish a subgraph transfer from L1. + * The subgraph must have been previously sent through the bridge + * using the sendSubgraphToL2 function on L1GNS. + * @param _l2SubgraphID Subgraph ID (aliased from the L1 subgraph ID) + * @param _subgraphDeploymentID Latest subgraph deployment to assign to the subgraph + * @param _subgraphMetadata IPFS hash of the subgraph metadata + * @param _versionMetadata IPFS hash of the version metadata + */ + function finishSubgraphTransferFromL1( + uint256 _l2SubgraphID, + bytes32 _subgraphDeploymentID, + bytes32 _subgraphMetadata, + bytes32 _versionMetadata + ) external override notPartialPaused onlySubgraphAuth(_l2SubgraphID) { + IL2GNS.SubgraphL2TransferData storage transferData = subgraphL2TransferData[_l2SubgraphID]; + SubgraphData storage subgraphData = _getSubgraphData(_l2SubgraphID); + require(transferData.subgraphReceivedOnL2BlockNumber != 0, "INVALID_SUBGRAPH"); + require(!transferData.l2Done, "ALREADY_DONE"); + transferData.l2Done = true; + + // New subgraph deployment must be non-empty + require(_subgraphDeploymentID != 0, "GNS: deploymentID != 0"); + + IL2Curation curation = IL2Curation(address(curation())); + // Update pool: constant nSignal, vSignal can change (w/no slippage protection) + // Buy all signal from the new deployment + uint256 vSignal = curation.mintTaxFree(_subgraphDeploymentID, transferData.tokens); + uint256 nSignal = vSignalToNSignal(_l2SubgraphID, vSignal); + + subgraphData.disabled = false; + subgraphData.vSignal = vSignal; + subgraphData.nSignal = nSignal; + subgraphData.curatorNSignal[msg.sender] = nSignal; + subgraphData.subgraphDeploymentID = _subgraphDeploymentID; + // Set the token metadata + _setSubgraphMetadata(_l2SubgraphID, _subgraphMetadata); + + emit SubgraphPublished(_l2SubgraphID, _subgraphDeploymentID, fixedReserveRatio); + emit SubgraphUpgraded( + _l2SubgraphID, + subgraphData.vSignal, + transferData.tokens, + _subgraphDeploymentID + ); + emit SubgraphVersionUpdated(_l2SubgraphID, _subgraphDeploymentID, _versionMetadata); + emit SubgraphL2TransferFinalized(_l2SubgraphID); + } + + /** + * @notice Publish a new version of an existing subgraph. + * @dev This is the same as the one in the base GNS, but skips the check for + * a subgraph to not be pre-curated, as the reserve ratio in L2 is set to 1, + * which prevents the risk of rug-pulling. + * @param _subgraphID Subgraph ID + * @param _subgraphDeploymentID Subgraph deployment ID of the new version + * @param _versionMetadata IPFS hash for the subgraph version metadata + */ + function publishNewVersion( + uint256 _subgraphID, + bytes32 _subgraphDeploymentID, + bytes32 _versionMetadata + ) external override notPaused onlySubgraphAuth(_subgraphID) { + // Perform the upgrade from the current subgraph deployment to the new one. + // This involves burning all signal from the old deployment and using the funds to buy + // from the new deployment. + // This will also make the change to target to the new deployment. + + // Subgraph check + SubgraphData storage subgraphData = _getSubgraphOrRevert(_subgraphID); + + // New subgraph deployment must be non-empty + require(_subgraphDeploymentID != 0, "GNS: Cannot set deploymentID to 0 in publish"); + + // New subgraph deployment must be different than current + require( + _subgraphDeploymentID != subgraphData.subgraphDeploymentID, + "GNS: Cannot publish a new version with the same subgraph deployment ID" + ); + + ICuration curation = curation(); + + // Move all signal from previous version to new version + // NOTE: We will only do this as long as there is signal on the subgraph + if (subgraphData.nSignal != 0) { + // Burn all version signal in the name pool for tokens (w/no slippage protection) + // Sell all signal from the old deployment + uint256 tokens = curation.burn( + subgraphData.subgraphDeploymentID, + subgraphData.vSignal, + 0 + ); + + // Take the owner cut of the curation tax, add it to the total + // Upgrade is only callable by the owner, we assume then that msg.sender = owner + address subgraphOwner = msg.sender; + uint256 tokensWithTax = _chargeOwnerTax( + tokens, + subgraphOwner, + curation.curationTaxPercentage() + ); + + // Update pool: constant nSignal, vSignal can change (w/no slippage protection) + // Buy all signal from the new deployment + (subgraphData.vSignal, ) = curation.mint(_subgraphDeploymentID, tokensWithTax, 0); + + emit SubgraphUpgraded( + _subgraphID, + subgraphData.vSignal, + tokensWithTax, + _subgraphDeploymentID + ); + } + + // Update target deployment + subgraphData.subgraphDeploymentID = _subgraphDeploymentID; + + emit SubgraphVersionUpdated(_subgraphID, _subgraphDeploymentID, _versionMetadata); + } + + /** + * @notice Return the aliased L2 subgraph ID from a transferred L1 subgraph ID + * @param _l1SubgraphID L1 subgraph ID + * @return L2 subgraph ID + */ + function getAliasedL2SubgraphID(uint256 _l1SubgraphID) public pure override returns (uint256) { + return _l1SubgraphID + SUBGRAPH_ID_ALIAS_OFFSET; + } + + /** + * @dev Receive a subgraph from L1. + * This function will initialize a subgraph received through the bridge, + * and store the transfer data so that it's finalized later using finishSubgraphTransferFromL1. + * @param _l1SubgraphID Subgraph ID in L1 (will be aliased) + * @param _subgraphOwner Owner of the subgraph + * @param _tokens Tokens to be deposited in the subgraph + */ + function _receiveSubgraphFromL1( + uint256 _l1SubgraphID, + address _subgraphOwner, + uint256 _tokens + ) internal { + uint256 l2SubgraphID = getAliasedL2SubgraphID(_l1SubgraphID); + SubgraphData storage subgraphData = _getSubgraphData(l2SubgraphID); + IL2GNS.SubgraphL2TransferData storage transferData = subgraphL2TransferData[l2SubgraphID]; + + subgraphData.reserveRatioDeprecated = fixedReserveRatio; + // The subgraph will be disabled until finishSubgraphTransferFromL1 is called + subgraphData.disabled = true; + + transferData.tokens = _tokens; + transferData.subgraphReceivedOnL2BlockNumber = block.number; + + // Mint the NFT. Use the subgraphID as tokenID. + // This function will check the if tokenID already exists. + // Note we do this here so that we can later do the onlySubgraphAuth + // check in finishSubgraphTransferFromL1. + _mintNFT(_subgraphOwner, l2SubgraphID); + + emit SubgraphReceivedFromL1(_l1SubgraphID, l2SubgraphID, _subgraphOwner, _tokens); + } + + /** + * @notice Deposit GRT into a subgraph and mint signal, using tokens received from L1. + * If the subgraph transfer was never finished (or the subgraph doesn't exist), the tokens will be sent to the curator. + * @dev This looks a lot like GNS.mintSignal, but doesn't pull the tokens from the + * curator and has no slippage protection. + * @param _l1SubgraphID Subgraph ID in L1 (will be aliased) + * @param _curator Curator address + * @param _tokensIn The amount of tokens the nameCurator wants to deposit + */ + function _mintSignalFromL1( + uint256 _l1SubgraphID, + address _curator, + uint256 _tokensIn + ) internal { + uint256 l2SubgraphID = getAliasedL2SubgraphID(_l1SubgraphID); + IL2GNS.SubgraphL2TransferData storage transferData = subgraphL2TransferData[l2SubgraphID]; + SubgraphData storage subgraphData = _getSubgraphData(l2SubgraphID); + + // If subgraph transfer wasn't finished, we should send the tokens to the curator + if (!transferData.l2Done || subgraphData.disabled) { + graphToken().transfer(_curator, _tokensIn); + emit CuratorBalanceReturnedToBeneficiary(_l1SubgraphID, _curator, _tokensIn); + } else { + // Get name signal to mint for tokens deposited + IL2Curation curation = IL2Curation(address(curation())); + uint256 vSignal = curation.mintTaxFree(subgraphData.subgraphDeploymentID, _tokensIn); + uint256 nSignal = vSignalToNSignal(l2SubgraphID, vSignal); + + // Update pools + subgraphData.vSignal = subgraphData.vSignal.add(vSignal); + subgraphData.nSignal = subgraphData.nSignal.add(nSignal); + subgraphData.curatorNSignal[_curator] = subgraphData.curatorNSignal[_curator].add( + nSignal + ); + + emit SignalMinted(l2SubgraphID, _curator, nSignal, vSignal, _tokensIn); + emit CuratorBalanceReceived(_l1SubgraphID, l2SubgraphID, _curator, _tokensIn); + } + } + + /** + * @dev Get subgraph data. + * Since there are no legacy subgraphs in L2, we override the base + * GNS method to save us the step of checking for legacy subgraphs. + * @param _subgraphID Subgraph ID + * @return Subgraph Data + */ + function _getSubgraphData(uint256 _subgraphID) + internal + view + override + returns (SubgraphData storage) + { + // Return new subgraph type + return subgraphs[_subgraphID]; + } +} diff --git a/contracts/l2/discovery/L2GNSStorage.sol b/contracts/l2/discovery/L2GNSStorage.sol new file mode 100644 index 000000000..f658c49d9 --- /dev/null +++ b/contracts/l2/discovery/L2GNSStorage.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { IL2GNS } from "./IL2GNS.sol"; + +/** + * @title L2GNSV1Storage + * @notice This contract holds all the L2-specific storage variables for the L2GNS contract, version 1 + * @dev + */ +abstract contract L2GNSV1Storage { + /// Data for subgraph transfer from L1 to L2 + mapping(uint256 => IL2GNS.SubgraphL2TransferData) public subgraphL2TransferData; + /// @dev Storage gap to keep storage slots fixed in future versions + uint256[50] private __gap; +} diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 7af102fb4..03417a59a 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -284,7 +284,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran } /** - * @notice Decodes calldata required for migration of tokens + * @notice Decodes calldata required for transfer of tokens to L1 * @dev extraData can be left empty * @param _data Encoded callhook data * @return Sender of the tx diff --git a/contracts/tests/LegacyGNSMock.sol b/contracts/tests/LegacyGNSMock.sol new file mode 100644 index 000000000..0e1e94be4 --- /dev/null +++ b/contracts/tests/LegacyGNSMock.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { L1GNS } from "../discovery/L1GNS.sol"; +import { IGNS } from "../discovery/IGNS.sol"; + +/** + * @title LegacyGNSMock contract + * @dev This is used to test the migration of legacy subgraphs to NFT-based subgraphs and transferring them to L2 + */ +contract LegacyGNSMock is L1GNS { + /** + * @notice Create a mock legacy subgraph (owned by the msg.sender) + * @param subgraphNumber Number of the subgraph (sequence ID for the account) + * @param subgraphDeploymentID Subgraph deployment ID + */ + function createLegacySubgraph(uint256 subgraphNumber, bytes32 subgraphDeploymentID) external { + SubgraphData storage subgraphData = legacySubgraphData[msg.sender][subgraphNumber]; + legacySubgraphs[msg.sender][subgraphNumber] = subgraphDeploymentID; + subgraphData.subgraphDeploymentID = subgraphDeploymentID; + subgraphData.nSignal = 1000; // Mock value + } + + /** + * @notice Get the subgraph deployment ID for a subgraph + * @param subgraphID Subgraph ID + * @return subgraphDeploymentID Subgraph deployment ID + */ + function getSubgraphDeploymentID(uint256 subgraphID) + external + view + returns (bytes32 subgraphDeploymentID) + { + IGNS.SubgraphData storage subgraph = _getSubgraphData(subgraphID); + subgraphDeploymentID = subgraph.subgraphDeploymentID; + } + + /** + * @notice Get the nSignal for a subgraph + * @param subgraphID Subgraph ID + * @return nSignal The subgraph's nSignal + */ + function getSubgraphNSignal(uint256 subgraphID) external view returns (uint256 nSignal) { + IGNS.SubgraphData storage subgraph = _getSubgraphData(subgraphID); + nSignal = subgraph.nSignal; + } +} diff --git a/e2e/deployment/config/gns.test.ts b/e2e/deployment/config/gns.test.ts index 4337dffd0..08408bcdd 100644 --- a/e2e/deployment/config/gns.test.ts +++ b/e2e/deployment/config/gns.test.ts @@ -3,7 +3,7 @@ import hre from 'hardhat' describe('GNS configuration', () => { const { - contracts: { Controller, GNS, BancorFormula, SubgraphNFT }, + contracts: { Controller, GNS, SubgraphNFT }, } = hre.graph() it('should be controlled by Controller', async function () { @@ -11,11 +11,6 @@ describe('GNS configuration', () => { expect(controller).eq(Controller.address) }) - it('bondingCurve should match the BancorFormula deployment address', async function () { - const bondingCurve = await GNS.bondingCurve() - expect(bondingCurve).eq(BancorFormula.address) - }) - it('subgraphNFT should match the SubgraphNFT deployment address', async function () { const subgraphNFT = await GNS.subgraphNFT() expect(subgraphNFT).eq(SubgraphNFT.address) diff --git a/e2e/deployment/config/curation.test.ts b/e2e/deployment/config/l1/curation.test.ts similarity index 85% rename from e2e/deployment/config/curation.test.ts rename to e2e/deployment/config/l1/curation.test.ts index 6d6a4d1ea..612510008 100644 --- a/e2e/deployment/config/curation.test.ts +++ b/e2e/deployment/config/l1/curation.test.ts @@ -1,12 +1,18 @@ import { expect } from 'chai' import hre from 'hardhat' -import { getItemValue } from '../../../cli/config' +import { getItemValue } from '../../../../cli/config' +import GraphChain from '../../../../gre/helpers/chain' -describe('Curation configuration', () => { +describe('[L1] Curation configuration', () => { + const graph = hre.graph() const { graphConfig, contracts: { Controller, Curation, BancorFormula, GraphCurationToken }, - } = hre.graph() + } = graph + + before(async function () { + if (GraphChain.isL2(graph.chainId)) this.skip() + }) it('should be controlled by Controller', async function () { const controller = await Curation.controller() diff --git a/e2e/deployment/config/l2/l2Curation.test.ts b/e2e/deployment/config/l2/l2Curation.test.ts new file mode 100644 index 000000000..809eb0ecd --- /dev/null +++ b/e2e/deployment/config/l2/l2Curation.test.ts @@ -0,0 +1,44 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { getItemValue } from '../../../../cli/config' +import GraphChain from '../../../../gre/helpers/chain' + +describe('[L2] L2Curation configuration', () => { + const graph = hre.graph() + const { + graphConfig, + contracts: { Controller, L2Curation, BancorFormula, GraphCurationToken }, + } = graph + + before(async function () { + if (GraphChain.isL1(graph.chainId)) this.skip() + }) + + it('should be controlled by Controller', async function () { + const controller = await L2Curation.controller() + expect(controller).eq(Controller.address) + }) + + it('curationTokenMaster should match the GraphCurationToken deployment address', async function () { + const gct = await L2Curation.curationTokenMaster() + expect(gct).eq(GraphCurationToken.address) + }) + + it('defaultReserveRatio should be a constant 1000000', async function () { + const value = await L2Curation.defaultReserveRatio() + const expected = 1000000 + expect(value).eq(expected) + }) + + it('curationTaxPercentage should match "curationTaxPercentage" in the config file', async function () { + const value = await L2Curation.curationTaxPercentage() + const expected = getItemValue(graphConfig, 'contracts/L2Curation/init/curationTaxPercentage') + expect(value).eq(expected) + }) + + it('minimumCurationDeposit should match "minimumCurationDeposit" in the config file', async function () { + const value = await L2Curation.minimumCurationDeposit() + const expected = getItemValue(graphConfig, 'contracts/L2Curation/init/minimumCurationDeposit') + expect(value).eq(expected) + }) +}) diff --git a/e2e/scenarios/lib/subgraph.ts b/e2e/scenarios/lib/subgraph.ts index f97f1d76b..24d523d45 100644 --- a/e2e/scenarios/lib/subgraph.ts +++ b/e2e/scenarios/lib/subgraph.ts @@ -3,6 +3,7 @@ import { BigNumber } from 'ethers' import { solidityKeccak256 } from 'ethers/lib/utils' import { NetworkContracts } from '../../../cli/contracts' import { randomHexBytes, sendTransaction } from '../../../cli/network' +import hre from 'hardhat' export const recreatePreviousSubgraphId = async ( contracts: NetworkContracts, @@ -14,7 +15,7 @@ export const recreatePreviousSubgraphId = async ( } export const buildSubgraphID = (account: string, seqID: BigNumber): string => - solidityKeccak256(['address', 'uint256'], [account, seqID]) + solidityKeccak256(['address', 'uint256', 'uint256'], [account, seqID, hre.network.config.chainId]) export const publishNewSubgraph = async ( contracts: NetworkContracts, diff --git a/test/curation/curation.test.ts b/test/curation/curation.test.ts index 897ef8da3..51b739f44 100644 --- a/test/curation/curation.test.ts +++ b/test/curation/curation.test.ts @@ -1,12 +1,23 @@ import { expect } from 'chai' -import { utils, BigNumber, Event } from 'ethers' +import { utils, BigNumber, Event, Signer } from 'ethers' import { Curation } from '../../build/types/Curation' import { GraphToken } from '../../build/types/GraphToken' import { Controller } from '../../build/types/Controller' import { NetworkFixture } from '../lib/fixtures' -import { getAccounts, randomHexBytes, toBN, toGRT, formatGRT, Account } from '../lib/testHelpers' +import { + getAccounts, + randomHexBytes, + toBN, + toGRT, + formatGRT, + Account, + impersonateAccount, + setAccountBalance, +} from '../lib/testHelpers' +import { GNS } from '../../build/types/GNS' +import { parseEther } from 'ethers/lib/utils' const MAX_PPM = 1000000 @@ -34,12 +45,14 @@ describe('Curation', () => { let governor: Account let curator: Account let stakingMock: Account + let gnsImpersonator: Signer let fixture: NetworkFixture let curation: Curation let grt: GraphToken let controller: Controller + let gns: GNS // Test values const signalAmountFor1000Tokens = toGRT('3.162277660168379331') @@ -189,11 +202,15 @@ describe('Curation', () => { ;[me, governor, curator, stakingMock] = await getAccounts() fixture = new NetworkFixture() - ;({ controller, curation, grt } = await fixture.load(governor.signer)) + ;({ controller, curation, grt, gns } = await fixture.load(governor.signer)) - // Give some funds to the curator and approve the curation contract + gnsImpersonator = await impersonateAccount(gns.address) + await setAccountBalance(gns.address, parseEther('1')) + // Give some funds to the curator and GNS impersonator and approve the curation contract await grt.connect(governor.signer).mint(curator.address, curatorTokens) await grt.connect(curator.signer).approve(curation.address, curatorTokens) + await grt.connect(governor.signer).mint(gns.address, curatorTokens) + await grt.connect(gnsImpersonator).approve(curation.address, curatorTokens) // Give some funds to the staking contract and approve the curation contract await grt.connect(governor.signer).mint(stakingMock.address, tokensToCollect) diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index 1959cba2f..1eb7ff134 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -31,6 +31,7 @@ describe('L1GraphTokenGateway', () => { let mockL2GRT: Account let mockL2Gateway: Account let pauseGuardian: Account + let mockL2GNS: Account let fixture: NetworkFixture let grt: GraphToken @@ -64,8 +65,16 @@ describe('L1GraphTokenGateway', () => { ) before(async function () { - ;[governor, tokenSender, l2Receiver, mockRouter, mockL2GRT, mockL2Gateway, pauseGuardian] = - await getAccounts() + ;[ + governor, + tokenSender, + l2Receiver, + mockRouter, + mockL2GRT, + mockL2Gateway, + pauseGuardian, + mockL2GNS, + ] = await getAccounts() // Dummy code on the mock router so that it appears as a contract await provider().send('hardhat_setCode', [mockRouter.address, '0x1234']) @@ -292,6 +301,7 @@ describe('L1GraphTokenGateway', () => { mockRouter.address, mockL2GRT.address, mockL2Gateway.address, + mockL2GNS.address, ) let tx = l1GraphTokenGateway.connect(governor.signer).setPaused(true) await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) @@ -323,6 +333,7 @@ describe('L1GraphTokenGateway', () => { mockRouter.address, mockL2GRT.address, mockL2Gateway.address, + mockL2GNS.address, ) await l1GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) let tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) @@ -428,6 +439,7 @@ describe('L1GraphTokenGateway', () => { mockRouter.address, mockL2GRT.address, mockL2Gateway.address, + mockL2GNS.address, ) }) diff --git a/test/gns.test.ts b/test/gns.test.ts index 1ee46002a..723e3c913 100644 --- a/test/gns.test.ts +++ b/test/gns.test.ts @@ -1,61 +1,79 @@ import { expect } from 'chai' import { ethers, ContractTransaction, BigNumber, Event } from 'ethers' -import { solidityKeccak256 } from 'ethers/lib/utils' +import { defaultAbiCoder, Interface } from 'ethers/lib/utils' import { SubgraphDeploymentID } from '@graphprotocol/common-ts' -import { GNS } from '../build/types/GNS' +import { LegacyGNSMock } from '../build/types/LegacyGNSMock' import { GraphToken } from '../build/types/GraphToken' import { Curation } from '../build/types/Curation' import { SubgraphNFT } from '../build/types/SubgraphNFT' -import { getAccounts, randomHexBytes, Account, toGRT } from './lib/testHelpers' -import { NetworkFixture } from './lib/fixtures' +import { + getAccounts, + randomHexBytes, + Account, + toGRT, + advanceBlocks, + provider, +} from './lib/testHelpers' +import { ArbitrumL1Mocks, NetworkFixture } from './lib/fixtures' import { toBN, formatGRT } from './lib/testHelpers' import { getContractAt } from '../cli/network' +import { deployContract } from './lib/deployment' +import { network } from '../cli' +import { Controller } from '../build/types/Controller' +import { GraphProxyAdmin } from '../build/types/GraphProxyAdmin' +import { L1GNS } from '../build/types/L1GNS' +import path from 'path' +import { Artifacts } from 'hardhat/internal/artifacts' +import { L1GraphTokenGateway } from '../build/types/L1GraphTokenGateway' +import { + AccountDefaultName, + buildLegacySubgraphID, + buildSubgraph, + buildSubgraphID, + createDefaultName, + PublishSubgraph, + Subgraph, + getTokensAndVSignal, + publishNewSubgraph, + publishNewVersion, + mintSignal, + deprecateSubgraph, + burnSignal, +} from './lib/gnsUtils' const { AddressZero, HashZero } = ethers.constants -// Entities -interface PublishSubgraph { - subgraphDeploymentID: string - versionMetadata: string - subgraphMetadata: string -} - -interface Subgraph { - vSignal: BigNumber - nSignal: BigNumber - subgraphDeploymentID: string - reserveRatio: number - disabled: boolean - withdrawableGRT: BigNumber - id?: string -} - -interface AccountDefaultName { - name: string - nameIdentifier: string -} +const ARTIFACTS_PATH = path.resolve('build/contracts') +const artifacts = new Artifacts(ARTIFACTS_PATH) +const l2GNSabi = artifacts.readArtifactSync('L2GNS').abi +const l2GNSIface = new Interface(l2GNSabi) // Utils - -const DEFAULT_RESERVE_RATIO = 1000000 const toFloat = (n: BigNumber) => parseFloat(formatGRT(n)) const toRound = (n: number) => n.toFixed(12) -const buildSubgraphID = (account: string, seqID: BigNumber): string => - solidityKeccak256(['address', 'uint256'], [account, seqID]) -describe('GNS', () => { +describe('L1GNS', () => { let me: Account let other: Account let another: Account let governor: Account + let mockRouter: Account + let mockL2GRT: Account + let mockL2Gateway: Account + let mockL2GNS: Account let fixture: NetworkFixture - let gns: GNS + let gns: L1GNS + let legacyGNSMock: LegacyGNSMock let grt: GraphToken let curation: Curation + let controller: Controller + let proxyAdmin: GraphProxyAdmin + let l1GraphTokenGateway: L1GraphTokenGateway + let arbitrumMocks: ArbitrumL1Mocks const tokens1000 = toGRT('1000') const tokens10000 = toGRT('10000') @@ -66,31 +84,9 @@ describe('GNS', () => { let newSubgraph1: PublishSubgraph let defaultName: AccountDefaultName - const buildSubgraph = (): PublishSubgraph => { - return { - subgraphDeploymentID: randomHexBytes(), - versionMetadata: randomHexBytes(), - subgraphMetadata: randomHexBytes(), - } - } - - const createDefaultName = (name: string): AccountDefaultName => { - return { - name: name, - nameIdentifier: ethers.utils.namehash(name), - } - } - - const getTokensAndVSignal = async (subgraphDeploymentID: string): Promise> => { - const curationPool = await curation.pools(subgraphDeploymentID) - const vSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) - return [curationPool.tokens, vSignal] - } - async function calcGNSBondingCurve( gnsSupply: BigNumber, // nSignal gnsReserveBalance: BigNumber, // vSignal - gnsReserveRatio: number, // default reserve ratio of GNS depositAmount: BigNumber, // GRT deposited subgraphID: string, ): Promise { @@ -142,309 +138,6 @@ describe('GNS', () => { ) } - const publishNewSubgraph = async ( - account: Account, - newSubgraph: PublishSubgraph, // Defaults to subgraph created in before() - ): Promise => { - const subgraphID = buildSubgraphID(account.address, await gns.nextAccountSeqID(account.address)) - - // Send tx - const tx = gns - .connect(account.signer) - .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.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) - - return { ...subgraph, id: subgraphID } - } - - const publishNewVersion = async ( - account: Account, - subgraphID: string, - newSubgraph: PublishSubgraph, - ) => { - // 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)] - - // Send tx - const tx = gns - .connect(account.signer) - .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)) { - 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, - ) - 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, - ) - expect(afterTokensNewCurve).eq(totalAdjustedUp.sub(newCurationTaxEstimate)) - expect(afterVSignalNewCurve).eq(newVSignalEstimate) - - // Check the nSignal pool - const afterSubgraph = await gns.subgraphs(subgraphID) - expect(afterSubgraph.vSignal).eq(afterVSignalNewCurve).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 - } - - const deprecateSubgraph = async (account: Account, subgraphID: string) => { - // Before state - const beforeSubgraph = await gns.subgraphs(subgraphID) - const [beforeTokens] = await getTokensAndVSignal(beforeSubgraph.subgraphDeploymentID) - - // 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.signer).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.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 - } - - /* - const upgradeNameSignal = async ( - account: Account, - graphAccount: string, - subgraphNumber0: number, - newSubgraphDeplyomentID: string, - ): Promise => { - // Before stats for the old vSignal curve - const beforeTokensVSigOldCuration = await getTokensAndVSignal(subgraph0.subgraphDeploymentID) - const beforeTokensOldCuration = beforeTokensVSigOldCuration[0] - const beforeVSignalOldCuration = beforeTokensVSigOldCuration[1] - - // Before stats for the name curve - const poolBefore = await gns.nameSignals(graphAccount, subgraphNumber0) - const nSigBefore = poolBefore[1] - - // Check what selling all nSignal, which == selling all vSignal, should return for tokens - const nSignalToTokensResult = await gns.nSignalToTokens( - graphAccount, - subgraphNumber0, - nSigBefore, - ) - const vSignalBurnEstimate = nSignalToTokensResult[0] - const tokensReceivedEstimate = nSignalToTokensResult[1] - - // since in upgrade, owner must refund fees, we need to actually add this back in - const feesToAddBackEstimate = nSignalToTokensResult[2] - const upgradeTokenReturn = tokensReceivedEstimate.add(feesToAddBackEstimate) - - // Get the value for new vSignal that should be created on the new curve - const newVSignalEstimate = await curation.tokensToSignal( - newSubgraphDeplyomentID, - upgradeTokenReturn, - ) - - // Do the upgrade - const tx = gns - .connect(account.signer) - .upgradeNameSignal(graphAccount, subgraphNumber0, newSubgraphDeplyomentID) - await expect(tx) - .emit(gns, 'NameSignalUpgrade') - .withArgs( - graphAccount, - subgraphNumber0, - newVSignalEstimate, - upgradeTokenReturn, - newSubgraphDeplyomentID, - ) - - // Check curation vSignal old was lowered and tokens too - const [afterTokensOldCuration, vSigAfterOldCuration] = await getTokensAndVSignal( - subgraph0.subgraphDeploymentID, - ) - expect(afterTokensOldCuration).eq(beforeTokensOldCuration.sub(upgradeTokenReturn)) - expect(vSigAfterOldCuration).eq(beforeVSignalOldCuration.sub(vSignalBurnEstimate)) - - // Check the vSignal of the new curation curve, amd tokens - const [afterTokensNewCurve, vSigAfterNewCurve] = await getTokensAndVSignal( - newSubgraphDeplyomentID, - ) - expect(afterTokensNewCurve).eq(upgradeTokenReturn) - expect(vSigAfterNewCurve).eq(newVSignalEstimate) - - // Check the nSignal pool - const pool = await gns.nameSignals(graphAccount, subgraphNumber0) - const vSigPool = pool[0] - const nSigAfter = pool[1] - const deploymentID = pool[2] - expect(vSigAfterNewCurve).eq(vSigPool).eq(newVSignalEstimate) - expect(nSigBefore).eq(nSigAfter) // should not change - expect(deploymentID).eq(newSubgraphDeplyomentID) - - return tx - } - */ - - const mintSignal = async ( - account: Account, - subgraphID: string, - tokensIn: BigNumber, - ): Promise => { - // Before state - const beforeSubgraph = await gns.subgraphs(subgraphID) - const [beforeTokens, beforeVSignal] = await getTokensAndVSignal( - beforeSubgraph.subgraphDeploymentID, - ) - - // Deposit - const { - 0: vSignalExpected, - 1: nSignalExpected, - 2: curationTax, - } = await gns.tokensToNSignal(subgraphID, tokensIn) - const tx = gns.connect(account.signer).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, - ) - - // 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 - } - - const burnSignal = async (account: Account, subgraphID: string): Promise => { - // Before state - const beforeSubgraph = await gns.subgraphs(subgraphID) - const [beforeTokens, beforeVSignal] = await getTokensAndVSignal( - beforeSubgraph.subgraphDeploymentID, - ) - 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.signer).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, - ) - - // Check state - expect(afterTokens).eq(beforeTokens.sub(tokensExpected)) - expect(afterVSignalCuration).eq(beforeVSignal.sub(vSignalExpected)) - expect(afterSubgraph.nSignal).eq(beforeSubgraph.nSignal.sub(beforeUsersNSignal)) - - return tx - } - const transferSignal = async ( subgraphID: string, owner: Account, @@ -503,22 +196,69 @@ describe('GNS', () => { return tx } + const deployLegacyGNSMock = async (): Promise => { + const subgraphDescriptor = await deployContract('SubgraphNFTDescriptor', governor.signer) + const subgraphNFT = (await deployContract( + 'SubgraphNFT', + governor.signer, + governor.address, + )) as SubgraphNFT + + // Deploy + legacyGNSMock = (await network.deployContractWithProxy( + proxyAdmin, + 'LegacyGNSMock', + [controller.address, subgraphNFT.address], + governor.signer, + )) as unknown as LegacyGNSMock + + // Post-config + await subgraphNFT.connect(governor.signer).setMinter(legacyGNSMock.address) + await subgraphNFT.connect(governor.signer).setTokenDescriptor(subgraphDescriptor.address) + await legacyGNSMock.connect(governor.signer).syncAllContracts() + await legacyGNSMock.connect(governor.signer).approveAll() + await l1GraphTokenGateway.connect(governor.signer).addToCallhookAllowlist(legacyGNSMock.address) + await legacyGNSMock.connect(governor.signer).setCounterpartGNSAddress(mockL2GNS.address) + } + before(async function () { - ;[me, other, governor, another] = await getAccounts() + ;[me, other, governor, another, mockRouter, mockL2GRT, mockL2Gateway, mockL2GNS] = + await getAccounts() + // Dummy code on the mock router so that it appears as a contract + await provider().send('hardhat_setCode', [mockRouter.address, '0x1234']) fixture = new NetworkFixture() - ;({ grt, curation, gns } = await fixture.load(governor.signer)) + const fixtureContracts = await fixture.load(governor.signer) + ;({ grt, curation, gns, controller, proxyAdmin, l1GraphTokenGateway } = fixtureContracts) 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.signer).mint(me.address, tokens100000) await grt.connect(governor.signer).mint(other.address, tokens100000) + await grt.connect(governor.signer).mint(another.address, tokens100000) await grt.connect(me.signer).approve(gns.address, tokens100000) await grt.connect(me.signer).approve(curation.address, tokens100000) await grt.connect(other.signer).approve(gns.address, tokens100000) await grt.connect(other.signer).approve(curation.address, tokens100000) + await grt.connect(another.signer).approve(gns.address, tokens100000) + await grt.connect(another.signer).approve(curation.address, tokens100000) // Update curation tax to test the functionality of it in disableNameSignal() await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) + + // Deploying a GNS mock with support for legacy subgraphs + await deployLegacyGNSMock() + await grt.connect(me.signer).approve(legacyGNSMock.address, tokens100000) + + arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + mockL2GNS.address, + ) }) beforeEach(async function () { @@ -550,6 +290,22 @@ describe('GNS', () => { }) }) + describe('setCounterpartGNSAddress', function () { + it('should set `counterpartGNSAddress`', async function () { + // Can set if allowed + const newValue = other.address + const tx = gns.connect(governor.signer).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.signer).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 @@ -593,7 +349,7 @@ describe('GNS', () => { let subgraph: Subgraph beforeEach(async function () { - subgraph = await publishNewSubgraph(me, newSubgraph0) + subgraph = await publishNewSubgraph(me, newSubgraph0, gns) }) it('updateSubgraphMetadata emits the event', async function () { @@ -615,21 +371,21 @@ describe('GNS', () => { describe('isPublished', function () { it('should return if the subgraph is published', async function () { - const subgraphID = buildSubgraphID(me.address, toBN(0)) + const subgraphID = await buildSubgraphID(me.address, toBN(0)) expect(await gns.isPublished(subgraphID)).eq(false) - await publishNewSubgraph(me, newSubgraph0) + await publishNewSubgraph(me, newSubgraph0, gns) expect(await gns.isPublished(subgraphID)).eq(true) }) }) describe('publishNewSubgraph', async function () { it('should publish a new subgraph and first version with it', async function () { - await publishNewSubgraph(me, newSubgraph0) + await publishNewSubgraph(me, newSubgraph0, gns) }) it('should publish a new subgraph with an incremented value', async function () { - const subgraph1 = await publishNewSubgraph(me, newSubgraph0) - const subgraph2 = await publishNewSubgraph(me, newSubgraph1) + const subgraph1 = await publishNewSubgraph(me, newSubgraph0, gns) + const subgraph2 = await publishNewSubgraph(me, newSubgraph1, gns) expect(subgraph1.id).not.eq(subgraph2.id) }) @@ -645,17 +401,17 @@ describe('GNS', () => { let subgraph: Subgraph beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0) - await mintSignal(me, subgraph.id, tokens10000) + subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + 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) + 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()) - await publishNewVersion(me, emptySignalSubgraph.id, newSubgraph1) + const emptySignalSubgraph = await publishNewSubgraph(me, buildSubgraph(), gns) + await publishNewVersion(me, emptySignalSubgraph.id, newSubgraph1, gns, curation) }) it('should reject a new version with the same subgraph deployment ID', async function () { @@ -711,7 +467,7 @@ describe('GNS', () => { }) it('should upgrade version when there is no signal with no signal migration', async function () { - await burnSignal(me, subgraph.id) + await burnSignal(me, subgraph.id, gns, curation) const tx = gns .connect(me.signer) .publishNewVersion( @@ -725,7 +481,7 @@ describe('GNS', () => { }) it('should fail when subgraph is deprecated', async function () { - await deprecateSubgraph(me, subgraph.id) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) const tx = gns .connect(me.signer) .publishNewVersion( @@ -737,21 +493,54 @@ describe('GNS', () => { 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) + 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) + 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', async function () { let subgraph: Subgraph beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0) - await mintSignal(me, subgraph.id, tokens10000) + subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + await mintSignal(me, subgraph.id, tokens10000, gns, curation) }) it('should deprecate a subgraph', async function () { - await deprecateSubgraph(me, subgraph.id) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) }) it('should prevent a deprecated subgraph from being republished', async function () { - await deprecateSubgraph(me, subgraph.id) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) const tx = gns .connect(me.signer) .publishNewVersion( @@ -779,13 +568,13 @@ describe('GNS', () => { describe('Curating on names', async function () { describe('mintSignal()', async function () { it('should deposit into the name signal curve', async function () { - const subgraph = await publishNewSubgraph(me, newSubgraph0) - await mintSignal(other, subgraph.id, tokens10000) + const subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + await mintSignal(other, subgraph.id, tokens10000, gns, curation) }) it('should fail when name signal is disabled', async function () { - const subgraph = await publishNewSubgraph(me, newSubgraph0) - await deprecateSubgraph(me, subgraph.id) + const subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) const tx = gns.connect(me.signer).mintSignal(subgraph.id, tokens1000, 0) await expect(tx).revertedWith('GNS: Must be active') }) @@ -798,7 +587,7 @@ describe('GNS', () => { it('reject minting if under slippage', async function () { // First publish the subgraph - const subgraph = await publishNewSubgraph(me, newSubgraph0) + const subgraph = await publishNewSubgraph(me, newSubgraph0, gns) // Set slippage to be 1 less than expected result to force reverting const { 1: expectedNSignal } = await gns.tokensToNSignal(subgraph.id, tokens1000) @@ -813,16 +602,16 @@ describe('GNS', () => { let subgraph: Subgraph beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0) - await mintSignal(other, subgraph.id, tokens10000) + subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + await mintSignal(other, subgraph.id, tokens10000, gns, curation) }) it('should withdraw from the name signal curve', async function () { - await burnSignal(other, subgraph.id) + await burnSignal(other, subgraph.id, gns, curation) }) it('should fail when name signal is disabled', async function () { - await deprecateSubgraph(me, subgraph.id) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) // just test 1 since it will fail const tx = gns.connect(me.signer).burnSignal(subgraph.id, 1, 0) await expect(tx).revertedWith('GNS: Must be active') @@ -858,8 +647,8 @@ describe('GNS', () => { let otherNSignal: BigNumber beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0) - await mintSignal(other, subgraph.id, tokens10000) + subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + await mintSignal(other, subgraph.id, tokens10000, gns, curation) otherNSignal = await gns.getCuratorSignal(subgraph.id, other.address) }) @@ -873,7 +662,7 @@ describe('GNS', () => { 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) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) const tx = gns .connect(other.signer) .transferSignal(subgraph.id, another.address, otherNSignal) @@ -897,12 +686,12 @@ describe('GNS', () => { let subgraph: Subgraph beforeEach(async () => { - subgraph = await publishNewSubgraph(me, newSubgraph0) - await mintSignal(other, subgraph.id, tokens10000) + subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + await mintSignal(other, subgraph.id, tokens10000, gns, curation) }) it('should withdraw GRT from a disabled name signal', async function () { - await deprecateSubgraph(me, subgraph.id) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) await withdraw(other, subgraph.id) }) @@ -912,14 +701,14 @@ describe('GNS', () => { }) it('should fail when there is no more GRT to withdraw', async function () { - await deprecateSubgraph(me, subgraph.id) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) await withdraw(other, subgraph.id) const tx = gns.connect(other.signer).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) + await deprecateSubgraph(me, subgraph.id, gns, curation, grt) const tx = gns.connect(me.signer).withdraw(subgraph.id) await expect(tx).revertedWith('GNS: No signal to withdraw GRT') }) @@ -937,7 +726,7 @@ describe('GNS', () => { toGRT('2000'), toGRT('123'), ] - const subgraph = await publishNewSubgraph(me, newSubgraph0) + const subgraph = await publishNewSubgraph(me, newSubgraph0, gns) // State updated const curationTaxPercentage = await curation.curationTaxPercentage() @@ -950,11 +739,10 @@ describe('GNS', () => { const expectedNSignal = await calcGNSBondingCurve( beforeSubgraph.nSignal, beforeSubgraph.vSignal, - beforeSubgraph.reserveRatio, tokensToDeposit.sub(curationTax), beforeSubgraph.subgraphDeploymentID, ) - const tx = await mintSignal(me, subgraph.id, tokensToDeposit) + 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'] @@ -979,11 +767,11 @@ describe('GNS', () => { toGRT('1'), // should mint below minimum deposit ] - const subgraph = await publishNewSubgraph(me, newSubgraph0) + const subgraph = await publishNewSubgraph(me, newSubgraph0, gns) // State updated for (const tokensToDeposit of tokensToDepositMany) { - await mintSignal(me, subgraph.id, tokensToDeposit) + await mintSignal(me, subgraph.id, tokensToDeposit, gns, curation) } }) }) @@ -994,12 +782,12 @@ describe('GNS', () => { await curation.setMinimumCurationDeposit(toGRT('1')) // Publish a named subgraph-0 -> subgraphDeployment0 - const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns) // Curate on the first subgraph await gns.connect(me.signer).mintSignal(subgraph0.id, toGRT('90000'), 0) // Publish a named subgraph-1 -> subgraphDeployment0 - const subgraph1 = await publishNewSubgraph(me, newSubgraph0) + const subgraph1 = await publishNewSubgraph(me, newSubgraph0, gns) // Curate on the second subgraph should work await gns.connect(me.signer).mintSignal(subgraph1.id, toGRT('10'), 0) }) @@ -1014,7 +802,7 @@ describe('GNS', () => { newSubgraph0.subgraphMetadata, ) // Curate on the subgraph - const subgraphID = buildSubgraphID(me.address, await gns.nextAccountSeqID(me.address)) + const subgraphID = await buildSubgraphID(me.address, await gns.nextAccountSeqID(me.address)) const tx2 = await gns.populateTransaction.mintSignal(subgraphID, toGRT('90000'), 0) // Batch send transaction @@ -1039,7 +827,7 @@ describe('GNS', () => { 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, me.address) + const tx1 = await gns.populateTransaction.initialize(me.address, me.address) // Create a subgraph const tx2 = await gns.populateTransaction.publishNewSubgraph( @@ -1076,8 +864,20 @@ describe('GNS', () => { }) describe('NFT descriptor', function () { + it('cannot be minted by an account that is not the minter (i.e. GNS)', async function () { + const subgraphNFTAddress = await gns.subgraphNFT() + const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT + const tx = subgraphNFT.connect(me.signer).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 subgraphNFTAddress = await gns.subgraphNFT() + const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT + const tx = subgraphNFT.connect(me.signer).burn(1) + await expect(tx).revertedWith('Must be a minter') + }) it('with token descriptor', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns) const subgraphNFTAddress = await gns.subgraphNFT() const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT @@ -1088,7 +888,7 @@ describe('GNS', () => { }) it('with token descriptor and baseURI', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns) const subgraphNFTAddress = await gns.subgraphNFT() const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT @@ -1100,7 +900,7 @@ describe('GNS', () => { }) it('without token descriptor', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns) const subgraphNFTAddress = await gns.subgraphNFT() const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT @@ -1112,7 +912,7 @@ describe('GNS', () => { }) it('without token descriptor and baseURI', async function () { - const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns) const subgraphNFTAddress = await gns.subgraphNFT() const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT @@ -1127,7 +927,7 @@ describe('GNS', () => { it('without token descriptor and 0x0 metadata', async function () { const newSubgraphNoMetadata = buildSubgraph() newSubgraphNoMetadata.subgraphMetadata = HashZero - const subgraph0 = await publishNewSubgraph(me, newSubgraphNoMetadata) + const subgraph0 = await publishNewSubgraph(me, newSubgraphNoMetadata, gns) const subgraphNFTAddress = await gns.subgraphNFT() const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT @@ -1137,4 +937,744 @@ describe('GNS', () => { 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.signer) + .createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) + const tx = legacyGNSMock + .connect(me.signer) + .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 = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT + const tokenURI = await subgraphNFT.connect(me.signer).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.signer) + .createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) + let tx = legacyGNSMock + .connect(me.signer) + .migrateLegacySubgraph(me.address, seqID, newSubgraph0.subgraphMetadata) + await expect(tx).emit(legacyGNSMock, ' LegacySubgraphClaimed').withArgs(me.address, seqID) + tx = legacyGNSMock + .connect(me.signer) + .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.signer) + .createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) + await legacyGNSMock + .connect(me.signer) + .migrateLegacySubgraph(me.address, seqID, newSubgraph0.subgraphMetadata) + + expect(await legacyGNSMock.isLegacySubgraph(subgraphId)).eq(true) + + const subgraph0 = await publishNewSubgraph(me, newSubgraph0, legacyGNSMock) + 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.signer) + .createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) + await legacyGNSMock + .connect(me.signer) + .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) + 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) + // Curate on the subgraph + await gns.connect(me.signer).mintSignal(subgraph0.id, toGRT('90000'), 0) + // Add an additional curator that is not the owner + await gns.connect(other.signer).mintSignal(subgraph0.id, toGRT('10000'), 0) + return subgraph0 + } + + const publishCurateAndSendSubgraph = async function ( + beforeTransferCallback?: (subgraphID: string) => Promise, + ): Promise { + const subgraph0 = await publishAndCurateOnSubgraph() + + if (beforeTransferCallback != null) { + 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.signer) + .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.signer) + .createLegacySubgraph(seqID, newSubgraph0.subgraphDeploymentID) + // The legacy subgraph must be claimed + const migrateTx = legacyGNSMock + .connect(me.signer) + .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.signer).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.signer) + .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, + mockL2GNS.address, + expectedSentToL2, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(gns.address, mockL2Gateway.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.signer) + .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, + mockL2GNS.address, + expectedSentToL2, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(legacyGNSMock.address, mockL2Gateway.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.signer) + .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.signer) + .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.signer) + .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.signer).deprecateSubgraph(subgraph0.id) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + const tx = gns + .connect(me.signer) + .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)) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + const tx = gns + .connect(me.signer) + .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.signer) + .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.signer) + .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.signer).burnSignal(subgraph0.id, toBN(1), toGRT('0')) + await expect(tx2).revertedWith('GNS: Must be active') + const tx3 = gns.connect(other.signer).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.signer) + .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.signer).transferSignal(subgraph0.id, other.address, toBN(1)) + await expect(tx2).revertedWith('GNS: Must be active') + const tx3 = gns.connect(other.signer).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.signer) + .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.signer).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.signer) + .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.signer).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, + mockL2GNS.address, + curatorTokens, + expectedCallhookData, + ) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + + const tx = gns + .connect(other.signer) + .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, mockL2Gateway.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, + mockL2GNS.address, + curatorTokens, + expectedCallhookData, + ) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + + const tx = gns + .connect(other.signer) + .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, mockL2Gateway.address, toBN('2'), expectedL2Data) + + const tx2 = gns + .connect(other.signer) + .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 () { + 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.signer).mintSignal(_subgraphId, toGRT('1000'), toBN(0)) + }) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + + await gns + .connect(other.signer) + .sendCuratorBalanceToBeneficiaryOnL2( + subgraph0.id, + other.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + { + value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), + }, + ) + + const tx = gns.connect(other.signer).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 () { + let beforeOtherNSignal: BigNumber + let beforeAnotherNSignal: BigNumber + const subgraph0 = await publishCurateAndSendSubgraph(async (subgraphID) => { + beforeOtherNSignal = await gns.getCuratorSignal(subgraphID, other.address) + await gns.connect(another.signer).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, + mockL2GNS.address, + curator1Tokens, + expectedCallhookData1, + ) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + + const tx = gns + .connect(other.signer) + .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, mockL2Gateway.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, + mockL2GNS.address, + curator2TokensUpdated, + expectedCallhookData2, + ) + const tx2 = gns + .connect(another.signer) + .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, mockL2Gateway.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.signer) + .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 advanceBlocks(256) + await gns.connect(me.signer).deprecateSubgraph(subgraph0.id) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + + const tx = gns + .connect(me.signer) + .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.signer) + .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.signer) + .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() + const afterSubgraph = await gns.subgraphs(subgraph0.id) + + await gns.connect(other.signer).withdraw(subgraph0.id) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + + const tx = gns + .connect(other.signer) + .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/test/l2/l2Curation.test.ts b/test/l2/l2Curation.test.ts new file mode 100644 index 000000000..b7732ebd0 --- /dev/null +++ b/test/l2/l2Curation.test.ts @@ -0,0 +1,781 @@ +import { expect } from 'chai' +import { utils, BigNumber, Event, Signer, constants } from 'ethers' + +import { L2Curation } from '../../build/types/L2Curation' +import { GraphToken } from '../../build/types/GraphToken' +import { Controller } from '../../build/types/Controller' +import { defaults } from '../lib/deployment' + +import { NetworkFixture } from '../lib/fixtures' +import { + getAccounts, + randomHexBytes, + toBN, + toGRT, + formatGRT, + Account, + impersonateAccount, + setAccountBalance, + randomAddress, +} from '../lib/testHelpers' +import { GNS } from '../../build/types/GNS' +import { parseEther, toUtf8String } from 'ethers/lib/utils' + +const { AddressZero } = constants + +const MAX_PPM = 1000000 + +const chunkify = (total: BigNumber, maxChunks = 10): Array => { + const chunks = [] + while (total.gt(0) && maxChunks > 0) { + const m = 1000000 + const p = Math.floor(Math.random() * m) + const n = total.mul(p).div(m) + chunks.push(n) + total = total.sub(n) + maxChunks-- + } + if (total.gt(0)) { + chunks.push(total) + } + return chunks +} + +const toFloat = (n: BigNumber) => parseFloat(formatGRT(n)) +const toRound = (n: number) => n.toPrecision(11) + +describe('L2Curation:Config', () => { + let me: Account + let governor: Account + + let fixture: NetworkFixture + + let curation: L2Curation + + before(async function () { + ;[me, governor] = await getAccounts() + + fixture = new NetworkFixture() + ;({ curation } = await fixture.loadL2(governor.signer)) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('defaultReserveRatio', function () { + it('should be fixed to MAX_PPM', async function () { + // Set right in the constructor + expect(await curation.defaultReserveRatio()).eq(MAX_PPM) + }) + it('cannot be changed because the setter is not implemented', async function () { + const tx = curation.connect(governor.signer).setDefaultReserveRatio(10) + await expect(tx).revertedWith('Not implemented in L2') + }) + }) + + describe('minimumCurationDeposit', function () { + it('should set `minimumCurationDeposit`', async function () { + // Set right in the constructor + expect(await curation.minimumCurationDeposit()).eq(defaults.curation.l2MinimumCurationDeposit) + + // Can set if allowed + const newValue = toBN('100') + await curation.connect(governor.signer).setMinimumCurationDeposit(newValue) + expect(await curation.minimumCurationDeposit()).eq(newValue) + }) + + it('reject set `minimumCurationDeposit` if out of bounds', async function () { + const tx = curation.connect(governor.signer).setMinimumCurationDeposit(0) + await expect(tx).revertedWith('Minimum curation deposit cannot be 0') + }) + + it('reject set `minimumCurationDeposit` if not allowed', async function () { + const tx = curation + .connect(me.signer) + .setMinimumCurationDeposit(defaults.curation.minimumCurationDeposit) + await expect(tx).revertedWith('Only Controller governor') + }) + }) + + describe('curationTaxPercentage', function () { + it('should set `curationTaxPercentage`', async function () { + const curationTaxPercentage = defaults.curation.curationTaxPercentage + + // Set new value + await curation.connect(governor.signer).setCurationTaxPercentage(0) + await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) + }) + + it('reject set `curationTaxPercentage` if out of bounds', async function () { + const tx = curation.connect(governor.signer).setCurationTaxPercentage(MAX_PPM + 1) + await expect(tx).revertedWith('Curation tax percentage must be below or equal to MAX_PPM') + }) + + it('reject set `curationTaxPercentage` if not allowed', async function () { + const tx = curation.connect(me.signer).setCurationTaxPercentage(0) + await expect(tx).revertedWith('Only Controller governor') + }) + }) + + describe('curationTokenMaster', function () { + it('should set `curationTokenMaster`', async function () { + const newCurationTokenMaster = curation.address + await curation.connect(governor.signer).setCurationTokenMaster(newCurationTokenMaster) + }) + + it('reject set `curationTokenMaster` to empty value', async function () { + const newCurationTokenMaster = AddressZero + const tx = curation.connect(governor.signer).setCurationTokenMaster(newCurationTokenMaster) + await expect(tx).revertedWith('Token master must be non-empty') + }) + + it('reject set `curationTokenMaster` to non-contract', async function () { + const newCurationTokenMaster = randomAddress() + const tx = curation.connect(governor.signer).setCurationTokenMaster(newCurationTokenMaster) + await expect(tx).revertedWith('Token master must be a contract') + }) + + it('reject set `curationTokenMaster` if not allowed', async function () { + const newCurationTokenMaster = curation.address + const tx = curation.connect(me.signer).setCurationTokenMaster(newCurationTokenMaster) + await expect(tx).revertedWith('Only Controller governor') + }) + }) +}) + +describe('L2Curation', () => { + let me: Account + let governor: Account + let curator: Account + let stakingMock: Account + let gnsImpersonator: Signer + + let fixture: NetworkFixture + + let curation: L2Curation + let grt: GraphToken + let controller: Controller + let gns: GNS + + // Test values + const signalAmountFor1000Tokens = toGRT('1000.0') + const signalAmountForMinimumCuration = toBN('1') + const subgraphDeploymentID = randomHexBytes() + const curatorTokens = toGRT('1000000000') + const tokensToDeposit = toGRT('1000') + const tokensToCollect = toGRT('2000') + + async function calcLinearBondingCurve( + supply: BigNumber, + reserveBalance: BigNumber, + depositAmount: BigNumber, + ): Promise { + // Handle the initialization of the bonding curve + if (supply.eq(0)) { + const minDeposit = await curation.minimumCurationDeposit() + if (depositAmount.lt(minDeposit)) { + throw new Error('deposit must be above minimum') + } + const minSupply = signalAmountForMinimumCuration + return ( + (await calcLinearBondingCurve(minSupply, minDeposit, depositAmount.sub(minDeposit))) + + toFloat(minSupply) + ) + } + // Calculate bonding curve in the test + return toFloat(supply) * (toFloat(depositAmount) / toFloat(reserveBalance)) + } + + const shouldMint = async (tokensToDeposit: BigNumber, expectedSignal: BigNumber) => { + // Before state + const beforeTokenTotalSupply = await grt.totalSupply() + const beforeCuratorTokens = await grt.balanceOf(curator.address) + const beforeCuratorSignal = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const beforePool = await curation.pools(subgraphDeploymentID) + const beforePoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const beforeTotalTokens = await grt.balanceOf(curation.address) + + // Calculations + const curationTaxPercentage = await curation.curationTaxPercentage() + const curationTax = tokensToDeposit.mul(toBN(curationTaxPercentage)).div(toBN(MAX_PPM)) + + // Curate + const tx = curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + await expect(tx) + .emit(curation, 'Signalled') + .withArgs(curator.address, subgraphDeploymentID, tokensToDeposit, expectedSignal, curationTax) + + // After state + const afterTokenTotalSupply = await grt.totalSupply() + const afterCuratorTokens = await grt.balanceOf(curator.address) + const afterCuratorSignal = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const afterPool = await curation.pools(subgraphDeploymentID) + const afterPoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const afterTotalTokens = await grt.balanceOf(curation.address) + + // Curator balance updated + expect(afterCuratorTokens).eq(beforeCuratorTokens.sub(tokensToDeposit)) + expect(afterCuratorSignal).eq(beforeCuratorSignal.add(expectedSignal)) + // Allocated and balance updated + expect(afterPool.tokens).eq(beforePool.tokens.add(tokensToDeposit.sub(curationTax))) + expect(afterPoolSignal).eq(beforePoolSignal.add(expectedSignal)) + // Pool reserveRatio is deprecated and therefore always zero in L2 + expect(afterPool.reserveRatio).eq(0) + // Contract balance updated + expect(afterTotalTokens).eq(beforeTotalTokens.add(tokensToDeposit.sub(curationTax))) + // Total supply is reduced to curation tax burning + expect(afterTokenTotalSupply).eq(beforeTokenTotalSupply.sub(curationTax)) + } + + const shouldMintTaxFree = async (tokensToDeposit: BigNumber, expectedSignal: BigNumber) => { + // Before state + const beforeTokenTotalSupply = await grt.totalSupply() + const beforeCuratorTokens = await grt.balanceOf(gns.address) + const beforeCuratorSignal = await curation.getCuratorSignal(gns.address, subgraphDeploymentID) + const beforePool = await curation.pools(subgraphDeploymentID) + const beforePoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const beforeTotalTokens = await grt.balanceOf(curation.address) + + // Curate + const tx = curation.connect(gnsImpersonator).mintTaxFree(subgraphDeploymentID, tokensToDeposit) + await expect(tx) + .emit(curation, 'Signalled') + .withArgs(gns.address, subgraphDeploymentID, tokensToDeposit, expectedSignal, 0) + + // After state + const afterTokenTotalSupply = await grt.totalSupply() + const afterCuratorTokens = await grt.balanceOf(gns.address) + const afterCuratorSignal = await curation.getCuratorSignal(gns.address, subgraphDeploymentID) + const afterPool = await curation.pools(subgraphDeploymentID) + const afterPoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const afterTotalTokens = await grt.balanceOf(curation.address) + + // Curator balance updated + expect(afterCuratorTokens).eq(beforeCuratorTokens.sub(tokensToDeposit)) + expect(afterCuratorSignal).eq(beforeCuratorSignal.add(expectedSignal)) + // Allocated and balance updated + expect(afterPool.tokens).eq(beforePool.tokens.add(tokensToDeposit)) + expect(afterPoolSignal).eq(beforePoolSignal.add(expectedSignal)) + // Pool reserveRatio is deprecated and therefore always zero in L2 + expect(afterPool.reserveRatio).eq(0) + // Contract balance updated + expect(afterTotalTokens).eq(beforeTotalTokens.add(tokensToDeposit)) + // Total supply is reduced to curation tax burning + expect(afterTokenTotalSupply).eq(beforeTokenTotalSupply) + } + + const shouldBurn = async (signalToRedeem: BigNumber, expectedTokens: BigNumber) => { + // Before balances + const beforeTokenTotalSupply = await grt.totalSupply() + const beforeCuratorTokens = await grt.balanceOf(curator.address) + const beforeCuratorSignal = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const beforePool = await curation.pools(subgraphDeploymentID) + const beforePoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const beforeTotalTokens = await grt.balanceOf(curation.address) + + // Redeem + const tx = curation.connect(curator.signer).burn(subgraphDeploymentID, signalToRedeem, 0) + await expect(tx) + .emit(curation, 'Burned') + .withArgs(curator.address, subgraphDeploymentID, expectedTokens, signalToRedeem) + + // After balances + const afterTokenTotalSupply = await grt.totalSupply() + const afterCuratorTokens = await grt.balanceOf(curator.address) + const afterCuratorSignal = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const afterPool = await curation.pools(subgraphDeploymentID) + const afterPoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const afterTotalTokens = await grt.balanceOf(curation.address) + + // Curator balance updated + expect(afterCuratorTokens).eq(beforeCuratorTokens.add(expectedTokens)) + expect(afterCuratorSignal).eq(beforeCuratorSignal.sub(signalToRedeem)) + // Curation balance updated + expect(afterPool.tokens).eq(beforePool.tokens.sub(expectedTokens)) + expect(afterPoolSignal).eq(beforePoolSignal.sub(signalToRedeem)) + // Contract balance updated + expect(afterTotalTokens).eq(beforeTotalTokens.sub(expectedTokens)) + // Total supply is conserved + expect(afterTokenTotalSupply).eq(beforeTokenTotalSupply) + } + + const shouldCollect = async (tokensToCollect: BigNumber) => { + // Before state + const beforePool = await curation.pools(subgraphDeploymentID) + const beforeTotalBalance = await grt.balanceOf(curation.address) + + // Source of tokens must be the staking for this to work + await grt.connect(stakingMock.signer).transfer(curation.address, tokensToCollect) + const tx = curation.connect(stakingMock.signer).collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).emit(curation, 'Collected').withArgs(subgraphDeploymentID, tokensToCollect) + + // After state + const afterPool = await curation.pools(subgraphDeploymentID) + const afterTotalBalance = await grt.balanceOf(curation.address) + + // State updated + expect(afterPool.tokens).eq(beforePool.tokens.add(tokensToCollect)) + expect(afterTotalBalance).eq(beforeTotalBalance.add(tokensToCollect)) + } + + before(async function () { + // Use stakingMock so we can call collect + ;[me, governor, curator, stakingMock] = await getAccounts() + + fixture = new NetworkFixture() + ;({ controller, curation, grt, gns } = await fixture.loadL2(governor.signer)) + + gnsImpersonator = await impersonateAccount(gns.address) + await setAccountBalance(gns.address, parseEther('1')) + // Give some funds to the curator and GNS impersonator and approve the curation contract + await grt.connect(governor.signer).mint(curator.address, curatorTokens) + await grt.connect(curator.signer).approve(curation.address, curatorTokens) + await grt.connect(governor.signer).mint(gns.address, curatorTokens) + await grt.connect(gnsImpersonator).approve(curation.address, curatorTokens) + + // Give some funds to the staking contract and approve the curation contract + await grt.connect(governor.signer).mint(stakingMock.address, tokensToCollect) + await grt.connect(stakingMock.signer).approve(curation.address, tokensToCollect) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('bonding curve', function () { + const tokensToDeposit = curatorTokens + + it('reject convert signal to tokens if subgraph deployment not initted', async function () { + const tx = curation.signalToTokens(subgraphDeploymentID, toGRT('100')) + await expect(tx).revertedWith('Subgraph deployment must be curated to perform calculations') + }) + + it('convert signal to tokens', async function () { + // Curate + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Conversion + const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signal) + expect(expectedTokens).eq(tokensToDeposit) + }) + + it('convert signal to tokens (with curation tax)', async function () { + // Set curation tax + const curationTaxPercentage = 50000 // 5% + await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) + + // Curate + const expectedCurationTax = tokensToDeposit.mul(curationTaxPercentage).div(MAX_PPM) + const { 1: curationTax } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Conversion + const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const tokens = await curation.signalToTokens(subgraphDeploymentID, signal) + expect(tokens).eq(tokensToDeposit.sub(expectedCurationTax)) + expect(expectedCurationTax).eq(curationTax) + }) + + it('convert tokens to signal', async function () { + // Conversion + const tokens = toGRT('1000') + const { 0: signal } = await curation.tokensToSignal(subgraphDeploymentID, tokens) + expect(signal).eq(signalAmountFor1000Tokens) + }) + + it('convert tokens to signal if non-curated subgraph', async function () { + // Conversion + const nonCuratedSubgraphDeploymentID = randomHexBytes() + const tokens = toGRT('0') + const tx = curation.tokensToSignal(nonCuratedSubgraphDeploymentID, tokens) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) + }) + + describe('curate', async function () { + it('reject deposit below minimum tokens required', async function () { + // Set the minimum to a value greater than 1 so that we can test + await curation.connect(governor.signer).setMinimumCurationDeposit(toBN('2')) + const tokensToDeposit = (await curation.minimumCurationDeposit()).sub(toBN(1)) + const tx = curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) + + it('should deposit on a subgraph deployment', async function () { + const tokensToDeposit = await curation.minimumCurationDeposit() + const expectedSignal = signalAmountForMinimumCuration // tax = 0 due to rounding + await shouldMint(tokensToDeposit, expectedSignal) + }) + + it('should get signal according to bonding curve', async function () { + const tokensToDeposit = toGRT('1000') + const expectedSignal = signalAmountFor1000Tokens + await shouldMint(tokensToDeposit, expectedSignal) + }) + + it('should get signal according to bonding curve (and account for curation tax)', async function () { + // Set curation tax + await curation.connect(governor.signer).setCurationTaxPercentage(50000) // 5% + + // Mint + const tokensToDeposit = toGRT('1000') + const { 0: expectedSignal } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await shouldMint(tokensToDeposit, expectedSignal) + }) + + it('should revert curate if over slippage', async function () { + const tokensToDeposit = toGRT('1000') + const expectedSignal = signalAmountFor1000Tokens + const tx = curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, expectedSignal.add(1)) + await expect(tx).revertedWith('Slippage protection') + }) + }) + + describe('curate tax free (from GNS)', async function () { + it('can not be called by anyone other than GNS', async function () { + const tokensToDeposit = await curation.minimumCurationDeposit() + const tx = curation.connect(curator.signer).mintTaxFree(subgraphDeploymentID, tokensToDeposit) + await expect(tx).revertedWith('Only the GNS can call this') + }) + + it('reject deposit below minimum tokens required', async function () { + // Set the minimum to a value greater than 1 so that we can test + await curation.connect(governor.signer).setMinimumCurationDeposit(toBN('2')) + const tokensToDeposit = (await curation.minimumCurationDeposit()).sub(toBN(1)) + const tx = curation + .connect(gnsImpersonator) + .mintTaxFree(subgraphDeploymentID, tokensToDeposit) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) + + it('should deposit on a subgraph deployment', async function () { + const tokensToDeposit = await curation.minimumCurationDeposit() + const expectedSignal = signalAmountForMinimumCuration + await shouldMintTaxFree(tokensToDeposit, expectedSignal) + }) + + it('should get signal according to bonding curve', async function () { + const tokensToDeposit = toGRT('1000') + const expectedSignal = signalAmountFor1000Tokens + await shouldMintTaxFree(tokensToDeposit, expectedSignal) + }) + + it('should get signal according to bonding curve (and with zero tax)', async function () { + // Set curation tax + await curation.connect(governor.signer).setCurationTaxPercentage(50000) // 5% + + // Mint + const tokensToDeposit = toGRT('1000') + const expectedSignal = await curation.tokensToSignalNoTax( + subgraphDeploymentID, + tokensToDeposit, + ) + await shouldMintTaxFree(tokensToDeposit, expectedSignal) + }) + }) + + describe('collect', async function () { + context('> not curated', async function () { + it('reject collect tokens distributed to the curation pool', async function () { + // Source of tokens must be the staking for this to work + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + const tx = curation + .connect(stakingMock.signer) + .collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).revertedWith('Subgraph deployment must be curated to collect fees') + }) + }) + + context('> curated', async function () { + beforeEach(async function () { + await curation.connect(curator.signer).mint(subgraphDeploymentID, toGRT('1000'), 0) + }) + + it('reject collect tokens distributed from invalid address', async function () { + const tx = curation.connect(me.signer).collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).revertedWith('Caller must be the staking contract') + }) + + it('should collect tokens distributed to the curation pool', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + await shouldCollect(toGRT('1')) + await shouldCollect(toGRT('10')) + await shouldCollect(toGRT('100')) + await shouldCollect(toGRT('200')) + await shouldCollect(toGRT('500.25')) + }) + + it('should collect tokens and then unsignal all', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + // Collect increase the pool reserves + await shouldCollect(toGRT('100')) + + // When we burn signal we should get more tokens than initially curated + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + await shouldBurn(signalToRedeem, toGRT('1100')) + }) + + it('should collect tokens and then unsignal multiple times', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + // Collect increase the pool reserves + const tokensToCollect = toGRT('100') + await shouldCollect(tokensToCollect) + + // Unsignal partially + const signalOutRemainder = toGRT(1) + const signalOutPartial = ( + await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + ).sub(signalOutRemainder) + const tx1 = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalOutPartial, 0) + const r1 = await tx1.wait() + const event1 = curation.interface.parseLog(r1.events[2]).args + const tokensOut1 = event1.tokens + + // Collect increase the pool reserves + await shouldCollect(tokensToCollect) + + // Unsignal the rest + const tx2 = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalOutRemainder, 0) + const r2 = await tx2.wait() + const event2 = curation.interface.parseLog(r2.events[2]).args + const tokensOut2 = event2.tokens + + expect(tokensOut1.add(tokensOut2)).eq(toGRT('1000').add(tokensToCollect.mul(2))) + }) + }) + }) + + describe('burn', async function () { + beforeEach(async function () { + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + }) + + it('reject redeem more than a curator owns', async function () { + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('1'), 0) + await expect(tx).revertedWith('Cannot burn more signal than you own') + }) + + it('reject redeem zero signal', async function () { + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('0'), 0) + await expect(tx).revertedWith('Cannot burn zero signal') + }) + + it('should allow to redeem *partially*', async function () { + // Redeem just one signal + const signalToRedeem = toGRT('1') + const expectedTokens = toGRT('1') + await shouldBurn(signalToRedeem, expectedTokens) + }) + + it('should allow to redeem *fully*', async function () { + // Get all signal of the curator + const signalToRedeem = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + const expectedTokens = tokensToDeposit + await shouldBurn(signalToRedeem, expectedTokens) + }) + + it('should allow to redeem back below minimum deposit', async function () { + // Set the minimum to a value greater than 1 so that we can test + await curation.connect(governor.signer).setMinimumCurationDeposit(toGRT('1')) + + // Redeem "almost" all signal + const signal = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + const signalToRedeem = signal.sub(toGRT('0.000001')) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signalToRedeem) + await shouldBurn(signalToRedeem, expectedTokens) + + // The pool should have less tokens that required by minimumCurationDeposit + const afterPool = await curation.pools(subgraphDeploymentID) + expect(afterPool.tokens).lt(await curation.minimumCurationDeposit()) + + // Should be able to deposit more after being under minimumCurationDeposit + const tokensToDeposit = toGRT('1') + const { 0: expectedSignal } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await shouldMint(tokensToDeposit, expectedSignal) + }) + + it('should revert redeem if over slippage', async function () { + const signalToRedeem = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + const expectedTokens = tokensToDeposit + + const tx = curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalToRedeem, expectedTokens.add(1)) + await expect(tx).revertedWith('Slippage protection') + }) + + it('should not re-deploy the curation token when signal is reset', async function () { + const beforeSubgraphPool = await curation.pools(subgraphDeploymentID) + + // Burn all the signal + const signalToRedeem = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + const expectedTokens = tokensToDeposit + await shouldBurn(signalToRedeem, expectedTokens) + + // Mint again on the same subgraph + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Check state + const afterSubgraphPool = await curation.pools(subgraphDeploymentID) + expect(afterSubgraphPool.gcs).eq(beforeSubgraphPool.gcs) + }) + }) + + describe('conservation', async function () { + it('should match multiple deposits and redeems back to initial state', async function () { + this.timeout(60000) // increase timeout for test runner + + const totalDeposits = toGRT('1000000000') + + // Signal multiple times + let totalSignal = toGRT('0') + for (const tokensToDeposit of chunkify(totalDeposits, 10)) { + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + totalSignal = totalSignal.add(signal) + } + + // Redeem signal multiple times + let totalTokens = toGRT('0') + for (const signalToRedeem of chunkify(totalSignal, 10)) { + const tx = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalToRedeem, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const tokens = event.args['tokens'] + totalTokens = totalTokens.add(tokens) + // console.log('<', formatEther(signalToRedeem), '=', formatEther(tokens)) + } + + // Conservation of work + const afterPool = await curation.pools(subgraphDeploymentID) + const afterPoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + expect(afterPool.tokens).eq(toGRT('0')) + expect(afterPoolSignal).eq(toGRT('0')) + expect(await curation.isCurated(subgraphDeploymentID)).eq(false) + expect(totalDeposits).eq(totalTokens) + }) + }) + + describe('multiple minting', async function () { + it('should mint the same signal every time due to the flat 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 the same GCS due to bonding curve! + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + for (const tokensToDeposit of tokensToDepositMany) { + const expectedSignal = await calcLinearBondingCurve( + await curation.getCurationPoolSignal(subgraphDeploymentID), + await curation.getCurationPoolTokens(subgraphDeploymentID), + tokensToDeposit, + ) + // SIGNAL_PER_MINIMUM_DEPOSIT should always give the same ratio + expect(tokensToDeposit.div(toGRT(expectedSignal))).eq(1) + + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + expect(toRound(toFloat(toBN(signal)))).eq(toRound(expectedSignal)) + } + }) + + it('should mint when using a different ratio between GRT and signal', async function () { + this.timeout(60000) // increase timeout for test runner + + // Setup edge case with 1 GRT = 1 wei signal + await curation.setMinimumCurationDeposit(toGRT('1')) + + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint proportionally the same GCS due to linear bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), + ] + + // Mint multiple times + for (const tokensToDeposit of tokensToDepositMany) { + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + expect(tokensToDeposit).eq(signal.mul(toGRT('1'))) // we compare 1 GRT : 1 wei ratio + } + }) + }) +}) diff --git a/test/l2/l2GNS.test.ts b/test/l2/l2GNS.test.ts new file mode 100644 index 000000000..4db26c13d --- /dev/null +++ b/test/l2/l2GNS.test.ts @@ -0,0 +1,811 @@ +import { expect } from 'chai' +import { ethers, ContractTransaction, BigNumber } from 'ethers' +import { defaultAbiCoder, parseEther } from 'ethers/lib/utils' + +import { + getAccounts, + randomHexBytes, + Account, + toGRT, + getL2SignerFromL1, + setAccountBalance, + latestBlock, +} from '../lib/testHelpers' +import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' +import { toBN } from '../lib/testHelpers' + +import { L2GNS } from '../../build/types/L2GNS' +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { + buildSubgraph, + buildSubgraphID, + burnSignal, + DEFAULT_RESERVE_RATIO, + deprecateSubgraph, + mintSignal, + publishNewSubgraph, + publishNewVersion, + PublishSubgraph, + Subgraph, +} from '../lib/gnsUtils' +import { L2Curation } from '../../build/types/L2Curation' +import { GraphToken } from '../../build/types/GraphToken' + +const { HashZero } = ethers.constants + +interface L1SubgraphParams { + l1SubgraphId: string + curatedTokens: BigNumber + subgraphMetadata: string + versionMetadata: string +} + +describe('L2GNS', () => { + let me: Account + let other: Account + let governor: Account + let mockRouter: Account + let mockL1GRT: Account + let mockL1Gateway: Account + let mockL1GNS: Account + let fixture: NetworkFixture + + let fixtureContracts: L2FixtureContracts + let l2GraphTokenGateway: L2GraphTokenGateway + let gns: L2GNS + let curation: L2Curation + let grt: GraphToken + + 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 mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) + // Eth for gas: + await setAccountBalance(await mockL1GatewayL2Alias.getAddress(), parseEther('1')) + + const tx = l2GraphTokenGateway + .connect(mockL1GatewayL2Alias) + .finalizeInboundTransfer(mockL1GRT.address, from, to, amount, callhookData) + return tx + } + + const defaultL1SubgraphParams = async function (): Promise { + return { + l1SubgraphId: await buildSubgraphID(me.address, toBN('1'), 1), + 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(mockL1GNS.address, gns.address, curatedTokens, callhookData) + + const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) + await gns + .connect(me.signer) + .finishSubgraphTransferFromL1( + l2SubgraphId, + newSubgraph0.subgraphDeploymentID, + subgraphMetadata, + versionMetadata, + ) + } + + before(async function () { + newSubgraph0 = buildSubgraph() + ;[me, other, governor, mockRouter, mockL1GRT, mockL1Gateway, mockL1GNS] = await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.loadL2(governor.signer) + ;({ l2GraphTokenGateway, gns, curation, grt } = fixtureContracts) + + await grt.connect(governor.signer).mint(me.address, toGRT('10000')) + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + mockL1GNS.address, + ) + }) + + 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', async 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.signer).mint(me.address, tokens100000) + await grt.connect(governor.signer).mint(other.address, tokens100000) + await grt.connect(me.signer).approve(gns.address, tokens100000) + await grt.connect(me.signer).approve(curation.address, tokens100000) + await grt.connect(other.signer).approve(gns.address, tokens100000) + await grt.connect(other.signer).approve(curation.address, tokens100000) + // Update curation tax to test the functionality of it in disableNameSignal() + await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) + subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + 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 with no current signal', async function () { + const emptySignalSubgraph = await publishNewSubgraph(me, buildSubgraph(), gns) + 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.signer) + .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.signer) + .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.signer) + .publishNewVersion( + subgraph.id, + newSubgraph1.subgraphDeploymentID, + newSubgraph1.versionMetadata, + ) + 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.signer).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.signer) + .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.signer) + .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('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.signer) + .onTokenTransfer(mockL1GNS.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( + mockL1GNS.address, + gns.address, + curatedTokens, + callhookData, + ) + + const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1GNS.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 latestBlock()) + + expect(subgraphData.vSignal).eq(0) + expect(subgraphData.nSignal).eq(0) + expect(subgraphData.subgraphDeploymentID).eq(HashZero) + expect(subgraphData.reserveRatioDeprecated).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) + + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], + ) + const tx = gatewayFinalizeTransfer( + mockL1GNS.address, + gns.address, + curatedTokens, + callhookData, + ) + + const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1GNS.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 latestBlock()) + + expect(subgraphData.vSignal).eq(0) + expect(subgraphData.nSignal).eq(0) + expect(subgraphData.subgraphDeploymentID).eq(HashZero) + expect(subgraphData.reserveRatioDeprecated).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.reserveRatioDeprecated).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(mockL1GNS.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.signer) + .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) + }) + 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(mockL1GNS.address, gns.address, curatedTokens, callhookData) + const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) + const tx = gns + .connect(other.signer) + .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'), 1) + const metadata = randomHexBytes() + const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) + const tx = gns + .connect(me.signer) + .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) + const metadata = randomHexBytes() + + const tx = gns + .connect(me.signer) + .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(mockL1GNS.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.signer).approve(curation.address, toGRT('100')) + await curation + .connect(me.signer) + .mint(newSubgraph0.subgraphDeploymentID, toGRT('100'), toBN('0')) + + expect(await curation.getCurationPoolTokens(newSubgraph0.subgraphDeploymentID)).eq( + toGRT('100'), + ) + const tx = gns + .connect(me.signer) + .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(mockL1GNS.address, gns.address, curatedTokens, callhookData) + const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) + const tx = gns + .connect(me.signer) + .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(mockL1GNS.address, gns.address, curatedTokens, callhookData) + const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) + await gns + .connect(me.signer) + .finishSubgraphTransferFromL1( + l2SubgraphId, + newSubgraph0.subgraphDeploymentID, + metadata, + metadata, + ) + + const tx = gns + .connect(me.signer) + .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 mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) + // Eth for gas: + await setAccountBalance(await mockL1GNSL2Alias.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( + mockL1GNS.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 mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) + // Eth for gas: + await setAccountBalance(await mockL1GNSL2Alias.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.signer).mint(other.address, toGRT('10')) + await grt.connect(other.signer).approve(gns.address, toGRT('10')) + await gns.connect(other.signer).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( + mockL1GNS.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.signer).onTokenTransfer(mockL1GNS.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 mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) + // Eth for gas: + await setAccountBalance(await mockL1GNSL2Alias.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(mockL1GNS.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 mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) + // Eth for gas: + await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) + + const l2Subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + + 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(mockL1GNS.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 mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) + // Eth for gas: + await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) + + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() + const callhookDataSG = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], + ) + await gatewayFinalizeTransfer(mockL1GNS.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(mockL1GNS.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 mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) + // Eth for gas: + await setAccountBalance(await mockL1GNSL2Alias.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.signer).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(mockL1GNS.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(mockL1GNS.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/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index 236817afd..2a2595419 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -28,6 +28,7 @@ describe('L2GraphTokenGateway', () => { let mockL1GRT: Account let mockL1Gateway: Account let pauseGuardian: Account + let mockL1GNS: Account let fixture: NetworkFixture let arbSysMock: FakeContract @@ -55,6 +56,7 @@ describe('L2GraphTokenGateway', () => { mockL1Gateway, l2Receiver, pauseGuardian, + mockL1GNS, ] = await getAccounts() fixture = new NetworkFixture() @@ -188,6 +190,7 @@ describe('L2GraphTokenGateway', () => { mockRouter.address, mockL1GRT.address, mockL1Gateway.address, + mockL1GNS.address, ) let tx = l2GraphTokenGateway.connect(governor.signer).setPaused(true) await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) @@ -218,6 +221,7 @@ describe('L2GraphTokenGateway', () => { mockRouter.address, mockL1GRT.address, mockL1Gateway.address, + mockL1GNS.address, ) await l2GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) let tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) @@ -280,6 +284,7 @@ describe('L2GraphTokenGateway', () => { mockRouter.address, mockL1GRT.address, mockL1Gateway.address, + mockL1GNS.address, ) }) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index e07b1e490..bf4c6ac17 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -9,6 +9,7 @@ import { BancorFormula } from '../../build/types/BancorFormula' import { Controller } from '../../build/types/Controller' import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' import { Curation } from '../../build/types/Curation' +import { L2Curation } from '../../build/types/L2Curation' import { DisputeManager } from '../../build/types/DisputeManager' import { EpochManager } from '../../build/types/EpochManager' import { GNS } from '../../build/types/GNS' @@ -22,6 +23,8 @@ import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' import { L2GraphToken } from '../../build/types/L2GraphToken' import { BridgeEscrow } from '../../build/types/BridgeEscrow' +import { L2GNS } from '../../build/types/L2GNS' +import { L1GNS } from '../../build/types/L1GNS' // Disable logging for tests logger.pause() @@ -32,6 +35,7 @@ export const defaults = { curation: { reserveRatio: toBN('500000'), minimumCurationDeposit: toGRT('100'), + l2MinimumCurationDeposit: toBN('1'), curationTaxPercentage: 0, }, dispute: { @@ -120,6 +124,28 @@ export async function deployCuration( ) as unknown as Curation } +export async function deployL2Curation( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + // Dependency + const curationTokenMaster = await deployContract('GraphCurationToken', deployer) + + // Deploy + return network.deployContractWithProxy( + proxyAdmin, + 'L2Curation', + [ + controller, + curationTokenMaster.address, + defaults.curation.curationTaxPercentage, + defaults.curation.l2MinimumCurationDeposit, + ], + deployer, + ) as unknown as L2Curation +} + export async function deployDisputeManager( deployer: Signer, controller: string, @@ -155,13 +181,13 @@ export async function deployEpochManager( ) as unknown as EpochManager } -export async function deployGNS( +async function deployL1OrL2GNS( deployer: Signer, controller: string, proxyAdmin: GraphProxyAdmin, -): Promise { + isL2: boolean, +): Promise { // Dependency - const bondingCurve = (await deployContract('BancorFormula', deployer)) as unknown as BancorFormula const subgraphDescriptor = await deployContract('SubgraphNFTDescriptor', deployer) const subgraphNFT = (await deployContract( 'SubgraphNFT', @@ -169,11 +195,17 @@ export async function deployGNS( await deployer.getAddress(), )) as SubgraphNFT + let name: string + if (isL2) { + name = 'L2GNS' + } else { + name = 'L1GNS' + } // Deploy const proxy = (await network.deployContractWithProxy( proxyAdmin, - 'GNS', - [controller, bondingCurve.address, subgraphNFT.address], + name, + [controller, subgraphNFT.address], deployer, )) as unknown as GNS @@ -181,7 +213,27 @@ export async function deployGNS( await subgraphNFT.connect(deployer).setMinter(proxy.address) await subgraphNFT.connect(deployer).setTokenDescriptor(subgraphDescriptor.address) - return proxy + if (isL2) { + return proxy as L2GNS + } else { + return proxy as L1GNS + } +} + +export async function deployL1GNS( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return deployL1OrL2GNS(deployer, controller, proxyAdmin, false) as unknown as L1GNS +} + +export async function deployL2GNS( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return deployL1OrL2GNS(deployer, controller, proxyAdmin, true) as unknown as L2GNS } export async function deployServiceRegistry( diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index ed6002a2d..4490fe915 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -12,7 +12,9 @@ import { DisputeManager } from '../../build/types/DisputeManager' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' import { Curation } from '../../build/types/Curation' -import { GNS } from '../../build/types/GNS' +import { L2Curation } from '../../build/types/L2Curation' +import { L1GNS } from '../../build/types/L1GNS' +import { L2GNS } from '../../build/types/L2GNS' import { Staking } from '../../build/types/Staking' import { RewardsManager } from '../../build/types/RewardsManager' import { ServiceRegistry } from '../../build/types/ServiceRegistry' @@ -28,7 +30,7 @@ export interface L1FixtureContracts { epochManager: EpochManager grt: GraphToken curation: Curation - gns: GNS + gns: L1GNS staking: Staking rewardsManager: RewardsManager serviceRegistry: ServiceRegistry @@ -42,8 +44,8 @@ export interface L2FixtureContracts { disputeManager: DisputeManager epochManager: EpochManager grt: L2GraphToken - curation: Curation - gns: GNS + curation: L2Curation + gns: L2GNS staking: Staking rewardsManager: RewardsManager serviceRegistry: ServiceRegistry @@ -91,8 +93,18 @@ export class NetworkFixture { grt = await deployment.deployGRT(deployer) } - const curation = await deployment.deployCuration(deployer, controller.address, proxyAdmin) - const gns = await deployment.deployGNS(deployer, controller.address, proxyAdmin) + let curation: Curation | L2Curation + if (isL2) { + curation = await deployment.deployL2Curation(deployer, controller.address, proxyAdmin) + } else { + curation = await deployment.deployCuration(deployer, controller.address, proxyAdmin) + } + let gns: L1GNS | L2GNS + if (isL2) { + gns = await deployment.deployL2GNS(deployer, controller.address, proxyAdmin) + } else { + gns = await deployment.deployL1GNS(deployer, controller.address, proxyAdmin) + } const staking = await deployment.deployStaking(deployer, controller.address, proxyAdmin) const disputeManager = await deployment.deployDisputeManager( deployer, @@ -137,6 +149,7 @@ export class NetworkFixture { await controller.setContractProxy(utils.id('DisputeManager'), staking.address) await controller.setContractProxy(utils.id('RewardsManager'), rewardsManager.address) await controller.setContractProxy(utils.id('ServiceRegistry'), serviceRegistry.address) + await controller.setContractProxy(utils.id('GNS'), gns.address) if (isL2) { await controller.setContractProxy(utils.id('GraphTokenGateway'), l2GraphTokenGateway.address) } else { @@ -233,6 +246,7 @@ export class NetworkFixture { mockRouterAddress: string, mockL2GRTAddress: string, mockL2GatewayAddress: string, + mockL2GNSAddress: string, ): Promise { // First configure the Arbitrum bridge mocks await arbitrumMocks.bridgeMock.connect(deployer).setInbox(arbitrumMocks.inboxMock.address, true) @@ -258,6 +272,10 @@ export class NetworkFixture { await l1FixtureContracts.bridgeEscrow .connect(deployer) .approveAll(l1FixtureContracts.l1GraphTokenGateway.address) + await l1FixtureContracts.gns.connect(deployer).setCounterpartGNSAddress(mockL2GNSAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .addToCallhookAllowlist(l1FixtureContracts.gns.address) await l1FixtureContracts.l1GraphTokenGateway.connect(deployer).setPaused(false) } @@ -267,6 +285,7 @@ export class NetworkFixture { mockRouterAddress: string, mockL1GRTAddress: string, mockL1GatewayAddress: string, + mockL1GNSAddress: string, ): Promise { // Configure the L2 GRT // Configure the gateway @@ -282,6 +301,7 @@ export class NetworkFixture { await l2FixtureContracts.l2GraphTokenGateway .connect(deployer) .setL1CounterpartAddress(mockL1GatewayAddress) + await l2FixtureContracts.gns.connect(deployer).setCounterpartGNSAddress(mockL1GNSAddress) await l2FixtureContracts.l2GraphTokenGateway.connect(deployer).setPaused(false) } diff --git a/test/lib/gnsUtils.ts b/test/lib/gnsUtils.ts new file mode 100644 index 000000000..1314f9c52 --- /dev/null +++ b/test/lib/gnsUtils.ts @@ -0,0 +1,331 @@ +import { BigNumber, ContractTransaction } from 'ethers' +import { namehash, solidityKeccak256 } from 'ethers/lib/utils' +import { Curation } from '../../build/types/Curation' +import { L1GNS } from '../../build/types/L1GNS' +import { L2GNS } from '../../build/types/L2GNS' +import { Account, getChainID, randomHexBytes, toBN } from './testHelpers' +import { expect } from 'chai' +import { L2Curation } from '../../build/types/L2Curation' +import { GraphToken } from '../../build/types/GraphToken' +import { L2GraphToken } from '../../build/types/L2GraphToken' + +// Entities +export interface PublishSubgraph { + subgraphDeploymentID: string + versionMetadata: string + subgraphMetadata: string +} + +export interface Subgraph { + vSignal: BigNumber + nSignal: BigNumber + subgraphDeploymentID: string + reserveRatioDeprecated: number + disabled: boolean + withdrawableGRT: BigNumber + id?: string +} + +export interface AccountDefaultName { + name: string + nameIdentifier: string +} + +export const DEFAULT_RESERVE_RATIO = 1000000 + +export const buildSubgraphID = async ( + account: string, + seqID: BigNumber, + chainID?: number, +): Promise => { + chainID = chainID ?? (await getChainID()) + return solidityKeccak256(['address', 'uint256', 'uint256'], [account, seqID, chainID]) +} + +export const buildLegacySubgraphID = (account: string, seqID: BigNumber): string => + solidityKeccak256(['address', 'uint256'], [account, seqID]) + +export const buildSubgraph = (): PublishSubgraph => { + return { + subgraphDeploymentID: randomHexBytes(), + versionMetadata: randomHexBytes(), + subgraphMetadata: randomHexBytes(), + } +} + +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: Account, + newSubgraph: PublishSubgraph, + gns: L1GNS | L2GNS, +): Promise => { + const subgraphID = await buildSubgraphID( + account.address, + await gns.nextAccountSeqID(account.address), + ) + + // Send tx + const tx = gns + .connect(account.signer) + .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.reserveRatioDeprecated).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) + + return { ...subgraph, id: subgraphID } +} + +export const publishNewVersion = async ( + account: Account, + 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 + const tx = gns + .connect(account.signer) + .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)) { + 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: Account, + 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.signer).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: Account, + 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.signer).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: Account, + 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.signer).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.reserveRatioDeprecated).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/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index 87e04beb1..266c6d628 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -133,12 +133,18 @@ export const applyL1ToL2Alias = (l1Address: string): string => { return l2AddressAsNumber.mod(mask).toHexString() } +export async function impersonateAccount(address: string): Promise { + await provider().send('hardhat_impersonateAccount', [address]) + return hre.ethers.getSigner(address) +} + +export async function setAccountBalance(address: string, newBalance: BigNumber): Promise { + await provider().send('hardhat_setBalance', [address, hexValue(newBalance)]) +} + // Adapted from: // https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/test/utils/messaging.ts#L5 export async function getL2SignerFromL1(l1Address: string): Promise { const l2Address = applyL1ToL2Alias(l1Address) - await provider().send('hardhat_impersonateAccount', [l2Address]) - const l2Signer = await hre.ethers.getSigner(l2Address) - - return l2Signer + return impersonateAccount(l2Address) } diff --git a/test/staking/allocation.test.ts b/test/staking/allocation.test.ts index 473ecb451..790c528d1 100644 --- a/test/staking/allocation.test.ts +++ b/test/staking/allocation.test.ts @@ -582,8 +582,6 @@ describe('Staking:Allocation', () => { for (const tokensToAllocate of [toBN(100), toBN(0)]) { context(`> with ${tokensToAllocate} allocated tokens`, async function () { beforeEach(async function () { - // Advance to next epoch to avoid creating the allocation - // right at the epoch boundary, which would mess up the tests. await advanceToNextEpoch(epochManager) await allocate(tokensToAllocate) })