diff --git a/.solcover.js b/.solcover.js index b10738c1f..8c5efb96b 100644 --- a/.solcover.js +++ b/.solcover.js @@ -7,4 +7,5 @@ module.exports = { }, skipFiles, istanbulFolder: './reports/coverage', + configureYulOptimizer: true, } diff --git a/.solhintignore b/.solhintignore index 24a21bb44..b36dffeb7 100644 --- a/.solhintignore +++ b/.solhintignore @@ -4,6 +4,5 @@ node_modules ./contracts/discovery/erc1056 ./contracts/rewards/RewardsManager.sol ./contracts/staking/libs/LibFixedMath.sol -./contracts/tests/RewardsManagerMock.sol ./contracts/tests/ens ./contracts/tests/testnet/GSRManager.sol diff --git a/addresses.json b/addresses.json index de5410660..2e90d5ee8 100644 --- a/addresses.json +++ b/addresses.json @@ -1,921 +1,170 @@ { - "1": { - "IENS": { - "address": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" - }, - "IEthereumDIDRegistry": { - "address": "0xdCa7EF03e98e0DC2B855bE647C39ABe984fcF21B" - }, - "GraphProxyAdmin": { - "address": "0xF3B000a6749259539aF4E49f24EEc74Ea0e71430", - "creationCodeHash": "0x26a6f47e71ad242e264768571ce7223bf5a86fd0113ab6cb8200f65820232904", - "runtimeCodeHash": "0xd5330527cfb09df657adc879d8ad704ce6b8d5917265cabbd3eb073d1399f122", - "txHash": "0xc5fe1a9f70e3cc4d286e19e3ee8ee9a0639c7415aea22a3f308951abf300382c" - }, - "BancorFormula": { - "address": "0xd0C61e8F15d9deF697E1100663eD7dA74d3727dC", - "creationCodeHash": "0x17f6de9ab8a9bcf03a548c01d620a32caf1f29be8d90a9688ebee54295f857ef", - "runtimeCodeHash": "0x97a57f69b0029383398d02587a3a357168950d61622fe9f9710bf02b59740d63", - "txHash": "0xcd0e28e7d328ff306bb1f2079e594ff9d04d09f21bc5f978b790c8d44b02055a" - }, - "Controller": { - "address": "0x24CCD4D3Ac8529fF08c58F74ff6755036E616117", - "creationCodeHash": "0x7f37a1844c38fffd5390d2114804ffc4e5cf66dfb5c7bd67a32a4f5d10eebd2d", - "runtimeCodeHash": "0x929c62381fbed59483f832611752177cc2642e1e35fedeeb6cd9703e278448a0", - "txHash": "0x12b13ed4ac6fee14335be09df76171b26223d870977524cfdce46c11112a5c04" - }, - "EpochManager": { - "address": "0x64F990Bf16552A693dCB043BB7bf3866c5E05DdB", - "initArgs": [ - { - "name": "controller", - "value": "0x24CCD4D3Ac8529fF08c58F74ff6755036E616117" - }, - { - "name": "lengthInBlocks", - "value": 6646 - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x9116a77a4e87eb3fe28514a26b1a6e3ee5ce982f9df3c18aadfc36d4f7f050e7", - "proxy": true, - "implementation": { - "address": "0x3fab259F2392F733c60C19492B5678E5D2D2Ee31", - "creationCodeHash": "0xf03074bb7f026a2574b6ffb5d0f63f0c4fee81e004e1c46ef262dd5802d3384f", - "runtimeCodeHash": "0x0d078a0bf778c6c713c46979ac668161a0a0466356252e47082f80912e4495b2", - "txHash": "0x730141db9a1dc5c9429f7543442e34e9eb994610e2ceabdedb6d322e1bedf2aa" - } - }, - "GraphToken": { - "address": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", - "constructorArgs": [ - { - "name": "initialSupply", - "value": "10000000000000000000000000000" - } - ], - "creationCodeHash": "0x30da7a30d71fbd41d3327e4d0183401f257af3e905a0c68ebfd18b590b27b530", - "runtimeCodeHash": "0xb964f76194a04272e7582382a4d6bd6271bbb90deb5c1fd3ae3913504ea3a830", - "txHash": "0x079625b9f58a40f1948b396b7007d09ff4aa193d7ec798923910fc179294cab8" - }, - "ServiceRegistry": { - "address": "0xaD0C9DaCf1e515615b0581c8D7E295E296Ec26E6", - "initArgs": [ - { - "name": "controller", - "value": "0x24CCD4D3Ac8529fF08c58F74ff6755036E616117" - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x94cbb1d3863e33bf92acc6fab534c5ce63a9d1347958a323ae496b06f710f042", - "proxy": true, - "implementation": { - "address": "0x866232Ec9a9F918a821eBa561Cc5FC960Ef5B3aa", - "creationCodeHash": "0xf5fa541b43d15fade518feb63a95a73b9c67626108ead259e444af3a7ae1804f", - "runtimeCodeHash": "0x9856d2c2985f410f2f77e456fe6163827ea5251eb5e3f3768d3d4f8868187882", - "txHash": "0xdf811598fbfbc487b16b5bb3444ed47ae3107d3dcde8dbd770e8810315f942b5" - } - }, - "GraphCurationToken": { - "address": "0xb2E26f17Aea8eFA534e15Bde5C79c25D0C3dfa2e", - "creationCodeHash": "0x7e9a56b6fc05d428d1c1116eaa88a658f05487b493d847bfe5c69e35ec34f092", - "runtimeCodeHash": "0x587f9d4e9ecf9e7048d9f42f027957ca34ee6a95ca37d9758d8cd0ee16e89818", - "txHash": "0x68eb11f4d6eaec5036c97b4c6102a509ac31933f1fe011f275b3e5fee30b6590" - }, - "Curation": { - "address": "0x8FE00a685Bcb3B2cc296ff6FfEaB10acA4CE1538", - "initArgs": [ - { - "name": "controller", - "value": "0x24CCD4D3Ac8529fF08c58F74ff6755036E616117" - }, - { - "name": "bondingCurve", - "value": "0xd0C61e8F15d9deF697E1100663eD7dA74d3727dC" - }, - { - "name": "reserveRatio", - "value": 500000 - }, - { - "name": "curationTaxPercentage", - "value": 25000 - }, - { - "name": "minimumCurationDeposit", - "value": "1000000000000000000" - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x64d8d94e21f1923bd1793011ba28f24befd57b511622920716238b05595dac7d", - "proxy": true, - "implementation": { - "address": "0x147A7758EA71d91D545407927b34DD77A5f7C21A", - "creationCodeHash": "0x4aea53d73a1b7b00db3ba36023a70f4e53df68f9b42cb8932afb9cf1837a8cf7", - "runtimeCodeHash": "0x6e5cb73148de597888b628c2e0d97fa0f66ee4867ee0905314034f9031d52872", - "txHash": "0x2e44da799ad8866ac49aae2e40a16c57784ed4b1e9343daa4f764c39a05e0826" - } - }, - "GNS": { - "address": "0xaDcA0dd4729c8BA3aCf3E99F3A9f471EF37b6825", - "initArgs": [ - { - "name": "controller", - "value": "0x24CCD4D3Ac8529fF08c58F74ff6755036E616117" - }, - { - "name": "bondingCurve", - "value": "0xd0C61e8F15d9deF697E1100663eD7dA74d3727dC" - }, - { - "name": "didRegistry", - "value": "0xdca7ef03e98e0dc2b855be647c39abe984fcf21b" - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x7ef90b0477e5c5d05bbd203af7d2bf15224640204e12abb07331df11425d2d00", - "proxy": true, - "implementation": { - "address": "0xfdf6de9c5603d85e1dae3d00a776f43913c9b203", - "creationCodeHash": "0x86499a1c90a73b062c0d25777379cdf52085e36c7f4ce44016adc7775ea24355", - "runtimeCodeHash": "0x85cc02c86b4ee2c1b080c6f70500f775bb0fab7960ce62444a8018f3af07af75", - "txHash": "0x218dbb4fd680db263524fc6be36462c18f3e267b87951cd86296eabd4a381183" - } - }, - "Staking": { - "address": "0xF55041E37E12cD407ad00CE2910B8269B01263b9", - "initArgs": [ - { - "name": "controller", - "value": "0x24CCD4D3Ac8529fF08c58F74ff6755036E616117" - }, - { - "name": "minimumIndexerStake", - "value": "100000000000000000000000" - }, - { - "name": "thawingPeriod", - "value": 186092 - }, - { - "name": "protocolPercentage", - "value": 10000 - }, - { - "name": "curationPercentage", - "value": 100000 - }, - { - "name": "channelDisputeEpochs", - "value": 7 - }, - { - "name": "maxAllocationEpochs", - "value": 28 - }, - { - "name": "delegationUnbondingPeriod", - "value": 28 - }, - { - "name": "delegationRatio", - "value": 16 - }, - { - "name": "rebateAlphaNumerator", - "value": 77 - }, - { - "name": "rebateAlphaDenominator", - "value": 100 - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x6c92edf1c385983d57be0635cf40e1d1068d778edecf2be1631f51556c731af7", - "proxy": true, - "implementation": { - "address": "0x0Cf97E609937418eBC8C209404B947cBC914F599", - "creationCodeHash": "0xefff927976deb5bcec19101657cf59fc0baf5a8858cfcfe9465c607ee8ee3465", - "runtimeCodeHash": "0x3493cd97b84aead4b9c6a39e2da80aa5e7b08aaabef1c18680bb0856916e9687", - "txHash": "0x104c3068e4d7c79cdbefe9da401197cc456d8f93cd1b8c2d42bc5abeec27f03e", - "libraries": { - "LibCobbDouglas": "0x054f94aB35ee8E92aA5a51084Fe44295844A2DEe" - } - } - }, - "RewardsManager": { - "address": "0x9Ac758AB77733b4150A901ebd659cbF8cB93ED66", - "initArgs": [ - { - "name": "controller", - "value": "0x24CCD4D3Ac8529fF08c58F74ff6755036E616117" - }, - { - "name": "issuanceRate", - "value": "1000000012184945188" - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0xd3a4d1b3e250e606f56417fd6e43d35bc794e793b1c5be4ffbecc3a43ca1b7b6", - "proxy": true, - "implementation": { - "address": "0x320Fe3AF387a5Fe4159b52dA62246834eCc4b0c1", - "creationCodeHash": "0xfec6d35d9de8a7234e77484ee4fa5d6867697d607b591ed5a8e01679fa1f0394", - "runtimeCodeHash": "0x4595f2b6c37d65ad1deed2497054b2319fb0c6419439e2e374b29a29aa9fcb81", - "txHash": "0x513d322a447acd84d04001933e151156a391999c4c09c7fccad65d6573d694bb" - } - }, - "DisputeManager": { - "address": "0x97307b963662cCA2f7eD50e38dCC555dfFc4FB0b", - "initArgs": [ - { - "name": "controller", - "value": "0x24CCD4D3Ac8529fF08c58F74ff6755036E616117" - }, - { - "name": "arbitrator", - "value": "0xE1FDD398329C6b74C14cf19100316f0826a492d3" - }, - { - "name": "minimumDeposit", - "value": "10000000000000000000000" - }, - { - "name": "fishermanRewardPercentage", - "value": 500000 - }, - { - "name": "slashingPercentage", - "value": 25000 - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x90cd5852f5824f76d93814ffea26040ff503c81a84c4430e3688f219f9b48465", - "proxy": true, - "implementation": { - "address": "0x444c138bf2b151f28a713b0ee320240365a5bfc2", - "creationCodeHash": "0xc00c4702d9683f70a90f0b73ce1842e66fa4c26b2cf75fb486a016bb7bac2102", - "runtimeCodeHash": "0x2bb6445bf9e12618423efe9ef64d05e14d283979829e751cd24685c1440c403f", - "txHash": "0x413cd4f8e9e70ad482500772c1f13b0be48deb42d7f2d0d5a74b56d5a6bd8a4d" - } - }, - "AllocationExchange": { - "address": "0x4a53cf3b3EdA545dc61dee0cA21eA8996C94385f", - "initArgs": [ - { - "name": "graphToken", - "value": "0xc944e90c64b2c07662a292be6244bdf05cda44a7" - }, - { - "name": "staking", - "value": "0xf55041e37e12cd407ad00ce2910b8269b01263b9" - }, - { - "name": "governor", - "value": "0x74db79268e63302d3fc69fb5a7627f7454a41732" - }, - { - "name": "authority", - "value": "0x79fd74da4c906509862c8fe93e87a9602e370bc4" - } - ], - "creationCodeHash": "0x1c7b0d7e81fc15f8ccc5b159e2cedb1f152653ebbce895b59eb74a1b26826fda", - "runtimeCodeHash": "0xa63c77e0724a5f679660358452e388f60379f1331e74542afb1436ffb213b960", - "txHash": "0x2ecd036c562f2503af9eaa1a9bca3729bd31ec8a91677530eefbecb398b793ba" - }, - "SubgraphNFTDescriptor": { - "address": "0x8F0B7e136891e8Bad6aa4Abcb64EeeFE29dC2Af0", - "creationCodeHash": "0x7ac0757e66857e512df199569ee11c47a61b00a8d812469b79afa5dafa98c0ed", - "runtimeCodeHash": "0x9a34ad6b202bdfa95ea85654ea2e0dd40a4b8b10847f1c3d3d805fa95a078a3d", - "txHash": "0x77d98358726575ae044ac988b98b63f537951ccae2010e7177c4a7833dce9158" - }, - "SubgraphNFT": { - "address": "0x24e36639b3A3aaA9c928a8A6f12d34F942f1ab67", - "creationCodeHash": "0x8c9929ec6293458209f9cbadd96821604765e3656fe3c7b289b99194ede15336", - "runtimeCodeHash": "0x6309a51754b6bec245685c7a81059dc28e3756f1045f18d059abc9294f454a6a", - "txHash": "0x106c31f2c24a5285c47a766422823766f1c939034513e85613d70d99ef697173" - }, - "BridgeEscrow": { - "address": "0x36aFF7001294daE4C2ED4fDEfC478a00De77F090", - "initArgs": ["0x24CCD4D3Ac8529fF08c58F74ff6755036E616117"], - "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", - "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x218aff2c804dd3dfe5064b08cab83ffb37382ca2aea1a225c2ead02ec99f38b5", - "proxy": true, - "implementation": { - "address": "0xBcD54513aa593646d72aEA31406c633C235Ad6EA", - "creationCodeHash": "0x6a1fc897c0130a1c99221cde1938d247de13a0861111ac47ad81c691f323df1a", - "runtimeCodeHash": "0xc8e31a4ebea0c3e43ceece974071ba0b6db2bed6725190795e07a2d369d2a8ab", - "txHash": "0x92908e33b54f59ec13a0f7bd29b818c421742294b9974d73859e0bde871bafb9" - } - }, - "L1GraphTokenGateway": { - "address": "0x01cDC91B0A9bA741903aA3699BF4CE31d6C5cC06", - "initArgs": ["0x24CCD4D3Ac8529fF08c58F74ff6755036E616117"], - "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", - "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0xd327568a286d6fcda1a6b78a14c87d660523a6900be901d6a7fbc2504faff64b", - "proxy": true, - "implementation": { - "address": "0x2bc65E92B68560851C225459a31Df6617448EC31", - "creationCodeHash": "0x67f10f3d0761db9c6c45ef15b19d7ffd56ca558767aa5b960ffaeec8aa607433", - "runtimeCodeHash": "0xbdb984007bd365bea1b1bee307eab5e40a9aa319c111fd7ccab29db30c19ae79", - "txHash": "0xe3b69f2bb04e430d23f73933b37a22d9edb651680cf2751221240afebc274ca1" - } - } - }, "5": { "GraphProxyAdmin": { - "address": "0x6D47902c3358E0BCC06171DE935cB23D8E276fdd", - "creationCodeHash": "0x8b9a4c23135748eb0e4d0e743f7276d86264ace935d23f9aadcfccd64b482055", - "runtimeCodeHash": "0x47aa67e4a85090fe7324268b55fb7b320ee7a8937f2ad02480b71d4bb3332b13", - "txHash": "0xd4be829c13c741b8b56ca5ee7d98d86237ce44df7c11eff73df26cd87d5cab94" - }, - "BancorFormula": { - "address": "0x2DFDC3e11E035dD96A4aB30Ef67fab4Fb6EC01f2", - "creationCodeHash": "0x7ae36017eddb326ddd79c7363781366121361d42fdb201cf57c57424eede46f4", - "runtimeCodeHash": "0xed6701e196ad93718e28c2a2a44d110d9e9906085bcfe4faf4f6604b08f0116c", - "txHash": "0x97ca33e6e7d1d7d62bdec4827f208076922d9c42bf149693b36ab91429e65740" - }, - "Controller": { - "address": "0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B", - "creationCodeHash": "0x4f2082404e96b71282e9d7a8b7efd0f34996b5edac6711095911d36a57637c88", - "runtimeCodeHash": "0xe31d064a2db44ac10d41d74265b7d4482f86ee95644b9745c04f9fc91006906d", - "txHash": "0x8087bd10cc8d456a7b573bc30308785342db2b90d80f3a750931ab9cf5273b83" - }, - "EpochManager": { - "address": "0x03541c5cd35953CD447261122F93A5E7b812D697", - "initArgs": ["0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B", "554"], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0xb1c6189514b52091e35c0349dff29357a2572cd9c2f9ad7f623b2b24252826d1", - "proxy": true, - "implementation": { - "address": "0xb6a641879F195448F3Da10fF3b3C4541808a9342", - "creationCodeHash": "0x729aca90fcffdeede93bc42a6e089a93085ec04133e965cf0291cf6245880595", - "runtimeCodeHash": "0xce525d338b6ed471eeb36d2927a26608cca2d5cfe52bd0585945eacc55b525cf", - "txHash": "0x139630c31b6a5799231572aa0b555a44209acd79fb3df98832d80cf9a1013b58" - } - }, - "GraphToken": { - "address": "0x5c946740441C12510a167B447B7dE565C20b9E3C", - "constructorArgs": ["10000000000000000000000000000"], - "creationCodeHash": "0xa749ef173d768ffe0786529cd23238bc525f4a621a91303d8fb176533c18cec2", - "runtimeCodeHash": "0xe584408c8e04a6200bc7a50816343585ad80f699bd394b89bb96964dbc1a2a92", - "txHash": "0x0639808a47da8a5270bc89eb3009c7d29167c8f32f015648920ec5d114225540" - }, - "GraphCurationToken": { - "address": "0x8bEd0a89F18a801Da9dEA994D475DEa74f75A059", - "creationCodeHash": "0x8c076dacbf98f839a0ff25c197eafc836fc3fc1ee5183c7f157acec17678a641", - "runtimeCodeHash": "0xad138b4c4f34501f83aea6c03a49c103a9115526c993860a9acbd6caeaaf0d64", - "txHash": "0xc09739affd3d9dd43f690d3a487b1c149ad8aa50164995cfbc9de73914ff278a" - }, - "ServiceRegistry": { - "address": "0x7CF8aD279E9F26b7DAD2Be452A74068536C8231F", - "initArgs": ["0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B"], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0xf250ef40f4172e54b96047a59cfd7fc35ffabe14484ff1d518e0262195895282", - "proxy": true, - "implementation": { - "address": "0xdC7Fb3a43B9e069df5F07eDc835f60dAc3fD40BA", - "creationCodeHash": "0x45f56a7ad420cd11a8585594fb29121747d87d412161c8779ea36dfd34a48e88", - "runtimeCodeHash": "0x26aceabe83e2b757b2f000e185017528cdde2323c2129fd612180ac3192adfda", - "txHash": "0x2fdb5fa641f707809322107573ce7799711e125cc781aade99fd2948455847ab" - } - }, - "Curation": { - "address": "0xE59B4820dDE28D2c235Bd9A73aA4e8716Cb93E9B", - "initArgs": [ - "0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B", - "0x2DFDC3e11E035dD96A4aB30Ef67fab4Fb6EC01f2", - "0x8bEd0a89F18a801Da9dEA994D475DEa74f75A059", - "500000", - "10000", - "1000000000000000000" - ], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0xf1b1f0f28b80068bcc9fd6ef475be6324a8b23cbdb792f7344f05ce00aa997d7", - "proxy": true, - "implementation": { - "address": "0xAeaA2B058539750b740E858f97159E6856948670", - "creationCodeHash": "0x022576ab4b739ee17dab126ea7e5a6814bda724aa0e4c6735a051b38a76bd597", - "runtimeCodeHash": "0xc7b1f9bef01ef92779aab0ae9be86376c47584118c508f5b4e612a694a4aab93", - "txHash": "0x400bfb7b6c384363b859a66930590507ddca08ebedf64b20c4b5f6bc8e76e125" - } - }, - "SubgraphNFTDescriptor": { - "address": "0xE7e406b4Bfce0B78A751712BFEb1D6B0ce60e8fb", - "creationCodeHash": "0xf16e8ff11d852eea165195ac9e0dfa00f98e48f6ce3c77c469c7df9bf195b651", - "runtimeCodeHash": "0x39583196f2bcb85789b6e64692d8c0aa56f001c46f0ca3d371abbba2c695860f", - "txHash": "0xffee21f6616abd4ffdab0b930dbf44d2ba381a08c3c834798df464fd85e8047e" - }, - "SubgraphNFT": { - "address": "0x083318049968F20EfaEA48b0978EC57bbb0ECbcE", - "constructorArgs": ["0xEfc519BEd6a43a14f1BBBbA9e796C4931f7A5540"], - "creationCodeHash": "0x5de044b15df24beb8781d1ebe71f01301a6b8985183f37eb8d599aa4059a1d3e", - "runtimeCodeHash": "0x6a7751298d6ffdbcf421a3b72faab5b7d425884b04757303123758dbcfb21dfa", - "txHash": "0x8884b65a236c188e4c61cf9593be2f67b27e4f80785939336d659866cfd97aec" - }, - "GNS": { - "address": "0x065611D3515325aE6fe14f09AEe5Aa2C0a1f0CA7", - "initArgs": [ - "0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B", - "0x2DFDC3e11E035dD96A4aB30Ef67fab4Fb6EC01f2", - "0x083318049968F20EfaEA48b0978EC57bbb0ECbcE" - ], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0x0149f062893acb0eafcbf67acc99da99e03aab3ee2b6b40fbe523d91e0fcecd1", - "proxy": true, - "implementation": { - "address": "0xa95ee5A5f6b45Fcf85A7fa0714f462472C467818", - "creationCodeHash": "0x2e71e4aefc1e678cb9c71882c1da67fc640389337a7d6ae43f78d0f13294594a", - "runtimeCodeHash": "0xde0e02c6a36a90e11c768f40a81430b7e9cda261aa6dada14eaad392d42efc21", - "txHash": "0xbc6e9171943020d30c22197282311f003e79374e6eeeaab9c360942bdf4193f4" - } - }, - "Staking": { - "address": "0x35e3Cb6B317690d662160d5d02A5b364578F62c9", - "initArgs": [ - "0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B", - "100000000000000000000000", - "6646", - "10000", - "100000", - "2", - "4", - "12", - "16", - "77", - "100" - ], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0x1960be49029284756037cf3ee8afe9eeaba93de4ba84875c5eefd5d2289903bd", - "proxy": true, - "implementation": { - "address": "0x16e64AA72De0f3BDa30d3D324E967BDecb7c826a", - "creationCodeHash": "0x6828025572bcf46c755088cd0b11329db6b249b0221140e93571799125255ae1", - "runtimeCodeHash": "0x523492e8e808f27ac0240edc7359b760b1c17d0572a13e68799775b53c2a50ec", - "txHash": "0x42ff9ce1b319bbdd8619cdd999b2c3c7c3aeacc5ac7a6eddcc1c3f0a2774f4a0", - "libraries": { - "LibCobbDouglas": "0x137e60D093F679B0fF9ad922EB14aCe0F4F443cf" - } - } - }, - "RewardsManager": { - "address": "0x1246D7c4c903fDd6147d581010BD194102aD4ee2", - "initArgs": ["0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B", "1000000012184945188"], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0x108efecde4422dacf6ec7a07884cab214ba0a441fc73a6ad82ceb5c73e1c9334", - "proxy": true, - "implementation": { - "address": "0x8BE5c7B041662C042aDB8349D5588BB82366BEC0", - "creationCodeHash": "0xfec6d35d9de8a7234e77484ee4fa5d6867697d607b591ed5a8e01679fa1f0394", - "runtimeCodeHash": "0x4595f2b6c37d65ad1deed2497054b2319fb0c6419439e2e374b29a29aa9fcb81", - "txHash": "0x31cd85ce9106900e49e9abf24fafad5493360096bd8486ddb2c42942b061bb56" - } - }, - "DisputeManager": { - "address": "0x8c344366D9269174F10bB588F16945eb47f78dc9", - "initArgs": [ - "0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B", - "0xFD01aa87BeB04D0ac764FC298aCFd05FfC5439cD", - "10000000000000000000000", - "500000", - "25000", - "25000" - ], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0xe93eba1bda0d262efabbc05d4e01b9ee197f22dd4f798e4c5fc5b1b9c137428e", - "proxy": true, - "implementation": { - "address": "0x476F0b8e5F952f0740aD3b0cb50648a7496c8388", - "creationCodeHash": "0x5b73c9b910d66426fd965ac3110e9debda1d81134c0354a7af8ec1f2ebd765f6", - "runtimeCodeHash": "0xcaf3547f0d675a1e1d2f887cf4666410bc3b084e65ad283ed3f1ff2b1bccc113", - "txHash": "0x6a90b5e2d5dcae2c94fe518ce7f6fb2ffc11e562b9feac6464dcb32e1e90c039" - } - }, - "AllocationExchange": { - "address": "0x67FBea097202f46D601D7C937b5DBb615659aDF2", - "constructorArgs": [ - "0x5c946740441C12510a167B447B7dE565C20b9E3C", - "0x35e3Cb6B317690d662160d5d02A5b364578F62c9", - "0xf1135bFF22512FF2A585b8d4489426CE660f204c", - "0x52e498aE9B8A5eE2A5Cd26805F06A9f29A7F489F" - ], - "creationCodeHash": "0x97714e1a80674ab0af90a10f2c7156cc92794ef81565fe9c7c35ecbe0025cc08", - "runtimeCodeHash": "0x5c20792fefe71126668be8ab19ab26cdb8ab9a6f73efbfa1d90f91e26459fa67", - "txHash": "0x87b35e5289792800832902206cf0ee4b9900e4d38089bd6634d10ea78729bf54" - }, - "IENS": { - "address": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" - }, - "IEthereumDIDRegistry": { - "address": "0xdCa7EF03e98e0DC2B855bE647C39ABe984fcF21B" - }, - "BridgeEscrow": { - "address": "0x8e4145358af77516B886D865e2EcacC0Fd832B75", - "initArgs": ["0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B"], - "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", - "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x190ea3c8f731a77a8fd1cbce860f9561f233adeafe559b33201b7d21ccd298cf", - "proxy": true, - "implementation": { - "address": "0xDD569E05D54fBF5d02fE4a26aC03Ea00317A0A2e", - "creationCodeHash": "0x6a1fc897c0130a1c99221cde1938d247de13a0861111ac47ad81c691f323df1a", - "runtimeCodeHash": "0xc8e31a4ebea0c3e43ceece974071ba0b6db2bed6725190795e07a2d369d2a8ab", - "txHash": "0x369038dcc8d8e70d40782dd761a82cc453c7a4f1939284c724a5a72119e3e566" - } - }, - "L1GraphTokenGateway": { - "address": "0xc82fF7b51c3e593D709BA3dE1b3a0d233D1DEca1", - "initArgs": ["0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B"], - "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", - "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x4a06731591df5c5f77c11bf8df7851234873eb6727fbbc93f5595a223f7cf3fc", - "proxy": true, - "implementation": { - "address": "0x06A7A68d0D0D496693508ad3f50A8EA962333B7D", - "creationCodeHash": "0x9dac8130793923c7f35f3943b755b7a88e2de9a25d0ae5c0b8cb020b6479151a", - "runtimeCodeHash": "0xcd798129b77d26c0b29369138d2d8dd413fcf6cb9b3838c5f95f50d9839a388a", - "txHash": "0xa4d75169094cd8601ec507234695d83042e888ec2ab49b0ce150d7aae908d895" - } - } - }, - "1337": { - "GraphProxyAdmin": { - "address": "0x5b1869D9A4C187F2EAa108f3062412ecf0526b24", - "creationCodeHash": "0x8b9a4c23135748eb0e4d0e743f7276d86264ace935d23f9aadcfccd64b482055", - "runtimeCodeHash": "0x47aa67e4a85090fe7324268b55fb7b320ee7a8937f2ad02480b71d4bb3332b13", - "txHash": "0xeb755c878246eb261061e61335dcabb25c12aa65f17272bd5e680474ebd7af5d" - }, - "BancorFormula": { - "address": "0xCfEB869F69431e42cdB54A4F4f105C19C080A601", - "creationCodeHash": "0x7ae36017eddb326ddd79c7363781366121361d42fdb201cf57c57424eede46f4", - "runtimeCodeHash": "0xed6701e196ad93718e28c2a2a44d110d9e9906085bcfe4faf4f6604b08f0116c", - "txHash": "0xcf896ae348d971744d7600ede45a58ed2598caaa286ac9794acc5f81cf14933c" - }, - "Controller": { - "address": "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", - "creationCodeHash": "0x4f2082404e96b71282e9d7a8b7efd0f34996b5edac6711095911d36a57637c88", - "runtimeCodeHash": "0xe31d064a2db44ac10d41d74265b7d4482f86ee95644b9745c04f9fc91006906d", - "txHash": "0x2df913aa6ca0bec47227ca347677627390a00e78787f1144bcfc78673981dcd6" - }, - "EpochManager": { - "address": "0xD833215cBcc3f914bD1C9ece3EE7BF8B14f841bb", - "initArgs": ["0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", "554"], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0x7e8cd2c928453b8a5ca041d14f6fea09de80e5e808f8d6ba480f35f10a28148a", - "proxy": true, - "implementation": { - "address": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550", - "creationCodeHash": "0x729aca90fcffdeede93bc42a6e089a93085ec04133e965cf0291cf6245880595", - "runtimeCodeHash": "0xce525d338b6ed471eeb36d2927a26608cca2d5cfe52bd0585945eacc55b525cf", - "txHash": "0x376a2ebfe783246d83c3cbbdd95e20daa3cd3695a52d8e38699a7fa487d8ed64" - } - }, - "GraphToken": { - "address": "0xe982E462b094850F12AF94d21D470e21bE9D0E9C", - "constructorArgs": ["10000000000000000000000000000"], - "creationCodeHash": "0xa749ef173d768ffe0786529cd23238bc525f4a621a91303d8fb176533c18cec2", - "runtimeCodeHash": "0xe584408c8e04a6200bc7a50816343585ad80f699bd394b89bb96964dbc1a2a92", - "txHash": "0xf564d09de26eaa6d80b4712a426bcf24e05c221a8ac6214d81946c24242df619" - }, - "ServiceRegistry": { - "address": "0x9b1f7F645351AF3631a656421eD2e40f2802E6c0", - "initArgs": ["0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B"], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0x3493336fb4bc76b11aa4e880bb5e8366d00a0b1211ce417f9125d9b97a48920c", - "proxy": true, - "implementation": { - "address": "0x0290FB167208Af455bB137780163b7B7a9a10C16", - "creationCodeHash": "0x45f56a7ad420cd11a8585594fb29121747d87d412161c8779ea36dfd34a48e88", - "runtimeCodeHash": "0x26aceabe83e2b757b2f000e185017528cdde2323c2129fd612180ac3192adfda", - "txHash": "0x567c09fd4920dd8ec2e359bd3b2a77aa69658af1ff515fe6d687007967229bee" - } - }, - "Curation": { - "address": "0xA57B8a5584442B467b4689F1144D269d096A3daF", - "initArgs": [ - "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", - "0xCfEB869F69431e42cdB54A4F4f105C19C080A601", - "0x59d3631c86BbE35EF041872d502F218A39FBa150", - "500000", - "10000", - "1000000000000000000" - ], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0xfd1002811e9c61acd016cfbebb0c348fedfb94402ad9c330d9bcbebcd58c3f9c", - "proxy": true, - "implementation": { - "address": "0x2612Af3A521c2df9EAF28422Ca335b04AdF3ac66", - "creationCodeHash": "0x022576ab4b739ee17dab126ea7e5a6814bda724aa0e4c6735a051b38a76bd597", - "runtimeCodeHash": "0xc7b1f9bef01ef92779aab0ae9be86376c47584118c508f5b4e612a694a4aab93", - "txHash": "0xc93d39f849b249792924ee973c022aea2445c6662ce26f450d324b1c721c25a7" - } - }, - "GNS": { - "address": "0xA94B7f0465E98609391C623d0560C5720a3f2D33", - "initArgs": [ - "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", - "0xCfEB869F69431e42cdB54A4F4f105C19C080A601", - "0x0E696947A06550DEf604e82C26fd9E493e576337" - ], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0x07c9dee04758b140616e0e9688bd310a86115b8dd2c5ab011f1edb709b429484", - "proxy": true, - "implementation": { - "address": "0xDb56f2e9369E0D7bD191099125a3f6C370F8ed15", - "creationCodeHash": "0x2e71e4aefc1e678cb9c71882c1da67fc640389337a7d6ae43f78d0f13294594a", - "runtimeCodeHash": "0xde0e02c6a36a90e11c768f40a81430b7e9cda261aa6dada14eaad392d42efc21", - "txHash": "0x4032407d59e4ac88868270bb8d920bfcc8fe6572a22ad4f3be9c64da5a8f926e" - } - }, - "Staking": { - "address": "0x5f8e26fAcC23FA4cbd87b8d9Dbbd33D5047abDE1", - "initArgs": [ - "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", - "100000000000000000000000", - "6646", - "10000", - "100000", - "2", - "4", - "12", - "16", - "77", - "100" - ], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0x81c0824918a3d2f9d196118b9ea1c8d15bdf7dd55f3d39ca99f047d38c30445f", - "proxy": true, - "implementation": { - "address": "0xFC628dd79137395F3C9744e33b1c5DE554D94882", - "creationCodeHash": "0x55e99794a19a3fea4152ac8cbbec6ed93e88fa0b09e21ac6fbf00f39bfa928f6", - "runtimeCodeHash": "0xef297f45b62801f615d3271bb40f07a30a9906be0f70f2d57dbf6a44408191d3", - "txHash": "0x149846f986f24a619f1137be908ed2cf82dce52c18bcbeacefdb663b1a6dd765", - "libraries": { - "LibCobbDouglas": "0xb09bCc172050fBd4562da8b229Cf3E45Dc3045A6" - } - } - }, - "RewardsManager": { - "address": "0x4bf749ec68270027C5910220CEAB30Cc284c7BA2", - "initArgs": ["0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", "1000000012184945188"], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0xf4a13ad82b067ffb59e689df3cb828a5cd4eac7e316323dfbb4b05b191127ce5", - "proxy": true, - "implementation": { - "address": "0xD86C8F0327494034F60e25074420BcCF560D5610", - "creationCodeHash": "0xfec6d35d9de8a7234e77484ee4fa5d6867697d607b591ed5a8e01679fa1f0394", - "runtimeCodeHash": "0x4595f2b6c37d65ad1deed2497054b2319fb0c6419439e2e374b29a29aa9fcb81", - "txHash": "0xeea5271a0af6be6cc23e7a98fa84343d6b2c2aefaf1a80be63e945be332b5b0e" - } - }, - "DisputeManager": { - "address": "0x5017A545b09ab9a30499DE7F431DF0855bCb7275", - "initArgs": [ - "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", - "0xFD01aa87BeB04D0ac764FC298aCFd05FfC5439cD", - "10000000000000000000000", - "500000", - "25000", - "25000" - ], - "creationCodeHash": "0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58", - "runtimeCodeHash": "0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6", - "txHash": "0xc76797d4f240e81607f679ed0f0cd483065f4c657743bacd2198fd42fe4f089b", - "proxy": true, - "implementation": { - "address": "0x7C728214be9A0049e6a86f2137ec61030D0AA964", - "creationCodeHash": "0x5b73c9b910d66426fd965ac3110e9debda1d81134c0354a7af8ec1f2ebd765f6", - "runtimeCodeHash": "0xcaf3547f0d675a1e1d2f887cf4666410bc3b084e65ad283ed3f1ff2b1bccc113", - "txHash": "0x29822affa517965e1995fc4e777cd709daf9df8f16f13e08b3829bba5c50bf90" - } - }, - "EthereumDIDRegistry": { - "address": "0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab", - "creationCodeHash": "0x20cd202f7991716a84c097da5fbd365fd27f7f35f241f82c529ad7aba18b814b", - "runtimeCodeHash": "0x5f396ffd54b6cd6b3faded0f366c5d7e148cc54743926061be2dfd12a75391de", - "txHash": "0xe68b0ca45476f9d07359ee078d16b8dc9ed9769495cc87ba034bbbfbd470588b" - }, - "GraphCurationToken": { - "address": "0x59d3631c86BbE35EF041872d502F218A39FBa150", - "creationCodeHash": "0x8c076dacbf98f839a0ff25c197eafc836fc3fc1ee5183c7f157acec17678a641", - "runtimeCodeHash": "0xad138b4c4f34501f83aea6c03a49c103a9115526c993860a9acbd6caeaaf0d64", - "txHash": "0x7290a04e5649738b46213e76a426bb59bebb6af80641af8197539446eb716249" - }, - "SubgraphNFTDescriptor": { - "address": "0x630589690929E9cdEFDeF0734717a9eF3Ec7Fcfe", - "creationCodeHash": "0xf16e8ff11d852eea165195ac9e0dfa00f98e48f6ce3c77c469c7df9bf195b651", - "runtimeCodeHash": "0x39583196f2bcb85789b6e64692d8c0aa56f001c46f0ca3d371abbba2c695860f", - "txHash": "0x3c5bff07a071ac0737f06a1e7a91a5f80fcf86a16dc8ac232ed0a305db9d9d85" - }, - "SubgraphNFT": { - "address": "0x0E696947A06550DEf604e82C26fd9E493e576337", - "constructorArgs": ["0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"], - "creationCodeHash": "0x5de044b15df24beb8781d1ebe71f01301a6b8985183f37eb8d599aa4059a1d3e", - "runtimeCodeHash": "0x6a7751298d6ffdbcf421a3b72faab5b7d425884b04757303123758dbcfb21dfa", - "txHash": "0xcb40328bd03b6b25e74203e10f9ce17a131aa514f6ba9156aa8fcb81fe5f8cc2" - }, - "AllocationExchange": { - "address": "0xFF6049B87215476aBf744eaA3a476cBAd46fB1cA", - "constructorArgs": [ - "0xe982E462b094850F12AF94d21D470e21bE9D0E9C", - "0x5f8e26fAcC23FA4cbd87b8d9Dbbd33D5047abDE1", - "0xf1135bFF22512FF2A585b8d4489426CE660f204c", - "0x52e498aE9B8A5eE2A5Cd26805F06A9f29A7F489F" - ], - "creationCodeHash": "0x97714e1a80674ab0af90a10f2c7156cc92794ef81565fe9c7c35ecbe0025cc08", - "runtimeCodeHash": "0x07012b5692ec6cbeb7a6986e061fc5026a2f76545b07bfd9182985de002fa281", - "txHash": "0xe3d870434e38ee37142a86e0fc54063df59c02c3b70135f070c3a1025c5e8246" - } - }, - "42161": { - "GraphProxyAdmin": { - "address": "0x2983936aC20202a6555993448E0d5654AC8Ca5fd", + "address": "0x47D34EF929A884f00BC667B9b75aa7FADc39070B", "creationCodeHash": "0x68b304ac6bce7380d5e0f6b14a122f628bffebcc75f8205cb60f0baf578b79c3", "runtimeCodeHash": "0x8d9ba87a745cf82ab407ebabe6c1490197084d320efb6c246d94bcc80e804417", - "txHash": "0x3ff82c38ec0e08e8f4194689188edcc1e8acb1f231c14cce8f0223f4dfc6cb76" + "txHash": "0x8d80a4fb6bc3589952415e642494742269c0478ee4504b955adfcd335bc17660" }, "BancorFormula": { - "address": "0xA489FDc65229D6225014C0b357BCD19af6f00eE9", + "address": "0x77216171f18DF3019F8078A26E6609b98e7Bb1E4", "creationCodeHash": "0x7ae36017eddb326ddd79c7363781366121361d42fdb201cf57c57424eede46f4", "runtimeCodeHash": "0xed6701e196ad93718e28c2a2a44d110d9e9906085bcfe4faf4f6604b08f0116c", - "txHash": "0xb2bb14ba3cbd1bb31b08b86aced469745f9888710254bb3baed047f435e788c0" + "txHash": "0x632e8af8de4e92001d3b6ea070de688d349b75c4ef60a1df794190e8d575e07d" }, "Controller": { - "address": "0x0a8491544221dd212964fbb96487467291b2C97e", - "creationCodeHash": "0x798f913fbaa1b2547c917e3dc31679089ab27cba442c511c159803acdba28c15", - "runtimeCodeHash": "0x00ae0824f79c4e48d2d23a8d4e6d075f04f44f3ea30a4f4305c345bb98117c62", - "txHash": "0x2a9d5744ad0e5e2e6bb6733ae890702fed2bce906e4e8b1cc50d2d3912c58d18" + "address": "0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee", + "creationCodeHash": "0x5bde9a87bc4e8dd24d41900f0a19321c1dc6d3373d51bba093b130bb5b80a677", + "runtimeCodeHash": "0x7f0479db1d60ecf6295d92ea2359ebdd223640795613558b0594680f5d4922c9", + "txHash": "0xf850e3e05227f8aec28956cd66cee3eb8ad74132e0dd6e5bce98503a70fa79c6" }, "EpochManager": { - "address": "0x5A843145c43d328B9bB7a4401d94918f131bB281", - "initArgs": ["0x0a8491544221dd212964fbb96487467291b2C97e", "6646"], + "address": "0x022B51837CA80bd0b5390C41f93eC78a80dBDCcB", + "initArgs": ["0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee", "554"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x4c70b8a56278452898d9eb23787a977d38141ebe48c79417c3acf6748ff921cf", + "txHash": "0x8b0342fa65f2a7ec5fd49f52757160eced740d1f99a5a3193f9cb8e24679fab3", "proxy": true, "implementation": { - "address": "0xeEDEdb3660154f439D93bfF612f7902edf07b848", - "creationCodeHash": "0x83bc0b08dbe1a9259666ec209f06223863f7bb9cfbf917a2d4b795c771a727fe", - "runtimeCodeHash": "0xed60261c6dc84ebc16830c36f3ee370a92802601d5a2fe1c3c19f5120dcbc2eb", - "txHash": "0x64fac1c567b7be735084b337a1e4ea9b990a8ffee8190485dc9b8dfcc257146c" + "address": "0xcc4308367828958e3C4c22AF49CeC01999033f50", + "creationCodeHash": "0xa626f76aa43eb933cd0c2834c96df30eb71061e3fb842ffc4b2f453e0a35748d", + "runtimeCodeHash": "0x188202cb51f71a6603ce7d191640909450fb660b7af3b9746dc7000685565a0b", + "txHash": "0x292eb529c40f3a68f02a8036e325ce59ac182368bca086ae57966d9a95180c4e" } }, - "L2GraphToken": { - "address": "0x9623063377AD1B27544C965cCd7342f7EA7e88C7", - "initArgs": ["0x4528FD7868c91Ef64B9907450Ee8d82dC639612c"], - "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", - "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x8465190df853c05bbdec00ba6b66139be0e5663fd5b740bdd464ad7409ce2100", - "proxy": true, - "implementation": { - "address": "0xaFFCb96181D920FE8C0Af046C49B2c9eC98b28df", - "creationCodeHash": "0x6c4146427aafa7375a569154be95c8c931bf83aab0315706dd78bdf79c889e4c", - "runtimeCodeHash": "0x004371d1d80011906953dcba17c648503fc94b94e1e0365c8d8c706ff91f93e9", - "txHash": "0xbd7d146ce80831ed7643e9f5b5a84737da354994ae080cb3d7ff7bbc3e696b3d" - } + "GraphToken": { + "address": "0x2Bd394eB925a8a454e17839b6e5763EAF8357eC8", + "constructorArgs": ["10000000000000000000000000000"], + "creationCodeHash": "0x9c50586e9e305b3a299f1cdf92ca9bb04fad5f43b5e0f7505054d79783fd8b69", + "runtimeCodeHash": "0xfe612acbb09bdb23fe60014e890054621fd34d74bf12bd94fb73351d474cd641", + "txHash": "0xac3b74690dade580f6a89e80ec5ae5968d8a4b2f8cc2e87b124c40dc3f1cbc39" }, "GraphCurationToken": { - "address": "0x47a0d56ea574419B524285d52fFe7198297D209c", + "address": "0xB3a9AE8Cd8DaF75603863b8d538aC59d5A7e34F9", "creationCodeHash": "0x1ee42ee271cefe20c33c0de904501e618ac4b56debca67c634d0564cecea9ff2", "runtimeCodeHash": "0x340e8f378c0117b300f3ec255bc5c3a273f9ab5bd2940fa8eb3b5065b21f86dc", - "txHash": "0x382568f1871a3d57f4d3787b255a2364e9926cb6770fdca3cde6cb04b577ecd5" + "txHash": "0x28684baaf760cf3738aa70b309c855f2ed9c02b9d19707acb37136322de24208" }, "ServiceRegistry": { - "address": "0x072884c745c0A23144753335776c99BE22588f8A", - "initArgs": ["0x0a8491544221dd212964fbb96487467291b2C97e"], + "address": "0x5D34007F105164273b912b68a1d84af7FE9bB4ad", + "initArgs": ["0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x54b1da3f2fdd2327abe01f75ac38a670ee16d3f47bc58641ddaef04f0b9d0f78", + "txHash": "0xc624e8443ecf456ed807e886d805ab327e4a9c89bf8b5e336a49f6769d34a916", "proxy": true, "implementation": { - "address": "0xD32569dA3B89b040A0589B5b8D2c721a68472ff3", - "creationCodeHash": "0x50808e8cce93cf78a23c9e6dd7984356bd2bd93be30b358982909140dd61f6ff", - "runtimeCodeHash": "0xaef79c87f7e80107c0dc568cf1f8950459b5174ee3aa565ec487556a655e71db", - "txHash": "0xca363c6bc841b43bd896b6d2098434679884d200a28013dedb48a2c95028ce40" + "address": "0x6FC262789f74774e6734bd97fae7BF879f62E1f8", + "creationCodeHash": "0x296d23581b42c9a0e43c36ff54cd30bb9f2d7d0da4c319baa19c1444b323fa83", + "runtimeCodeHash": "0xa4512f526b488673b9ffc778cd0250a56e28d3f75d4fe1653d838ad606a647fe", + "txHash": "0x343eede3ea390e36af8adbf2e0798b3f1fe1b0e8ed7e507925d588bbc5bdf007" } }, "Curation": { - "address": "0x22d78fb4bc72e191C765807f8891B5e1785C8014", + "address": "0x0d0972Ae5612d71ded47337a5e17Fa5A21678310", "initArgs": [ - "0x0a8491544221dd212964fbb96487467291b2C97e", - "0xA489FDc65229D6225014C0b357BCD19af6f00eE9", - "0x47a0d56ea574419B524285d52fFe7198297D209c", - "1000000", + "0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee", + "0x77216171f18DF3019F8078A26E6609b98e7Bb1E4", + "0xB3a9AE8Cd8DaF75603863b8d538aC59d5A7e34F9", + "500000", "10000", "1000000000000000000" ], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x8f856e2090af3243349199f7991e01b1c28de7b70b0185d2370d8ada5ce9c97b", + "txHash": "0x405dcba54f0f8abc624e0249958953ed30483dd4311d4bc6f742ccfa2004f794", "proxy": true, "implementation": { - "address": "0x234071F4B1e322d1167D63503498f82cC7Fa4606", - "creationCodeHash": "0xa5fa77df71a72c5aadba812345978c291c5fa1a3a23129b6eba3a38ac85d8b5d", - "runtimeCodeHash": "0x1d265e9f658778b48a0247cfef79bfc9304d1faa1f1e085f2fea85629f68e2d5", - "txHash": "0x68d06c576b5bc472152f4ab4afa90e2c5832512fa83c69493be5da52b585f45c" + "address": "0x2C0348Fa890536288b695328c2dD7B26cfA73871", + "creationCodeHash": "0xfb47446e88d5b37a17879ef2f74e9c1817865ae0b8d01eb156c790f6ca37aa15", + "runtimeCodeHash": "0x2c6cc86f602f9b2445d11723276c704d1eb51df27321e332a7f079aa0efad7a4", + "txHash": "0xf651be52668beea362fa3ea31564b5704b81b5663f8a9dc4be7096a94a91b600" } }, "SubgraphNFTDescriptor": { - "address": "0x96cce9b6489744630A058324FB22e7CD02815ebe", + "address": "0x646d5961B83Ee23072Cf2e927a0D13F4950D796D", "creationCodeHash": "0xf16e8ff11d852eea165195ac9e0dfa00f98e48f6ce3c77c469c7df9bf195b651", "runtimeCodeHash": "0x39583196f2bcb85789b6e64692d8c0aa56f001c46f0ca3d371abbba2c695860f", - "txHash": "0xbb01566726e1d58825cf7aa2860f0f571ff47f92b3837aad0e73e7791fbca48c" + "txHash": "0x6e4e6bd2d26a9ff2a4e68f1f433229b933747dd6fca9f2940dac7cff5dc25bf8" }, "SubgraphNFT": { - "address": "0x3FbD54f0cc17b7aE649008dEEA12ed7D2622B23f", - "constructorArgs": ["0x4528FD7868c91Ef64B9907450Ee8d82dC639612c"], + "address": "0x052b1eEC9a3EB30aeA1311976354B8eC6C66Ab27", + "constructorArgs": ["0xBc7f4d3a85B820fDB1058FD93073Eb6bc9AAF59b"], "creationCodeHash": "0xc1e58864302084de282dffe54c160e20dd96c6cfff45e00e6ebfc15e04136982", "runtimeCodeHash": "0x7216e736a8a8754e88688fbf5c0c7e9caf35c55ecc3a0c5a597b951c56cf7458", - "txHash": "0x4334bd64938c1c5c604bde96467a8601875046569f738e6860851594c91681ff" + "txHash": "0xf97a420b9724ccc372cdbbf514f017dddab38a2e35caa8a9aca3cc7f8126b8a8" }, - "GNS": { - "address": "0xec9A7fb6CbC2E41926127929c2dcE6e9c5D33Bec", + "L1GNS": { + "address": "0xAcE28C3634B2255eB3e984CE952BC5872826C54a", "initArgs": [ - "0x0a8491544221dd212964fbb96487467291b2C97e", - "0xA489FDc65229D6225014C0b357BCD19af6f00eE9", - "0x3FbD54f0cc17b7aE649008dEEA12ed7D2622B23f" + "0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee", + "0x052b1eEC9a3EB30aeA1311976354B8eC6C66Ab27" ], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0xf7f2747d1aafd1684ffee7316e727475249cd358af559c6234089b72ffc25f5d", + "txHash": "0x50f246c91d07d082e0221660cb3d8642d6ef61c24b317356e5e0d43467241fcc", "proxy": true, "implementation": { - "address": "0x8Cab11d17082C67aFc3dc35D1a2E02B23dB914ab", - "creationCodeHash": "0xb0be24e926bb24420bb5a8d3f7bd0b70a545fdddbf8cb177a42478adf4435aae", - "runtimeCodeHash": "0x4cb62b9def5b691e43ed06808b18efe682fcefb7739909be0d6c87f1eda724cd", - "txHash": "0xb4bf3e0fdf9486ff24c567fecb90875a8d11efa1a1a4dba36f25d529c586852c" + "address": "0x42B86B155a24eaD976e72000c636EbfD14CAAb92", + "creationCodeHash": "0x8fafddf74b3b585bd747da8b1ad18dac495bce460384656108940c8c6755d435", + "runtimeCodeHash": "0x2e527cbf30f30c411bfcfbd890a0b443c36a2c378a2a27c9dffa7f30611cc651", + "txHash": "0x0c2b04f8540c22affa26667a68ccb22609db01b8023f5fa6c3b0521292e742b7" } }, - "Staking": { - "address": "0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03", + "StakingExtension": { + "address": "0xeb633Fd581a12e3dA05bBbd85FBFFFa0D7715AF7", + "creationCodeHash": "0x6e94435d4cdd8508d87c54ef0dead97918783a1e0701771bb13ea07685697f2d", + "runtimeCodeHash": "0x5f358ee8930c845d3b28ee9c46c638980d34171ea1db177bb15d61c7d8ed9453", + "txHash": "0xb3dee1ad07bd3a52233465c66a09a8154c5efaa125a5b56e936e727a8fffd5b3" + }, + "L1Staking": { + "address": "0x5829D7C7Bf2Cb672E923732bA99A57Ea21C2C253", "initArgs": [ - "0x0a8491544221dd212964fbb96487467291b2C97e", + "0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee", "100000000000000000000000", - "186092", + "6646", "10000", "100000", - "7", - "28", - "28", + "2", + "4", + "12", "16", "77", - "100" + "100", + "0xeb633Fd581a12e3dA05bBbd85FBFFFa0D7715AF7" ], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0xa33c0d58ddaed7e3f7381a33e3d5f63e39219863019f00d54ce2fd2446076ac7", + "txHash": "0xf00eca755d79e565c64029d07f793ec564e2d8af2fb79ff728151c9650ddf711", "proxy": true, "implementation": { - "address": "0x2787f89355924a8781Acf988f12855C6CD495A06", - "creationCodeHash": "0xa4e467ac964866579894ebd6afae491e807a16969f9f1faa95707dc25d8d201c", - "runtimeCodeHash": "0x1880c562574956c097d7f6d8f09a91d76e50bf1babd71e369f032d4b0716f0f8", - "txHash": "0x3ed3b4744df380b7f87341839f16c19234a1565f56a8c672183396678f8390c4", + "address": "0x4AeA6541Dd3c777E129170889Aa3557Cfc00c12a", + "creationCodeHash": "0x4f274713e8a608b9b5a65eabaca2858db2c7ea0dc3067892fe029cd9bcbd9d13", + "runtimeCodeHash": "0xe3d4b8019718ee229c06b9e1ae339a467dc8040eca1612aa43bbb483116f59c1", + "txHash": "0xbb1e8e208445c0d6d3dfea39789f6c05b2ed0d3916628bf8d8df1ceb4fcc3f88", "libraries": { - "LibCobbDouglas": "0x86e80908F80B40B924966B781a8e20338670B431" + "LibCobbDouglas": "0x9Ce3faa1b439670b3e37D794015c079Cf7E6b6C8" } } }, "RewardsManager": { - "address": "0x971B9d3d0Ae3ECa029CAB5eA1fB0F72c85e6a525", - "initArgs": ["0x0a8491544221dd212964fbb96487467291b2C97e"], + "address": "0x748Df1c00B1D60f088Bf76Dc62A0a2b55C6ceE4f", + "initArgs": ["0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x222e14cb6f49e3e7b76f6a523c1a3c24f96402676be8662bf1b94bb2250ddd0f", + "txHash": "0x4a5c28b298358920a0aef25f3d42aeeb08493f56e3b3fa161b7271cc475520ad", "proxy": true, "implementation": { - "address": "0x225aB818cD003BB17728768e6a48c160d89C64d0", - "creationCodeHash": "0x98aaabec491a17401ca37209db0613c91285de061e859574526f841a4dd60c4a", - "runtimeCodeHash": "0x2795a83531898957014373bd4595f1f9a381ecfaf787bdfc64380563af06f06a", - "txHash": "0x6b6f45a955e114102874d34e31106ad00e04a9dba96515a9153d60539eb9208a" + "address": "0xe5c490d045d04aAc17b7Eea8D66d94aC81f6497E", + "creationCodeHash": "0x5c93fcb74170b85ce5e63c09ddba2eb762db75375f004aa7b1b53c8113cc7613", + "runtimeCodeHash": "0xe18a3e48c5055b110fdd1bd8e3147ba28106c2b39616362d9a2f362221040107", + "txHash": "0x156f71bb2879df3f574a6560c1e933d9ad240e4efedd90227e0c67b594911f07" } }, "DisputeManager": { - "address": "0x0Ab2B043138352413Bb02e67E626a70320E3BD46", + "address": "0xf0fD669397704F99eA833b56C45762EA73dD86fA", "initArgs": [ - "0x0a8491544221dd212964fbb96487467291b2C97e", - "0x113DC95e796836b8F0Fa71eE7fB42f221740c3B0", + "0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee", + "0x54d1a1020C5bc929A603DC2161BF6C71ae05553E", "10000000000000000000000", "500000", "25000", @@ -923,174 +172,176 @@ ], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x68f08fe0a1179170c8b4c7542725d71432b4171604d7456dff824e0ec1c6cdb9", + "txHash": "0x36f5d840518b276a3cebe8e48859011fef561fc854548281b72ea7dab8b93b6d", "proxy": true, "implementation": { - "address": "0x0E55B996EB7Bfc3175E883D02FF55a34f8C9986e", - "creationCodeHash": "0x2e77ad7a1627b6e04bece0fe18b3ab543ef4a2d6914f2e5e640b2c8175aca3a8", - "runtimeCodeHash": "0x0186afe711eff4ceea28620d091e3c6034fd15be05894119c74a38b020e3a554", - "txHash": "0xbb04391bd3353d6f2210e98ced779edcda954d971effcae7fd8676a94afa2655" + "address": "0xE8a798480f2ccEd69549039e07F3a9369E387eC1", + "creationCodeHash": "0xa8879b9893390aac4e0df919d3e9233a63728b39e2dcb1eb3d1162df0ca62b86", + "runtimeCodeHash": "0xefc118c16dc91266c9a594e01444357feaaf357c00d03860ddf56dfee2626bbd", + "txHash": "0xffa51ff8aa8d5ca3a297ece7b74dea966c4c7ac740d164bb69958d2960ef4858" } }, "AllocationExchange": { - "address": "0x993F00C98D1678371a7b261Ed0E0D4b6F42d9aEE", + "address": "0x29c2F47CF89E960f433Ec30D2BEeE55955012d36", "constructorArgs": [ - "0x9623063377AD1B27544C965cCd7342f7EA7e88C7", - "0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03", - "0x270Ea4ea9e8A699f8fE54515E3Bb2c418952623b", - "0x79f2212de27912bCb25a452fC102C85c142E3eE3" + "0x2Bd394eB925a8a454e17839b6e5763EAF8357eC8", + "0x5829D7C7Bf2Cb672E923732bA99A57Ea21C2C253", + "0xcfF86De5ccc3f27574C63E1CaBD97CdD840Ee798", + "0x142eb17fCd30Bc31Dfd69312c0f4E5E329Cc5a3C" ], - "creationCodeHash": "0x96c5b59557c161d80f1617775a7b9537a89b0ecf2258598b3a37724be91ae80a", - "runtimeCodeHash": "0xc86fd1d67a0db0aed4cb310f977ebf3e70865e2095a167f4a103c3792146027c", - "txHash": "0x2bad6b8e5eda0026c8c38a70b925bbedd6a617a1e06952fb30e427fdbc592422" + "creationCodeHash": "0x4bc830095b2703a3c3cddcaa2ce66bc656d049d5c5684d4a6afa52d048a618cc", + "runtimeCodeHash": "0x0fcf0a3453d7421495ddd812f4ce744ef2eee8035f6f542a9333ea3dfd86412e", + "txHash": "0x8b626bf573be8a97586280b328ed02f564fb4a14da5aaa4eda71c76a6eda8730" }, - "L2GraphTokenGateway": { - "address": "0x65E1a5e8946e7E87d9774f5288f41c30a99fD302", - "initArgs": ["0x0a8491544221dd212964fbb96487467291b2C97e"], + "L1GraphTokenGateway": { + "address": "0x4E8b0949acd89bb86399d501a7f1da7A088512d2", + "initArgs": ["0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x50816047ea926423ec02b6b89efb96efcd3d7e7028ea7cf82c3da9fd1bf3869e", + "txHash": "0x3cfec420092cd34fa39b11d3853ad56ef635531ede341a67c8398b689da1b90d", "proxy": true, "implementation": { - "address": "0x6f37b2AF8A0Cc74f1bFddf2E9302Cb226710127F", - "creationCodeHash": "0xbd52455bd8b14bfc27af623388fe2f9e06ddd4c4be3fc06c51558a912de91770", - "runtimeCodeHash": "0x29e47f693053f978d6b2ac0a327319591bf5b5e8a6e6c0744b8afcc0250bf667", - "txHash": "0x0eaa9d03982b88e765262a15b95548cb688ce9337a48460f39e55f8850690cbe" + "address": "0x0A48df942A91331f3715dcfb33faa0f2e5a93592", + "creationCodeHash": "0xd3e42ea3aea1f3d3fef70033f9dc2287d56fadcab862975e2a78885da819b844", + "runtimeCodeHash": "0x47368d46b689ce9f09c8a82b144696c335454e6a78d248ae38f4ffc031e47abb", + "txHash": "0x2a59556410f16707d6d55b0eb4b558c73354899a4e09679a72c1f3636cc29927" } }, - "EthereumDIDRegistry": { - "address": "0xa9AEb1c6f14f4244547B9a0946C485DA99047638", - "creationCodeHash": "0x20cd202f7991716a84c097da5fbd365fd27f7f35f241f82c529ad7aba18b814b", - "runtimeCodeHash": "0x5f396ffd54b6cd6b3faded0f366c5d7e148cc54743926061be2dfd12a75391de", - "txHash": "0xdd23b546fa3b6be0cea2339abe3023a082153693fbc7bf1bc86d190165823b39" - }, - "IEthereumDIDRegistry": { - "address": "0xa9AEb1c6f14f4244547B9a0946C485DA99047638" + "BridgeEscrow": { + "address": "0x873b5Bd895ECA503897847622c7Cf0DfBf95c2c6", + "initArgs": ["0x3DA68DAD02b388DD423Ef708B1Cbd76f9Cf414Ee"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0xd454df38552f10fb51a7a7b6588a4bd86d2ab1365831c69e16289b698542f3c2", + "proxy": true, + "implementation": { + "address": "0x68C838F1338041343687451AABd41156c3af99Af", + "creationCodeHash": "0xae110dc97e044272b3724b221c113799c99137ee00f4fa7b154fe4721a19ea6f", + "runtimeCodeHash": "0x8447cb58aafe36625f240cdfefa73f78f8fb5dcf0f9a45cb3ac3d657d065e743", + "txHash": "0x2c863a40f6f2400faef2b3980083783f46a8d2c032ac0b8bbcebd2c178ba22b2" + } } }, "421613": { "GraphProxyAdmin": { - "address": "0x4037466bb242f51575d32E8B1be693b3E5Cd1386", + "address": "0x987f08c2Fd9F6D63D82974400Bfb74fe39d8c91a", "creationCodeHash": "0x68b304ac6bce7380d5e0f6b14a122f628bffebcc75f8205cb60f0baf578b79c3", "runtimeCodeHash": "0x8d9ba87a745cf82ab407ebabe6c1490197084d320efb6c246d94bcc80e804417", - "txHash": "0x9c4d5f8c0ab5a5bc36b0a063ab1ff04372ce7d917c0b200b94544b5da4f0230d" - }, - "BancorFormula": { - "address": "0x71319060b9fdeD6174b6368bE04F9A1b7c9aCe48", - "creationCodeHash": "0x7ae36017eddb326ddd79c7363781366121361d42fdb201cf57c57424eede46f4", - "runtimeCodeHash": "0xed6701e196ad93718e28c2a2a44d110d9e9906085bcfe4faf4f6604b08f0116c", - "txHash": "0x7fe8cabb7a4fe56311591aa8d68d6c82cb0d5c232fc5aaf28bed4d1ece0e42e5" + "txHash": "0xb61459ef2677c72dcc015ac994d69b43732d442f68f3d869e634c8d36fb7319c" }, "Controller": { - "address": "0x7f734E995010Aa8d28b912703093d532C37b6EAb", - "creationCodeHash": "0x798f913fbaa1b2547c917e3dc31679089ab27cba442c511c159803acdba28c15", - "runtimeCodeHash": "0x00ae0824f79c4e48d2d23a8d4e6d075f04f44f3ea30a4f4305c345bb98117c62", - "txHash": "0x6213da3e6367ef47cd6e1fe23e4d83296f16153a64236a5c91f865f2ec84c089" + "address": "0x6b070868542E1b8D5399a0eFd7E742080cf300b9", + "creationCodeHash": "0x5bde9a87bc4e8dd24d41900f0a19321c1dc6d3373d51bba093b130bb5b80a677", + "runtimeCodeHash": "0x7f0479db1d60ecf6295d92ea2359ebdd223640795613558b0594680f5d4922c9", + "txHash": "0xce36fdbaff2f772ff760228ae2614c1a8a7187effa5f85fe24ae2fc2e79da1b8" }, "EpochManager": { - "address": "0x8ECedc7631f4616D7f4074f9fC9D0368674794BE", - "initArgs": ["0x7f734E995010Aa8d28b912703093d532C37b6EAb", "554"], + "address": "0xf1ab30b86eE6EA0976D27b2e58e88a7f5c3cc569", + "initArgs": ["0x6b070868542E1b8D5399a0eFd7E742080cf300b9", "554"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x62b0d6b8556be9443397ad1f6030fdc47b1a4a3ebcc63f34cdf4091420aec84b", + "txHash": "0xa85fc77d2776057244465e899435a2ff0c823b0d2a58d6e11abf0d524905804f", "proxy": true, "implementation": { - "address": "0xAaB195Ed1B445A2A0E357494d9036bC746227AE2", - "creationCodeHash": "0x83bc0b08dbe1a9259666ec209f06223863f7bb9cfbf917a2d4b795c771a727fe", - "runtimeCodeHash": "0xed60261c6dc84ebc16830c36f3ee370a92802601d5a2fe1c3c19f5120dcbc2eb", - "txHash": "0xd4f8780490f63432580e3dd5b2b4d9b39e904e8b4ac5cfd23540658cbafe449d" + "address": "0xa412E54656dFB310357844837C44B01F39265cd9", + "creationCodeHash": "0xa626f76aa43eb933cd0c2834c96df30eb71061e3fb842ffc4b2f453e0a35748d", + "runtimeCodeHash": "0x188202cb51f71a6603ce7d191640909450fb660b7af3b9746dc7000685565a0b", + "txHash": "0x3cbc73f22690f96f2ef7118d4c1e5928e5d9c7be22d4abf0b4848edae4a50909" } }, "L2GraphToken": { - "address": "0x18C924BD5E8b83b47EFaDD632b7178E2Fd36073D", - "initArgs": ["0xEfc519BEd6a43a14f1BBBbA9e796C4931f7A5540"], + "address": "0xD3757675af31c0c8f1d3aEcF6466DBf8e44605be", + "initArgs": ["0x48Ed1128A24fe9053E3F0C8358eC43D86A18c121"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x7ec14b524141af953959b537c1acbea9b49b12ee906563a6172123b09ab3d1f6", + "txHash": "0x72bb217118be1aff57428d994bee68124015800d847af7749486bc1aeff59146", "proxy": true, "implementation": { - "address": "0x5dcAcF820D7b9F0640e8a23a5a857675A774C34a", + "address": "0xAB6E31dB739Ffa6F0260eEF53Fdd88196d33f977", "creationCodeHash": "0x6c4146427aafa7375a569154be95c8c931bf83aab0315706dd78bdf79c889e4c", "runtimeCodeHash": "0x004371d1d80011906953dcba17c648503fc94b94e1e0365c8d8c706ff91f93e9", - "txHash": "0xb748498a2ebc90e20dc8da981be832f4e00f08ea9ff289880738705e45d6aeca" + "txHash": "0x6e4d648c4d85f974fbb90743b2969c8ff756448cfd7919a412931a21a2044dbf" } }, "GraphCurationToken": { - "address": "0x2B757ad83e4ed51ecaE8D4dC9AdE8E3Fa29F7BdC", + "address": "0x837D5a86a4c717a7FE75FD3B644e323ED97d2c55", "creationCodeHash": "0x1ee42ee271cefe20c33c0de904501e618ac4b56debca67c634d0564cecea9ff2", "runtimeCodeHash": "0x340e8f378c0117b300f3ec255bc5c3a273f9ab5bd2940fa8eb3b5065b21f86dc", - "txHash": "0x1aa753cd01fa4505c71f6866dae35faee723d181141ed91b6e5cf3082ee90f9b" + "txHash": "0xfcc7242629c4d4313a9fc417277ebdfa0ae774a8f45066db9d661cd6c39c359d" }, "ServiceRegistry": { - "address": "0x07ECDD4278D83Cd2425cA86256634f666b659e53", - "initArgs": ["0x7f734E995010Aa8d28b912703093d532C37b6EAb"], + "address": "0xe9ECfe44280c23E3614016Ace80b72e9A40486Ad", + "initArgs": ["0x6b070868542E1b8D5399a0eFd7E742080cf300b9"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x8a13420fdc91139297ab1497fbf5b443c156bbc7b9d2a1ac97fb9f23abde2723", + "txHash": "0x3c807d0731a47d05e6e9f2a641fbc2b8e5d6377d4d357fe9d539616a6d3a98a7", "proxy": true, "implementation": { - "address": "0xd18D4B4e84eA4713E04060c93bD079A974BE6C4a", - "creationCodeHash": "0x50808e8cce93cf78a23c9e6dd7984356bd2bd93be30b358982909140dd61f6ff", - "runtimeCodeHash": "0xaef79c87f7e80107c0dc568cf1f8950459b5174ee3aa565ec487556a655e71db", - "txHash": "0x2d6043d89a5f5c4f3d0df0f50264ab7efebc898be0b5d358a00715ba9f657a89" + "address": "0x0F8209eeC0FfE352c252B5B4b1Eeb04100E1b1d0", + "creationCodeHash": "0x296d23581b42c9a0e43c36ff54cd30bb9f2d7d0da4c319baa19c1444b323fa83", + "runtimeCodeHash": "0xa4512f526b488673b9ffc778cd0250a56e28d3f75d4fe1653d838ad606a647fe", + "txHash": "0x69ae05cacf951d9bbec0490e5bdfaa5c2a3f3b34b32b7ee885264feefec630d8" } }, - "Curation": { - "address": "0x7080AAcC4ADF4b1E72615D6eb24CDdE40a04f6Ca", + "L2Curation": { + "address": "0xC4f505088b5E0465EECf795B3D4b08c696114E78", "initArgs": [ - "0x7f734E995010Aa8d28b912703093d532C37b6EAb", - "0x71319060b9fdeD6174b6368bE04F9A1b7c9aCe48", - "0x2B757ad83e4ed51ecaE8D4dC9AdE8E3Fa29F7BdC", - "1000000", + "0x6b070868542E1b8D5399a0eFd7E742080cf300b9", + "0x837D5a86a4c717a7FE75FD3B644e323ED97d2c55", "10000", "1000000000000000000" ], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x2e5744fa4eca56cf6902e27fcc0509487f39bdb0d29b9eb0181db986235289a0", + "txHash": "0xb8d1e04b29a10f229a1f1bcc44b381c2f7c30f0bd46f02e5ce75d375f32ef363", "proxy": true, "implementation": { - "address": "0xDA6c9d39b49c3d41CaC2030c6B75b40Efea09817", - "creationCodeHash": "0xa5fa77df71a72c5aadba812345978c291c5fa1a3a23129b6eba3a38ac85d8b5d", - "runtimeCodeHash": "0x1d265e9f658778b48a0247cfef79bfc9304d1faa1f1e085f2fea85629f68e2d5", - "txHash": "0x815eda87a2599d6f2c7458c7b164e7307d05018f0dd72073a50971d424313377" + "address": "0x9bDd83bdA33b75354c96916566d8C102A0770db6", + "creationCodeHash": "0xc3a534c27c86cbb6e33a8ee75b7d8ed9ff8cc56a6afb9ed3e549ae994d8e119d", + "runtimeCodeHash": "0xddf4318db627bd664b0c1e6c8b5903f354f24735c7c1ff0e66db7bbacfae11c9", + "txHash": "0x4920d43efb5f007e02aaec918d93678f4c3be0f2dd5a85cdeb7c492fc78bc1bc" } }, "SubgraphNFTDescriptor": { - "address": "0x30545f313bD2eb0F85E4f808Ae4D2C016efE78b2", + "address": "0xc18d12A8726BB192E30F427535a8c5a48c522595", "creationCodeHash": "0xf16e8ff11d852eea165195ac9e0dfa00f98e48f6ce3c77c469c7df9bf195b651", "runtimeCodeHash": "0x39583196f2bcb85789b6e64692d8c0aa56f001c46f0ca3d371abbba2c695860f", - "txHash": "0x060839a09e89cbd47adbb8c04cc76b21a00785600a4e8b44939dd928391777e1" + "txHash": "0x561046f3755f319052220b54bbfa496fa8ab10c5ea66e5e74be2be9e784dcab5" }, "SubgraphNFT": { - "address": "0x5571D8FE183AD1367dF21eE9968690f0Eabdc593", - "constructorArgs": ["0xEfc519BEd6a43a14f1BBBbA9e796C4931f7A5540"], + "address": "0x6F36eBAC060432115Bf5435B00E7b7DF918D88F7", + "constructorArgs": ["0x48Ed1128A24fe9053E3F0C8358eC43D86A18c121"], "creationCodeHash": "0xc1e58864302084de282dffe54c160e20dd96c6cfff45e00e6ebfc15e04136982", "runtimeCodeHash": "0x7216e736a8a8754e88688fbf5c0c7e9caf35c55ecc3a0c5a597b951c56cf7458", - "txHash": "0xc11917ffedda6867648fa2cb62cca1df3c0ed485a0a0885284e93a2c5d33455c" + "txHash": "0xf3b7666eec153478fb6bb0aba767cdb404f9595eb85cb56296bbe2f247384235" }, - "GNS": { - "address": "0x6bf9104e054537301cC23A1023Ca30A6Df79eB21", + "L2GNS": { + "address": "0x01bA532c0c963cfe411F05179E9E729a050A270C", "initArgs": [ - "0x7f734E995010Aa8d28b912703093d532C37b6EAb", - "0x71319060b9fdeD6174b6368bE04F9A1b7c9aCe48", - "0x5571D8FE183AD1367dF21eE9968690f0Eabdc593" + "0x6b070868542E1b8D5399a0eFd7E742080cf300b9", + "0x6F36eBAC060432115Bf5435B00E7b7DF918D88F7" ], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x3c2509730e06249d970818319bb507185d4fdea13d5600cef87928a718950c19", + "txHash": "0x9242ea41a8605474c4d823683c3d8073e47515263ee6b52176187af2965109c6", "proxy": true, "implementation": { - "address": "0x7eCb82A9Cf9B370d3fC2Ef66E38F38EDFAeaa125", - "creationCodeHash": "0xb0be24e926bb24420bb5a8d3f7bd0b70a545fdddbf8cb177a42478adf4435aae", - "runtimeCodeHash": "0x4cb62b9def5b691e43ed06808b18efe682fcefb7739909be0d6c87f1eda724cd", - "txHash": "0xf1d41fc99ed716a0c890ea62e13ee108ddcb4ecfc74efb715a4ef05605ce449b" + "address": "0x0E58e81dDEBd60D1723F9370F08e417AAe16820E", + "creationCodeHash": "0x5b20456892c055c8d96c1c23439409aaa05e2cac7a0d43ce9434b1b4d4c64dbd", + "runtimeCodeHash": "0x99c9d74ec9ceeb9026a08fc756293934b28f62607fcd5cb408449d36bbff455c", + "txHash": "0xe96761695876637d727728e2f95a009c7edbf4206ba914bdc6a0efd1f179b242" } }, - "Staking": { - "address": "0xcd549d0C43d915aEB21d3a331dEaB9B7aF186D26", + "StakingExtension": { + "address": "0x1d23fA1A9257da1Ff4d1cdA5de146F62108D4cef", + "creationCodeHash": "0x6e94435d4cdd8508d87c54ef0dead97918783a1e0701771bb13ea07685697f2d", + "runtimeCodeHash": "0x5f358ee8930c845d3b28ee9c46c638980d34171ea1db177bb15d61c7d8ed9453", + "txHash": "0x7d21dd100479eee9b0080747f54409a5d2fd2c8cd0f871fa6e4f6e7b5097514a" + }, + "L2Staking": { + "address": "0x6465cD301a62078Ef6339753b52BF6Bd5522da61", "initArgs": [ - "0x7f734E995010Aa8d28b912703093d532C37b6EAb", + "0x6b070868542E1b8D5399a0eFd7E742080cf300b9", "100000000000000000000000", "6646", "10000", @@ -1100,41 +351,42 @@ "12", "16", "77", - "100" + "100", + "0x1d23fA1A9257da1Ff4d1cdA5de146F62108D4cef" ], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0xc98ebdd0a80b97ef8f6305903ef6496a7781db76a5b1b3c3c3b2b10dbd9a7af5", + "txHash": "0x8d74d49b3058b075da2911ac48463ddbaef03370b88b73c30018622685a9471e", "proxy": true, "implementation": { - "address": "0x8E56ee65Ed613f2AecA8898D19497D288601bdeb", - "creationCodeHash": "0x75b63ef816627315c635cae7f95917764e2cb797496280cdeaa9b3230bf7f7bc", - "runtimeCodeHash": "0x461ccf91c7c6188c94c6df430b6954dfd9c5cc2a79a5e4db21422e11b663d319", - "txHash": "0xb9ce53dafab3dcaad25b24d9f998888225103265bd2d84cb1545b4e06e96e3b6", + "address": "0x477043fD633c3E2441e3b8AE76e6f8Fb38C2FE11", + "creationCodeHash": "0x4dd0bf194a1ac73e6495f26752ed68c13b4a0544a7102b62c11bc9507bdd7a54", + "runtimeCodeHash": "0x822969db855e4c63c9fccdbf0bfe7d16909e78374d4b99e02a3b08522d69c51b", + "txHash": "0xd8f75e4c60f36daeaf29f3ae4980acc98587790e390637c0ef50226b0b13f58b", "libraries": { - "LibCobbDouglas": "0x86f0f6cd9a38A851E3AB8f110be06B77C199eC1F" + "LibCobbDouglas": "0x4C784507B529b20Bb5e1e7B9e9B2a84d27F019FD" } } }, "RewardsManager": { - "address": "0x5F06ABd1CfAcF7AE99530D7Fed60E085f0B15e8D", - "initArgs": ["0x7f734E995010Aa8d28b912703093d532C37b6EAb"], + "address": "0x4764A9AdBC4b77Cf6ec8B1eA399Cf864AEF5D6c3", + "initArgs": ["0x6b070868542E1b8D5399a0eFd7E742080cf300b9"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0xd4cfa95475e9e867fb24babd6a00a5b6b01d2267533e2412986aa1ff94d51c02", + "txHash": "0x71e49cdf8ef12b3b9fa26889f1f2d3e0a2a04835963cde6919118ea5886579bc", "proxy": true, "implementation": { - "address": "0x80b54Ba64d8a207785969d9ae0dA984EfE8D10dF", - "creationCodeHash": "0x98aaabec491a17401ca37209db0613c91285de061e859574526f841a4dd60c4a", - "runtimeCodeHash": "0x2795a83531898957014373bd4595f1f9a381ecfaf787bdfc64380563af06f06a", - "txHash": "0xb4bc7ae32ec98394c448f8773bdd3049ab83e236acb6823a7a322d88ecfbfd99" + "address": "0x85Ad3382621d041F1d6713Fb82F77aB731B61134", + "creationCodeHash": "0x5c93fcb74170b85ce5e63c09ddba2eb762db75375f004aa7b1b53c8113cc7613", + "runtimeCodeHash": "0xe18a3e48c5055b110fdd1bd8e3147ba28106c2b39616362d9a2f362221040107", + "txHash": "0x63d796c3f729706624804a031bc5d584736eadbd9b4fbfcb9406d5cd5858930a" } }, "DisputeManager": { - "address": "0x16DEF7E0108A5467A106dbD7537f8591f470342E", + "address": "0xFFc59B3AfA142453a6Fe5137CDB7E87d8614ea5A", "initArgs": [ - "0x7f734E995010Aa8d28b912703093d532C37b6EAb", - "0xF89688d5d44d73cc4dE880857A3940487076e5A4", + "0x6b070868542E1b8D5399a0eFd7E742080cf300b9", + "0xed42A803C9f0bAE74bF0E63f36FF0Ae7FF38beF7", "10000000000000000000000", "500000", "25000", @@ -1142,43 +394,40 @@ ], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x70188c9243c2226ac793ac8c0a9eecd76c9b44e53f7f6f97fa177a34808421a0", + "txHash": "0x1e921caa24bcb75464fe820788d9c73b97fe9f35ae1719ef8197dc195395b4b3", "proxy": true, "implementation": { - "address": "0x39aEdA1d6ea3B62b76C7c439beBfFCb5369a175C", - "creationCodeHash": "0x2e77ad7a1627b6e04bece0fe18b3ab543ef4a2d6914f2e5e640b2c8175aca3a8", - "runtimeCodeHash": "0x0186afe711eff4ceea28620d091e3c6034fd15be05894119c74a38b020e3a554", - "txHash": "0x4efbd28e55866c0292309964f47bd805922ad417e5980e14e055ad693024582d" + "address": "0x3e4262742f290E779D4b7cA4492B4eb9287341CF", + "creationCodeHash": "0xa8879b9893390aac4e0df919d3e9233a63728b39e2dcb1eb3d1162df0ca62b86", + "runtimeCodeHash": "0xefc118c16dc91266c9a594e01444357feaaf357c00d03860ddf56dfee2626bbd", + "txHash": "0xb69221f1a94d7c0f51b152c4dd488a9747e440b551985f455dffca12ad01e8e7" } }, "AllocationExchange": { - "address": "0x61809D6Cde07f27D2fcDCb67a42d0Af1988Be5e8", + "address": "0x80BA0e50410AE7ee9A651DeD29716a3450992C13", "constructorArgs": [ - "0x18C924BD5E8b83b47EFaDD632b7178E2Fd36073D", - "0xcd549d0C43d915aEB21d3a331dEaB9B7aF186D26", - "0x05F359b1319f1Ca9b799CB6386F31421c2c49dBA", - "0xD06f366678AE139a94b2AaC2913608De568F1D03" + "0xD3757675af31c0c8f1d3aEcF6466DBf8e44605be", + "0x6465cD301a62078Ef6339753b52BF6Bd5522da61", + "0x91cEc32a6975265cF96A43f4209F39274cBEc088", + "0xDfaf2F953899c3Fae4aa4979727fd9F441E006b2" ], - "creationCodeHash": "0x96c5b59557c161d80f1617775a7b9537a89b0ecf2258598b3a37724be91ae80a", - "runtimeCodeHash": "0xed3d9cce65ddfa8a237d4d7d294ffdb13a082e0adcda3bbd313029cfae1365f3", - "txHash": "0x1df63329a21dca69d20e03c076dd89c350970d35319eeefab028cebbc78d29dc" + "creationCodeHash": "0x4bc830095b2703a3c3cddcaa2ce66bc656d049d5c5684d4a6afa52d048a618cc", + "runtimeCodeHash": "0xe8cc4e5291b60c1a94727ed70ffc9675fd64d72554200f75c855a8e71cfe0f11", + "txHash": "0x5f08ee18d48eaf322318f50dadf3c79f771df7dda59f749c7b0e90cd4238ee22" }, "L2GraphTokenGateway": { - "address": "0xef2757855d2802bA53733901F90C91645973f743", - "initArgs": ["0x7f734E995010Aa8d28b912703093d532C37b6EAb"], + "address": "0xb190742488d6b43D2E0Ec6e3F86364c8A1a53570", + "initArgs": ["0x6b070868542E1b8D5399a0eFd7E742080cf300b9"], "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", - "txHash": "0x47bde4e3ad0bc077897a3de65058c4b7dd710aa447ec25942f716321cbdc590d", + "txHash": "0x19b15ec580284c910ac2419058bd0b6f7a43e20f3609e59594d2406de32785fc", "proxy": true, "implementation": { - "address": "0xc68cd0d2ca533232Fd86D6e48b907338B2E0a74A", - "creationCodeHash": "0xbd52455bd8b14bfc27af623388fe2f9e06ddd4c4be3fc06c51558a912de91770", - "runtimeCodeHash": "0x29e47f693053f978d6b2ac0a327319591bf5b5e8a6e6c0744b8afcc0250bf667", - "txHash": "0xf68a5e1e516ee9a646f19bbe4d58336fdfcf5fc859f84cdac5e68b00bcd3a09a" + "address": "0xC679Bdbc8B7954306B340458F1F426DBB3aAf1e1", + "creationCodeHash": "0xba767f3ee199746710f31fb56566218040ab22d1916d94a94176c1a470387c52", + "runtimeCodeHash": "0x722dc1462f2cf00c0b43c94f844b1cfdd65e26d76e4f279202cefe3e16674ee3", + "txHash": "0xeb0da2c2d48fe64f935f82e146d3b76595dc16b7ab6754546685736f5a1d1029" } - }, - "IEthereumDIDRegistry": { - "address": "0x8FFfcD6a85D29E9C33517aaf60b16FE4548f517E" } } } diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 335b6fa32..079e1d731 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -29,8 +29,9 @@ let allContracts = [ 'Curation', 'SubgraphNFTDescriptor', 'SubgraphNFT', - 'GNS', - 'Staking', + 'L1GNS', + 'StakingExtension', + 'L1Staking', 'RewardsManager', 'DisputeManager', 'AllocationExchange', @@ -40,17 +41,17 @@ let allContracts = [ const l2Contracts = [ 'GraphProxyAdmin', - 'BancorFormula', 'Controller', 'EpochManager', 'L2GraphToken', 'GraphCurationToken', 'ServiceRegistry', - 'Curation', + 'L2Curation', 'SubgraphNFTDescriptor', 'SubgraphNFT', - 'GNS', - 'Staking', + 'L2GNS', + 'StakingExtension', + 'L2Staking', 'RewardsManager', 'DisputeManager', 'AllocationExchange', diff --git a/cli/commands/protocol/configure-bridge.ts b/cli/commands/protocol/configure-bridge.ts index 057ebc653..f142bd7f9 100644 --- a/cli/commands/protocol/configure-bridge.ts +++ b/cli/commands/protocol/configure-bridge.ts @@ -39,6 +39,20 @@ export const configureL1Bridge = async (cli: CLIEnvironment, cliArgs: CLIArgs): l1Inbox.address, l1Router.address, ]) + + const l1GNS = cli.contracts.L1GNS + logger.info('L1 GNS address: ' + l1GNS.address) + const l2GNS = l2AddressBook.getEntry('L2GNS') + logger.info('L2 GNS address: ' + l2GNS.address) + await sendTransaction(cli.wallet, l1GNS, 'setCounterpartGNSAddress', [l2GNS.address]) + await sendTransaction(cli.wallet, gateway, 'addToCallhookAllowlist', [l1GNS.address]) + + const l1Staking = cli.contracts.L1Staking + logger.info('L1 Staking address: ' + l1Staking.address) + const l2Staking = l2AddressBook.getEntry('L2Staking') + logger.info('L2 Staking address: ' + l2Staking.address) + await sendTransaction(cli.wallet, l1Staking, 'setCounterpartStakingAddress', [l2Staking.address]) + await sendTransaction(cli.wallet, gateway, 'addToCallhookAllowlist', [l1Staking.address]) } export const configureL2Bridge = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { @@ -68,6 +82,18 @@ export const configureL2Bridge = async (cli: CLIEnvironment, cliArgs: CLIArgs): logger.info('L2 Router address: ' + l2Router.address) await sendTransaction(cli.wallet, gateway, 'setL2Router', [l2Router.address]) + const l1GNS = l1AddressBook.getEntry('L1GNS') + const l2GNS = cli.contracts['L2GNS'] + logger.info('L1 GNS address: ' + l1GNS.address) + logger.info('L2 GNS address: ' + l2GNS.address) + await sendTransaction(cli.wallet, l2GNS, 'setCounterpartGNSAddress', [l1GNS.address]) + + const l1Staking = l1AddressBook.getEntry('L1Staking') + const l2Staking = cli.contracts['L2Staking'] + logger.info('L1 Staking address: ' + l1Staking.address) + logger.info('L2 Staking address: ' + l2Staking.address) + await sendTransaction(cli.wallet, l2Staking, 'setCounterpartStakingAddress', [l1Staking.address]) + logger.info('L2 Gateway address: ' + gateway.address) await sendTransaction(cli.wallet, token, 'setGateway', [gateway.address]) } diff --git a/cli/commands/protocol/get.ts b/cli/commands/protocol/get.ts index 439b539b4..801e468a7 100644 --- a/cli/commands/protocol/get.ts +++ b/cli/commands/protocol/get.ts @@ -34,7 +34,7 @@ export const gettersList = { 'epochs-length': { contract: 'EpochManager', name: 'epochLength' }, 'epochs-current': { contract: 'EpochManager', name: 'currentEpoch' }, // Rewards - 'rewards-issuance-rate': { contract: 'RewardsManager', name: 'issuanceRate' }, + 'rewards-issuance-per-block': { contract: 'RewardsManager', name: 'issuancePerBlock' }, 'subgraph-availability-oracle': { contract: 'RewardsManager', name: 'subgraphAvailabilityOracle', diff --git a/cli/commands/protocol/set.ts b/cli/commands/protocol/set.ts index 966d07897..a7850601a 100644 --- a/cli/commands/protocol/set.ts +++ b/cli/commands/protocol/set.ts @@ -39,7 +39,7 @@ export const settersList = { // Epochs 'epochs-length': { contract: 'EpochManager', name: 'setEpochLength' }, // Rewards - 'rewards-issuance-rate': { contract: 'RewardsManager', name: 'setIssuanceRate' }, + 'rewards-issuance-per-block': { contract: 'RewardsManager', name: 'setIssuancePerBlock' }, 'subgraph-availability-oracle': { contract: 'RewardsManager', name: 'setSubgraphAvailabilityOracle', diff --git a/cli/contracts.ts b/cli/contracts.ts index c36e83567..0c8da1b91 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -18,11 +18,14 @@ import { getContractAt } from './network' import { EpochManager } from '../build/types/EpochManager' import { DisputeManager } from '../build/types/DisputeManager' -import { Staking } from '../build/types/Staking' +import { L1Staking } from '../build/types/L1Staking' +import { L2Staking } from '../build/types/L2Staking' 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 +40,21 @@ 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' +import { IL1Staking } from '../build/types/IL1Staking' +import { IL2Staking } from '../build/types/IL2Staking' +import { Interface } from 'ethers/lib/utils' +import { loadArtifact } from './artifacts' export interface NetworkContracts { EpochManager: EpochManager DisputeManager: DisputeManager - Staking: Staking + Staking: IL1Staking | IL2Staking 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 +69,10 @@ export interface NetworkContracts { BridgeEscrow: BridgeEscrow L2GraphToken: L2GraphToken L2GraphTokenGateway: L2GraphTokenGateway + L1GNS: L1GNS + L2GNS: L2GNS + L1Staking: IL1Staking + L2Staking: IL2Staking } export const loadAddressBookContract = ( @@ -91,6 +104,15 @@ export const loadContracts = ( contract.connect = getWrappedConnect(contract, contractName) contract = wrapCalls(contract, contractName) } + if (contractName == 'L1Staking') { + // Hack the contract into behaving like an IL1Staking + const iface = new Interface(loadArtifact('IL1Staking').abi) + contract = new Contract(contract.address, iface) as unknown as IL1Staking + } else if (contractName == 'L2Staking') { + // Hack the contract into behaving like an IL2Staking + const iface = new Interface(loadArtifact('IL2Staking').abi) + contract = new Contract(contract.address, iface) as unknown as IL2Staking + } contracts[contractName] = contract if (signerOrProvider) { @@ -101,6 +123,21 @@ 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 == 'L2Staking') { + contracts['Staking'] = contracts[contractName] + } + if (signerOrProvider && chainIdIsL2(chainId) && contractName == 'L2Curation') { + contracts['Curation'] = contracts[contractName] + } + if (signerOrProvider && !chainIdIsL2(chainId) && contractName == 'L1GNS') { + contracts['GNS'] = contracts[contractName] + } + if (signerOrProvider && !chainIdIsL2(chainId) && contractName == 'L1Staking') { + contracts['Staking'] = contracts[contractName] + } } catch (err) { logger.warn(`Could not load contract ${contractName} - ${err.message}`) } diff --git a/cli/network.ts b/cli/network.ts index 9a0618ff3..5422eac73 100644 --- a/cli/network.ts +++ b/cli/network.ts @@ -18,6 +18,9 @@ import { AddressBook } from './address-book' import { loadArtifact } from './artifacts' import { defaultOverrides } from './defaults' import { GraphToken } from '../build/types/GraphToken' +import { Interface } from 'ethers/lib/utils' +import { IL1Staking } from '../build/types/IL1Staking' +import { IL2Staking } from '../build/types/IL2Staking' const { keccak256, randomBytes, parseUnits, hexlify } = utils @@ -197,7 +200,7 @@ export const deployContract = async ( // Deploy const factory = getContractFactory(name, libraries) - const contract = await factory.connect(sender).deploy(...args) + let contract = await factory.connect(sender).deploy(...args) const txHash = contract.deployTransaction.hash logger.info(`> Deploy ${name}, txHash: ${txHash}`) await sender.provider.waitForTransaction(txHash) @@ -209,6 +212,15 @@ export const deployContract = async ( logger.info(`= RuntimeCodeHash: ${runtimeCodeHash}`) logger.info(`${name} has been deployed to address: ${contract.address}`) + if (name == 'L1Staking') { + // Hack the contract into behaving like an IL1Staking + const iface = new Interface(loadArtifact('IL1Staking').abi) + contract = new Contract(contract.address, iface, sender) as unknown as IL1Staking + } else if (name == 'L2Staking') { + // Hack the contract into behaving like an IL2Staking + const iface = new Interface(loadArtifact('IL2Staking').abi) + contract = new Contract(contract.address, iface, sender) as unknown as IL2Staking + } return { contract, creationCodeHash, runtimeCodeHash, txHash, libraries } } diff --git a/config/graph.arbitrum-goerli-scratch-3.yml b/config/graph.arbitrum-goerli-scratch-3.yml new file mode 100644 index 000000000..15cd4db20 --- /dev/null +++ b/config/graph.arbitrum-goerli-scratch-3.yml @@ -0,0 +1,151 @@ +general: + arbitrator: &arbitrator "0xed42A803C9f0bAE74bF0E63f36FF0Ae7FF38beF7" # Arbitration Council (TODO: update) + governor: &governor "0xf6De21Ce446B47d7599BC6554Eaa9EDF05Dfe731" # Graph Council (TODO: update) + authority: &authority "0xDfaf2F953899c3Fae4aa4979727fd9F441E006b2" # Authority that signs payment vouchers + availabilityOracle: &availabilityOracle "0xC241E5A6e35432bf340B9853F025D90031a9E8ef" # Subgraph Availability Oracle (TODO: update) + pauseGuardian: &pauseGuardian "0x5753d3c0c08C2Cee7be69eBbd058299faB0ea966" # Protocol pause guardian (TODO: update) + allocationExchangeOwner: &allocationExchangeOwner "0x91cEc32a6975265cF96A43f4209F39274cBEc088" # Allocation Exchange owner (TODO: update) + +contracts: + Controller: + calls: + - fn: "setContractProxy" + id: "0xe6876326c1291dfcbbd3864a6816d698cd591defc7aa2153d7f9c4c04016c89f" # keccak256('Curation') + contractAddress: "${{L2Curation.address}}" + - fn: "setContractProxy" + id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') + contractAddress: "${{L2GNS.address}}" + - fn: "setContractProxy" + id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') + contractAddress: "${{DisputeManager.address}}" + - fn: "setContractProxy" + id: "0xc713c3df6d14cdf946460395d09af88993ee2b948b1a808161494e32c5f67063" # keccak256('EpochManager') + contractAddress: "${{EpochManager.address}}" + - fn: "setContractProxy" + id: "0x966f1e8d8d8014e05f6ec4a57138da9be1f7c5a7f802928a18072f7c53180761" # keccak256('RewardsManager') + contractAddress: "${{RewardsManager.address}}" + - fn: "setContractProxy" + id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') + contractAddress: "${{L2Staking.address}}" + - fn: "setContractProxy" + id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') + contractAddress: "${{L2GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setPauseGuardian" + pauseGuardian: *pauseGuardian + - fn: "transferOwnership" + owner: *governor + GraphProxyAdmin: + calls: + - fn: "transferOwnership" + owner: *governor + ServiceRegistry: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "syncAllContracts" + EpochManager: + proxy: true + init: + controller: "${{Controller.address}}" + lengthInBlocks: 554 # length in hours = lengthInBlocks*13/60/60 (~13 second blocks) + L2GraphToken: + proxy: true + init: + owner: "${{Env.deployer}}" + calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" + - fn: "renounceMinter" + - fn: "transferOwnership" + owner: *governor + L2Curation: + proxy: true + init: + controller: "${{Controller.address}}" + curationTokenMaster: "${{GraphCurationToken.address}}" + curationTaxPercentage: 10000 # in parts per million + minimumCurationDeposit: "1000000000000000000" # in wei + calls: + - fn: "syncAllContracts" + DisputeManager: + proxy: true + init: + controller: "${{Controller.address}}" + arbitrator: *arbitrator + minimumDeposit: "10000000000000000000000" # in wei + fishermanRewardPercentage: 500000 # in parts per million + idxSlashingPercentage: 25000 # in parts per million + qrySlashingPercentage: 25000 # in parts per million + calls: + - fn: "syncAllContracts" + L2GNS: + proxy: true + init: + controller: "${{Controller.address}}" + subgraphNFT: "${{SubgraphNFT.address}}" + calls: + - fn: "approveAll" + - fn: "syncAllContracts" + SubgraphNFT: + init: + governor: "${{Env.deployer}}" + calls: + - fn: "setTokenDescriptor" + tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" + - fn: "setMinter" + minter: "${{L2GNS.address}}" + - fn: "transferOwnership" + owner: *governor + L2Staking: + proxy: true + init: + controller: "${{Controller.address}}" + minimumIndexerStake: "100000000000000000000000" # in wei + thawingPeriod: 6646 # in blocks + protocolPercentage: 10000 # in parts per million + curationPercentage: 100000 # in parts per million + channelDisputeEpochs: 2 # in epochs + maxAllocationEpochs: 4 # in epochs + delegationUnbondingPeriod: 12 # in epochs + delegationRatio: 16 # delegated stake to indexer stake multiplier + rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator + rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + extensionImpl: "${{StakingExtension.address}}" + calls: + - fn: "setDelegationTaxPercentage" + delegationTaxPercentage: 5000 # parts per million + - fn: "setSlasher" + slasher: "${{DisputeManager.address}}" + allowed: true + - fn: "setAssetHolder" + assetHolder: "${{AllocationExchange.address}}" + allowed: true + - fn: "syncAllContracts" + RewardsManager: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "setSubgraphAvailabilityOracle" + subgraphAvailabilityOracle: *availabilityOracle + - fn: "syncAllContracts" + AllocationExchange: + init: + graphToken: "${{L2GraphToken.address}}" + staking: "${{L2Staking.address}}" + governor: *allocationExchangeOwner + authority: *authority + calls: + - fn: "approveAll" + L2GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "syncAllContracts" + - fn: "setPauseGuardian" + pauseGuardian: *pauseGuardian diff --git a/config/graph.arbitrum-goerli.yml b/config/graph.arbitrum-goerli.yml index ed1bdb33c..161761205 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}}" @@ -26,7 +26,7 @@ contracts: contractAddress: "${{RewardsManager.address}}" - fn: "setContractProxy" id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') - contractAddress: "${{Staking.address}}" + contractAddress: "${{L2Staking.address}}" - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{L2GraphToken.address}}" @@ -57,16 +57,16 @@ contracts: init: owner: "${{Env.deployer}}" calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" - 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 calls: @@ -82,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" @@ -98,10 +97,10 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L2GNS.address}}" - fn: "transferOwnership" owner: *governor - Staking: + L2Staking: proxy: true init: controller: "${{Controller.address}}" @@ -115,6 +114,7 @@ contracts: delegationRatio: 16 # delegated stake to indexer stake multiplier rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + extensionImpl: "${{StakingExtension.address}}" calls: - fn: "setDelegationTaxPercentage" delegationTaxPercentage: 5000 # parts per million @@ -136,7 +136,7 @@ contracts: AllocationExchange: init: graphToken: "${{L2GraphToken.address}}" - staking: "${{Staking.address}}" + staking: "${{L2Staking.address}}" governor: *allocationExchangeOwner authority: *authority calls: diff --git a/config/graph.arbitrum-localhost.yml b/config/graph.arbitrum-localhost.yml index a17755fbc..3382d88d6 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}}" @@ -26,7 +26,7 @@ contracts: contractAddress: "${{RewardsManager.address}}" - fn: "setContractProxy" id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') - contractAddress: "${{Staking.address}}" + contractAddress: "${{L2Staking.address}}" - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{L2GraphToken.address}}" @@ -57,16 +57,16 @@ contracts: init: owner: "${{Env.deployer}}" calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" - 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 calls: @@ -82,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" @@ -98,10 +97,10 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L2GNS.address}}" - fn: "transferOwnership" owner: *governor - Staking: + L2Staking: proxy: true init: controller: "${{Controller.address}}" @@ -115,6 +114,7 @@ contracts: delegationRatio: 16 # delegated stake to indexer stake multiplier rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + extensionImpl: "${{StakingExtension.address}}" calls: - fn: "setDelegationTaxPercentage" delegationTaxPercentage: 5000 # parts per million @@ -136,7 +136,7 @@ contracts: AllocationExchange: init: graphToken: "${{L2GraphToken.address}}" - staking: "${{Staking.address}}" + staking: "${{L2Staking.address}}" governor: *allocationExchangeOwner authority: *authority calls: diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index ce32bc324..1b881061f 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}}" @@ -26,7 +26,7 @@ contracts: contractAddress: "${{RewardsManager.address}}" - fn: "setContractProxy" id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') - contractAddress: "${{Staking.address}}" + contractAddress: "${{L2Staking.address}}" - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{L2GraphToken.address}}" @@ -57,16 +57,16 @@ contracts: init: owner: "${{Env.deployer}}" calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" - 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 calls: @@ -82,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" @@ -98,10 +97,10 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L2GNS.address}}" - fn: "transferOwnership" owner: *governor - Staking: + L2Staking: proxy: true init: controller: "${{Controller.address}}" @@ -115,6 +114,7 @@ contracts: delegationRatio: 16 # delegated stake to indexer stake multiplier rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + extensionImpl: "${{StakingExtension.address}}" calls: - fn: "setDelegationTaxPercentage" delegationTaxPercentage: 5000 # parts per million @@ -136,7 +136,7 @@ contracts: AllocationExchange: init: graphToken: "${{L2GraphToken.address}}" - staking: "${{Staking.address}}" + staking: "${{L2Staking.address}}" governor: *allocationExchangeOwner authority: *authority calls: diff --git a/config/graph.goerli-scratch-3.yml b/config/graph.goerli-scratch-3.yml new file mode 100644 index 000000000..68be67bad --- /dev/null +++ b/config/graph.goerli-scratch-3.yml @@ -0,0 +1,162 @@ +general: + arbitrator: &arbitrator "0x54d1a1020C5bc929A603DC2161BF6C71ae05553E" # Arbitration Council + governor: &governor "0x68C18C161C46D2E6097980e0D89aB35f28c365E2" # Graph Council + authority: &authority "0x142eb17fCd30Bc31Dfd69312c0f4E5E329Cc5a3C" # Authority that signs payment vouchers + availabilityOracle: &availabilityOracle "0xBB3Fbf50896943Cd05550b7d59cB6905c54053df" # Subgraph Availability Oracle + pauseGuardian: &pauseGuardian "0xF7470147bF547108c58197DbcBfD58D931e7908f" # Protocol pause guardian + allocationExchangeOwner: &allocationExchangeOwner "0xcfF86De5ccc3f27574C63E1CaBD97CdD840Ee798" # Allocation Exchange owner + +contracts: + Controller: + calls: + - fn: "setContractProxy" + id: "0xe6876326c1291dfcbbd3864a6816d698cd591defc7aa2153d7f9c4c04016c89f" # keccak256('Curation') + contractAddress: "${{Curation.address}}" + - fn: "setContractProxy" + id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') + contractAddress: "${{L1GNS.address}}" + - fn: "setContractProxy" + id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') + contractAddress: "${{DisputeManager.address}}" + - fn: "setContractProxy" + id: "0xc713c3df6d14cdf946460395d09af88993ee2b948b1a808161494e32c5f67063" # keccak256('EpochManager') + contractAddress: "${{EpochManager.address}}" + - fn: "setContractProxy" + id: "0x966f1e8d8d8014e05f6ec4a57138da9be1f7c5a7f802928a18072f7c53180761" # keccak256('RewardsManager') + contractAddress: "${{RewardsManager.address}}" + - fn: "setContractProxy" + id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') + contractAddress: "${{L1Staking.address}}" + - fn: "setContractProxy" + id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') + contractAddress: "${{GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L1GraphTokenGateway.address}}" + - fn: "setPauseGuardian" + pauseGuardian: *pauseGuardian + - fn: "transferOwnership" + owner: *governor + GraphProxyAdmin: + calls: + - fn: "transferOwnership" + owner: *governor + ServiceRegistry: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "syncAllContracts" + EpochManager: + proxy: true + init: + controller: "${{Controller.address}}" + lengthInBlocks: 554 # length in hours = lengthInBlocks*13/60/60 (~13 second blocks) + GraphToken: + init: + initialSupply: "10000000000000000000000000000" # in wei + calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" + - fn: "addMinter" + minter: "${{L1GraphTokenGateway.address}}" + - fn: "renounceMinter" + - fn: "transferOwnership" + owner: *governor + Curation: + proxy: true + init: + controller: "${{Controller.address}}" + bondingCurve: "${{BancorFormula.address}}" + curationTokenMaster: "${{GraphCurationToken.address}}" + reserveRatio: 500000 # in parts per million + curationTaxPercentage: 10000 # in parts per million + minimumCurationDeposit: "1000000000000000000" # in wei + calls: + - fn: "syncAllContracts" + DisputeManager: + proxy: true + init: + controller: "${{Controller.address}}" + arbitrator: *arbitrator + minimumDeposit: "10000000000000000000000" # in wei + fishermanRewardPercentage: 500000 # in parts per million + idxSlashingPercentage: 25000 # in parts per million + qrySlashingPercentage: 25000 # in parts per million + calls: + - fn: "syncAllContracts" + L1GNS: + proxy: true + init: + controller: "${{Controller.address}}" + subgraphNFT: "${{SubgraphNFT.address}}" + calls: + - fn: "approveAll" + - fn: "syncAllContracts" + SubgraphNFT: + init: + governor: "${{Env.deployer}}" + calls: + - fn: "setTokenDescriptor" + tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" + - fn: "setMinter" + minter: "${{L1GNS.address}}" + - fn: "transferOwnership" + owner: *governor + L1Staking: + proxy: true + init: + controller: "${{Controller.address}}" + minimumIndexerStake: "100000000000000000000000" # in wei + thawingPeriod: 6646 # in blocks + protocolPercentage: 10000 # in parts per million + curationPercentage: 100000 # in parts per million + channelDisputeEpochs: 2 # in epochs + maxAllocationEpochs: 4 # in epochs + delegationUnbondingPeriod: 12 # in epochs + delegationRatio: 16 # delegated stake to indexer stake multiplier + rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator + rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + extensionImpl: "${{StakingExtension.address}}" + calls: + - fn: "setDelegationTaxPercentage" + delegationTaxPercentage: 5000 # parts per million + - fn: "setSlasher" + slasher: "${{DisputeManager.address}}" + allowed: true + - fn: "setAssetHolder" + assetHolder: "${{AllocationExchange.address}}" + allowed: true + - fn: "syncAllContracts" + RewardsManager: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "setIssuancePerBlock" + issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 + - fn: "setSubgraphAvailabilityOracle" + subgraphAvailabilityOracle: *availabilityOracle + - fn: "syncAllContracts" + AllocationExchange: + init: + graphToken: "${{GraphToken.address}}" + staking: "${{L1Staking.address}}" + governor: *allocationExchangeOwner + authority: *authority + calls: + - fn: "approveAll" + L1GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "syncAllContracts" + - fn: "setPauseGuardian" + pauseGuardian: *pauseGuardian + BridgeEscrow: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "syncAllContracts" diff --git a/config/graph.goerli.yml b/config/graph.goerli.yml index 856d73f86..43900a631 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}}" @@ -26,7 +26,7 @@ contracts: contractAddress: "${{RewardsManager.address}}" - fn: "setContractProxy" id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') - contractAddress: "${{Staking.address}}" + contractAddress: "${{L1Staking.address}}" - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{GraphToken.address}}" @@ -58,6 +58,8 @@ contracts: calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "addMinter" + minter: "${{L1GraphTokenGateway.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -83,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" @@ -99,10 +100,10 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L1GNS.address}}" - fn: "transferOwnership" owner: *governor - Staking: + L1Staking: proxy: true init: controller: "${{Controller.address}}" @@ -116,6 +117,7 @@ contracts: delegationRatio: 16 # delegated stake to indexer stake multiplier rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + extensionImpl: "${{StakingExtension.address}}" calls: - fn: "setDelegationTaxPercentage" delegationTaxPercentage: 5000 # parts per million @@ -131,15 +133,15 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000011247641700" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + - fn: "setIssuancePerBlock" + issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" AllocationExchange: init: graphToken: "${{GraphToken.address}}" - staking: "${{Staking.address}}" + staking: "${{L1Staking.address}}" governor: *allocationExchangeOwner authority: *authority calls: diff --git a/config/graph.localhost.yml b/config/graph.localhost.yml index b93f88dad..55fcd24d1 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}}" @@ -26,7 +26,7 @@ contracts: contractAddress: "${{RewardsManager.address}}" - fn: "setContractProxy" id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') - contractAddress: "${{Staking.address}}" + contractAddress: "${{L1Staking.address}}" - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{GraphToken.address}}" @@ -58,6 +58,8 @@ contracts: calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "addMinter" + minter: "${{L1GraphTokenGateway.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -83,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" @@ -99,10 +100,10 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L1GNS.address}}" - fn: "transferOwnership" owner: *governor - Staking: + L1Staking: proxy: true init: controller: "${{Controller.address}}" @@ -116,6 +117,7 @@ contracts: delegationRatio: 16 # delegated stake to indexer stake multiplier rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + extensionImpl: "${{StakingExtension.address}}" calls: - fn: "setDelegationTaxPercentage" delegationTaxPercentage: 5000 # parts per million @@ -131,15 +133,15 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000011247641700" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + - fn: "setIssuancePerBlock" + issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" AllocationExchange: init: graphToken: "${{GraphToken.address}}" - staking: "${{Staking.address}}" + staking: "${{L1Staking.address}}" governor: *allocationExchangeOwner authority: *authority calls: diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index 872f3e31a..176ab7254 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}}" @@ -26,7 +26,7 @@ contracts: contractAddress: "${{RewardsManager.address}}" - fn: "setContractProxy" id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') - contractAddress: "${{Staking.address}}" + contractAddress: "${{L1Staking.address}}" - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{GraphToken.address}}" @@ -58,6 +58,8 @@ contracts: calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "addMinter" + minter: "${{L1GraphTokenGateway.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -83,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" @@ -99,10 +100,10 @@ contracts: - fn: "setTokenDescriptor" tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" - minter: "${{GNS.address}}" + minter: "${{L1GNS.address}}" - fn: "transferOwnership" owner: *governor - Staking: + L1Staking: proxy: true init: controller: "${{Controller.address}}" @@ -116,6 +117,7 @@ contracts: delegationRatio: 16 # delegated stake to indexer stake multiplier rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + extensionImpl: "${{StakingExtension.address}}" calls: - fn: "setDelegationTaxPercentage" delegationTaxPercentage: 5000 # parts per million @@ -131,15 +133,15 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000011247641700" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + - fn: "setIssuancePerBlock" + issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" AllocationExchange: init: graphToken: "${{GraphToken.address}}" - staking: "${{Staking.address}}" + staking: "${{L1Staking.address}}" governor: *allocationExchangeOwner authority: *authority calls: 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..3e8d94360 --- /dev/null +++ b/contracts/discovery/L1GNS.sol @@ -0,0 +1,196 @@ +// 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 migrating 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. + * @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(!subgraphMigratedToL2[_subgraphID], "ALREADY_DONE"); + + SubgraphData storage subgraphData = _getSubgraphOrRevert(_subgraphID); + // This is just like onlySubgraphAuth, but we want it to run after the subgraphMigratedToL2 check + // to revert with a nicer message in that case: + require(ownerOf(_subgraphID) == msg.sender, "GNS: Must be authorized"); + subgraphMigratedToL2[_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 migrate 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 + * migrated 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 migration was finished in L2, + * but if it wasn't, the tokens will be sent to the beneficiary in L2. + * @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(subgraphMigratedToL2[_subgraphID], "!MIGRATED"); + + // 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..64163636e --- /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 migrated to L2 + mapping(uint256 => bool) public subgraphMigratedToL2; + /// @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 094d99611..d946b78a7 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -40,6 +40,14 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess address public escrow; /// Addresses for which this mapping is true are allowed to send callhooks in outbound transfers mapping(address => bool) public callhookAllowlist; + /// Total amount minted from L2 + uint256 public totalMintedFromL2; + /// Accumulated allowance for tokens minted from L2 at lastL2MintAllowanceUpdateBlock + uint256 public accumulatedL2MintAllowanceSnapshot; + /// Block at which new L2 allowance starts accumulating + uint256 public lastL2MintAllowanceUpdateBlock; + /// New L2 mint allowance per block + uint256 public l2MintAllowancePerBlock; /// Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 event DepositInitiated( @@ -71,6 +79,14 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess event AddedToCallhookAllowlist(address newAllowlisted); /// Emitted when an address is removed from the callhook allowlist event RemovedFromCallhookAllowlist(address notAllowlisted); + /// Emitted when the L2 mint allowance per block is updated + event L2MintAllowanceUpdated( + uint256 accumulatedL2MintAllowanceSnapshot, + uint256 l2MintAllowancePerBlock, + uint256 lastL2MintAllowanceUpdateBlock + ); + /// Emitted when tokens are minted due to an incoming transfer from L2 + event TokensMintedFromL2(uint256 amount); /** * @dev Allows a function to be called only by the gateway's L2 counterpart. @@ -182,6 +198,56 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess emit RemovedFromCallhookAllowlist(_notAllowlisted); } + /** + * @dev Updates the L2 mint allowance per block + * It is meant to be called _after_ the issuancePerBlock is updated in L2. + * The caller should provide the new issuance per block and the block at which it was updated, + * the function will automatically compute the values so that the bridge's mint allowance + * correctly tracks the maximum rewards minted in L2. + * @param _l2IssuancePerBlock New issuancePerBlock that has been set in L2 + * @param _updateBlockNum L1 Block number at which issuancePerBlock was updated in L2 + */ + function updateL2MintAllowance(uint256 _l2IssuancePerBlock, uint256 _updateBlockNum) + external + onlyGovernor + { + require(_updateBlockNum < block.number, "BLOCK_MUST_BE_PAST"); + require(_updateBlockNum > lastL2MintAllowanceUpdateBlock, "BLOCK_MUST_BE_INCREMENTING"); + accumulatedL2MintAllowanceSnapshot = accumulatedL2MintAllowanceAtBlock(_updateBlockNum); + lastL2MintAllowanceUpdateBlock = _updateBlockNum; + l2MintAllowancePerBlock = _l2IssuancePerBlock; + emit L2MintAllowanceUpdated( + accumulatedL2MintAllowanceSnapshot, + l2MintAllowancePerBlock, + lastL2MintAllowanceUpdateBlock + ); + } + + /** + * @dev Manually sets the parameters used to compute the L2 mint allowance + * The use of this function is not recommended, use updateL2MintAllowance instead; + * this one is only meant to be used as a backup recovery if a previous call to + * updateL2MintAllowance was done with incorrect values. + * @param _accumulatedL2MintAllowanceSnapshot Accumulated L2 mint allowance at L1 block _lastL2MintAllowanceUpdateBlock + * @param _l2MintAllowancePerBlock L2 issuance per block since block number _lastL2MintAllowanceUpdateBlock + * @param _lastL2MintAllowanceUpdateBlock L1 Block number at which issuancePerBlock was last updated in L2 + */ + function setL2MintAllowanceParametersManual( + uint256 _accumulatedL2MintAllowanceSnapshot, + uint256 _l2MintAllowancePerBlock, + uint256 _lastL2MintAllowanceUpdateBlock + ) external onlyGovernor { + require(_lastL2MintAllowanceUpdateBlock < block.number, "BLOCK_MUST_BE_PAST"); + accumulatedL2MintAllowanceSnapshot = _accumulatedL2MintAllowanceSnapshot; + l2MintAllowancePerBlock = _l2MintAllowancePerBlock; + lastL2MintAllowanceUpdateBlock = _lastL2MintAllowanceUpdateBlock; + emit L2MintAllowanceUpdated( + accumulatedL2MintAllowanceSnapshot, + l2MintAllowancePerBlock, + lastL2MintAllowanceUpdateBlock + ); + } + /** * @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox. * The tokens are escrowed by the gateway until they are withdrawn back to L1. @@ -277,8 +343,10 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess require(_l1Token == address(token), "TOKEN_NOT_GRT"); uint256 escrowBalance = token.balanceOf(escrow); - // If the bridge doesn't have enough tokens, something's very wrong! - require(_amount <= escrowBalance, "BRIDGE_OUT_OF_FUNDS"); + if (_amount > escrowBalance) { + // This will revert if trying to mint more than allowed + _mintFromL2(_amount.sub(escrowBalance)); + } token.transferFrom(escrow, _to, _amount); emit WithdrawalFinalized(_l1Token, _from, _to, 0, _amount); @@ -381,4 +449,42 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); return (from, maxSubmissionCost, extraData); } + + /** + * @dev Get the accumulated L2 mint allowance at a particular block number + * @param _blockNum Block at which allowance will be computed + * @return The accumulated GRT amount that can be minted from L2 at the specified block + */ + function accumulatedL2MintAllowanceAtBlock(uint256 _blockNum) public view returns (uint256) { + require(_blockNum >= lastL2MintAllowanceUpdateBlock, "INVALID_BLOCK_FOR_MINT_ALLOWANCE"); + return + accumulatedL2MintAllowanceSnapshot.add( + l2MintAllowancePerBlock.mul(_blockNum.sub(lastL2MintAllowanceUpdateBlock)) + ); + } + + /** + * @dev Mint new L1 tokens coming from L2 + * This will check if the amount to mint is within the L2's mint allowance, and revert otherwise. + * The tokens will be sent to the bridge escrow (from where they will then be sent to the destinatary + * of the current inbound transfer). + * @param _amount Number of tokens to mint + */ + function _mintFromL2(uint256 _amount) internal { + // If we're trying to mint more than allowed, something's gone terribly wrong + // (either the L2 issuance is wrong, or the Arbitrum bridge has been compromised) + require(_l2MintAmountAllowed(_amount), "INVALID_L2_MINT_AMOUNT"); + totalMintedFromL2 = totalMintedFromL2.add(_amount); + graphToken().mint(escrow, _amount); + emit TokensMintedFromL2(_amount); + } + + /** + * @dev Check if minting a certain amount of tokens from L2 is within allowance + * @param _amount Number of tokens that would be minted + * @return true if minting those tokens is allowed, or false if it would be over allowance + */ + function _l2MintAmountAllowed(uint256 _amount) internal view returns (bool) { + return (totalMintedFromL2.add(_amount) <= accumulatedL2MintAllowanceAtBlock(block.number)); + } } 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..bd8806538 --- /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 migration). + * @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..7c493a612 --- /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 = 1e18; // 1 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 migration). + * @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..227625928 --- /dev/null +++ b/contracts/l2/discovery/IL2GNS.sol @@ -0,0 +1,42 @@ +// 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 SubgraphL2MigrationData struct holds information + * about a subgraph related to its migration from L1 to L2. + */ + struct SubgraphL2MigrationData { + 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; // Migration finished on L2 side + uint256 subgraphReceivedOnL2BlockNumber; // Block number when the subgraph was received on L2 + } + + /** + * @notice Finish a subgraph migration from L1. + * The subgraph must have been previously sent through the bridge + * using the sendSubgraphToL2 function on L1GNS. + * @param _subgraphID 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 finishSubgraphMigrationFromL1( + uint256 _subgraphID, + bytes32 _subgraphDeploymentID, + bytes32 _subgraphMetadata, + bytes32 _versionMetadata + ) external; +} diff --git a/contracts/l2/discovery/L2GNS.sol b/contracts/l2/discovery/L2GNS.sol new file mode 100644 index 000000000..dd31c4726 --- /dev/null +++ b/contracts/l2/discovery/L2GNS.sol @@ -0,0 +1,298 @@ +// 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 migrated from L1. + */ +contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { + using SafeMathUpgradeable for uint256; + + /// @dev Emitted when a subgraph is received from L1 through the bridge + event SubgraphReceivedFromL1( + uint256 indexed _subgraphID, + address indexed _owner, + uint256 _tokens + ); + /// @dev Emitted when a subgraph migration from L1 is finalized, so the subgraph is published + event SubgraphMigrationFinalized(uint256 indexed _subgraphID); + /// @dev Emitted when the L1 balance for a curator has been claimed + event CuratorBalanceReceived(uint256 _subgraphID, address _l2Curator, uint256 _tokens); + /// @dev Emitted when the L1 balance for a curator has been returned to the beneficiary. + /// This can happen if the subgraph migration was not finished when the curator's tokens arrived. + event CuratorBalanceReturnedToBeneficiary( + uint256 _subgraphID, + address _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 migration 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 subgraphID, address beneficiary) = abi.decode( + _data, + (uint8, uint256, address) + ); + + if (code == uint8(L1MessageCodes.RECEIVE_SUBGRAPH_CODE)) { + _receiveSubgraphFromL1(subgraphID, beneficiary, _amount); + } else if (code == uint8(L1MessageCodes.RECEIVE_CURATOR_BALANCE_CODE)) { + _mintSignalFromL1(subgraphID, beneficiary, _amount); + } else { + revert("INVALID_CODE"); + } + } + + /** + * @notice Finish a subgraph migration from L1. + * The subgraph must have been previously sent through the bridge + * using the sendSubgraphToL2 function on L1GNS. + * @param _subgraphID 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 finishSubgraphMigrationFromL1( + uint256 _subgraphID, + bytes32 _subgraphDeploymentID, + bytes32 _subgraphMetadata, + bytes32 _versionMetadata + ) external override notPartialPaused onlySubgraphAuth(_subgraphID) { + IL2GNS.SubgraphL2MigrationData storage migratedData = subgraphL2MigrationData[_subgraphID]; + SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); + require(migratedData.subgraphReceivedOnL2BlockNumber != 0, "INVALID_SUBGRAPH"); + require(!migratedData.l2Done, "ALREADY_DONE"); + migratedData.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, migratedData.tokens); + uint256 nSignal = vSignalToNSignal(_subgraphID, vSignal); + + subgraphData.disabled = false; + subgraphData.vSignal = vSignal; + subgraphData.nSignal = nSignal; + subgraphData.curatorNSignal[msg.sender] = nSignal; + subgraphData.subgraphDeploymentID = _subgraphDeploymentID; + // Set the token metadata + _setSubgraphMetadata(_subgraphID, _subgraphMetadata); + + emit SubgraphPublished(_subgraphID, _subgraphDeploymentID, fixedReserveRatio); + emit SubgraphUpgraded( + _subgraphID, + subgraphData.vSignal, + migratedData.tokens, + _subgraphDeploymentID + ); + emit SubgraphVersionUpdated(_subgraphID, _subgraphDeploymentID, _versionMetadata); + emit SubgraphMigrationFinalized(_subgraphID); + } + + /** + * @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); + } + + /** + * @dev Receive a subgraph from L1. + * This function will initialize a subgraph received through the bridge, + * and store the migration data so that it's finalized later using finishSubgraphMigrationFromL1. + * @param _subgraphID Subgraph ID + * @param _subgraphOwner Owner of the subgraph + * @param _tokens Tokens to be deposited in the subgraph + */ + function _receiveSubgraphFromL1( + uint256 _subgraphID, + address _subgraphOwner, + uint256 _tokens + ) internal { + IL2GNS.SubgraphL2MigrationData storage migratedData = subgraphL2MigrationData[_subgraphID]; + SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); + + subgraphData.reserveRatioDeprecated = fixedReserveRatio; + // The subgraph will be disabled until finishSubgraphMigrationFromL1 is called + subgraphData.disabled = true; + + migratedData.tokens = _tokens; + migratedData.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 finishSubgraphMigrationFromL1. + _mintNFT(_subgraphOwner, _subgraphID); + + emit SubgraphReceivedFromL1(_subgraphID, _subgraphOwner, _tokens); + } + + /** + * @notice Deposit GRT into a subgraph and mint signal, using tokens received from L1. + * If the subgraph migration 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 _subgraphID Subgraph ID + * @param _curator Curator address + * @param _tokensIn The amount of tokens the nameCurator wants to deposit + */ + function _mintSignalFromL1( + uint256 _subgraphID, + address _curator, + uint256 _tokensIn + ) internal { + IL2GNS.SubgraphL2MigrationData storage migratedData = subgraphL2MigrationData[_subgraphID]; + SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); + + // If subgraph migration wasn't finished, we should send the tokens to the curator + if (!migratedData.l2Done || subgraphData.disabled) { + graphToken().transfer(_curator, _tokensIn); + emit CuratorBalanceReturnedToBeneficiary(_subgraphID, _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(_subgraphID, 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(_subgraphID, _curator, nSignal, vSignal, _tokensIn); + emit CuratorBalanceReceived(_subgraphID, _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..9a2951d53 --- /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 migration from L1 to L2 + mapping(uint256 => IL2GNS.SubgraphL2MigrationData) public subgraphL2MigrationData; + /// @dev Storage gap to keep storage slots fixed in future versions + uint256[50] private __gap; +} diff --git a/contracts/l2/staking/IL2Staking.sol b/contracts/l2/staking/IL2Staking.sol new file mode 100644 index 000000000..2b4d3c083 --- /dev/null +++ b/contracts/l2/staking/IL2Staking.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.6.12 <0.8.0; +pragma abicoder v2; + +import { IStaking } from "../../staking/IStaking.sol"; +import { IL2StakingBase } from "./IL2StakingBase.sol"; + +/** + * @title Interface for the L2 Staking contract + * @notice This is the interface that should be used when interacting with the L2 Staking contract. + * It extends the IStaking interface with the functions that are specific to L2, adding the callhook receiver + * to receive migrated stake and delegation from L1. + * @dev Note that L2Staking doesn't actually inherit this interface. This is because of + * the custom setup of the Staking contract where part of the functionality is implemented + * in a separate contract (StakingExtension) to which calls are delegated through the fallback function. + */ +interface IL2Staking is IStaking, IL2StakingBase { + /// @dev Message codes for the L1 -> L2 bridge callhook + enum L1MessageCodes { + RECEIVE_INDEXER_STAKE_CODE, + RECEIVE_DELEGATION_CODE + } + + /// @dev Encoded message struct when receiving indexer stake through the bridge + struct ReceiveIndexerStakeData { + address indexer; + } + + /// @dev Encoded message struct when receiving delegation through the bridge + struct ReceiveDelegationData { + address indexer; + address delegator; + } +} diff --git a/contracts/l2/staking/IL2StakingBase.sol b/contracts/l2/staking/IL2StakingBase.sol new file mode 100644 index 000000000..edf19874d --- /dev/null +++ b/contracts/l2/staking/IL2StakingBase.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; + +/** + * @title Base interface for the L2Staking contract. + * @notice This interface is used to define the callhook receiver interface that is implemented by L2Staking. + * @dev Note it includes only the L2-specific functionality, not the full IStaking interface. + */ +interface IL2StakingBase is ICallhookReceiver { + // Nothing to see here +} diff --git a/contracts/l2/staking/L2Staking.sol b/contracts/l2/staking/L2Staking.sol new file mode 100644 index 000000000..ad1181b92 --- /dev/null +++ b/contracts/l2/staking/L2Staking.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { Staking } from "../../staking/Staking.sol"; +import { IL2StakingBase } from "./IL2StakingBase.sol"; +import { IL2Staking } from "./IL2Staking.sol"; +import { Stakes } from "../../staking/libs/Stakes.sol"; + +/** + * @title L2Staking contract + * @dev This contract is the L2 variant of the Staking contract. It adds a function + * to receive an indexer's stake or delegation from L1. Note that this contract inherits Staking, + * which uses a StakingExtension contract to implement the full IStaking interface through delegatecalls. + */ +contract L2Staking is Staking, IL2StakingBase { + using SafeMath for uint256; + using Stakes for Stakes.Indexer; + + /** + * @dev Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator + * gets `shares` for the delegation pool proportionally to the tokens staked. + * This is copied from IStakingExtension, but we can't inherit from it because we + * don't implement the full interface here. + */ + event StakeDelegated( + address indexed indexer, + address indexed delegator, + uint256 tokens, + uint256 shares + ); + + /** + * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. + */ + modifier onlyL2Gateway() { + require(msg.sender == address(graphTokenGateway()), "ONLY_GATEWAY"); + _; + } + + /** + * @notice Receive ETH into the L2Staking contract: this will always revert + * @dev This function is only here to prevent ETH from being sent to the contract + */ + receive() external payable { + revert("RECEIVE_ETH_NOT_ALLOWED"); + } + + /** + * @notice Receive tokens with a callhook from the bridge. + * @dev The encoded _data can contain information about an indexer's stake + * or a delegator's delegation. + * See L1MessageCodes in IL2Staking for the supported messages. + * @param _from Token sender in L1 + * @param _amount Amount of tokens that were transferred + * @param _data ABI-encoded callhook data which must include a uint8 code and either a ReceiveIndexerStakeData or ReceiveDelegationData struct. + */ + function onTokenTransfer( + address _from, + uint256 _amount, + bytes calldata _data + ) external override notPartialPaused onlyL2Gateway { + require(_from == counterpartStakingAddress, "ONLY_L1_STAKING_THROUGH_BRIDGE"); + (uint8 code, bytes memory functionData) = abi.decode(_data, (uint8, bytes)); + + if (code == uint8(IL2Staking.L1MessageCodes.RECEIVE_INDEXER_STAKE_CODE)) { + IL2Staking.ReceiveIndexerStakeData memory indexerData = abi.decode( + functionData, + (IL2Staking.ReceiveIndexerStakeData) + ); + _receiveIndexerStake(_amount, indexerData); + } else if (code == uint8(IL2Staking.L1MessageCodes.RECEIVE_DELEGATION_CODE)) { + IL2Staking.ReceiveDelegationData memory delegationData = abi.decode( + functionData, + (IL2Staking.ReceiveDelegationData) + ); + _receiveDelegation(_amount, delegationData); + } else { + revert("INVALID_CODE"); + } + } + + /** + * @dev Receive an Indexer's stake from L1. + * The specified amount is added to the indexer's stake; the indexer's + * address is specified in the _indexerData struct. + * @param _amount Amount of tokens that were transferred + * @param _indexerData struct containing the indexer's address + */ + function _receiveIndexerStake( + uint256 _amount, + IL2Staking.ReceiveIndexerStakeData memory _indexerData + ) internal { + address indexer = _indexerData.indexer; + __stakes[indexer].deposit(_amount); + emit StakeDeposited(indexer, _amount); + } + + /** + * @dev Receive a Delegator's delegation from L1. + * The specified amount is added to the delegator's delegation; the delegator's + * address and the indexer's address are specified in the _delegationData struct. + * Note that no delegation tax is applied here. + * @param _amount Amount of tokens that were transferred + * @param _delegationData struct containing the delegator's address and the indexer's address + */ + function _receiveDelegation( + uint256 _amount, + IL2Staking.ReceiveDelegationData memory _delegationData + ) internal { + // Get the delegation pool of the indexer + DelegationPool storage pool = __delegationPools[_delegationData.indexer]; + Delegation storage delegation = pool.delegators[_delegationData.delegator]; + + // Calculate shares to issue (without applying any delegation tax) + uint256 shares = (pool.tokens == 0) ? _amount : _amount.mul(pool.shares).div(pool.tokens); + + // Update the delegation pool + pool.tokens = pool.tokens.add(_amount); + pool.shares = pool.shares.add(shares); + + // Update the individual delegation + delegation.shares = delegation.shares.add(shares); + + emit StakeDelegated(_delegationData.indexer, _delegationData.delegator, _amount, shares); + } +} diff --git a/contracts/rewards/IRewardsManager.sol b/contracts/rewards/IRewardsManager.sol index dc17c8ba8..87d3c2455 100644 --- a/contracts/rewards/IRewardsManager.sol +++ b/contracts/rewards/IRewardsManager.sol @@ -15,7 +15,7 @@ interface IRewardsManager { // -- Config -- - function setIssuanceRate(uint256 _issuanceRate) external; + function setIssuancePerBlock(uint256 _issuancePerBlock) external; function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 6d2e78965..e259d50b8 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -28,11 +28,10 @@ import "./IRewardsManager.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; - uint256 private constant TOKEN_DECIMALS = 1e18; - uint256 private constant MIN_ISSUANCE_RATE = 1e18; + uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; // -- Events -- @@ -76,29 +75,28 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa // -- Config -- /** - * @dev Sets the issuance rate. - * The issuance rate is defined as a percentage increase of the total supply per block. - * This means that it needs to be greater than 1.0, any number under 1.0 is not - * allowed and an issuance rate of 1.0 means no issuance. - * To accommodate a high precision the issuance rate is expressed in wei. - * @param _issuanceRate Issuance rate expressed in wei + * @dev Sets the GRT issuance per block. + * The issuance is defined as a fixed amount of rewards per block in GRT. + * Whenever this function is called in layer 2, the updateL2MintAllowance function + * _must_ be called on the L1GraphTokenGateway in L1, to ensure the bridge can mint the + * right amount of tokens. + * @param _issuancePerBlock Issuance expressed in GRT per block (scaled by 1e18) */ - function setIssuanceRate(uint256 _issuanceRate) external override onlyGovernor { - _setIssuanceRate(_issuanceRate); + function setIssuancePerBlock(uint256 _issuancePerBlock) external override onlyGovernor { + _setIssuancePerBlock(_issuancePerBlock); } /** - * @dev Sets the issuance rate. - * @param _issuanceRate Issuance rate + * @dev Sets the GRT issuance per block. + * The issuance is defined as a fixed amount of rewards per block in GRT. + * @param _issuancePerBlock Issuance expressed in GRT per block (scaled by 1e18) */ - function _setIssuanceRate(uint256 _issuanceRate) private { - require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); - - // Called since `issuance rate` will change + function _setIssuancePerBlock(uint256 _issuancePerBlock) private { + // Called since `issuance per block` will change updateAccRewardsPerSignal(); - issuanceRate = _issuanceRate; - emit ParameterUpdated("issuanceRate"); + issuancePerBlock = _issuancePerBlock; + emit ParameterUpdated("issuancePerBlock"); } /** @@ -188,18 +186,13 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Gets the issuance of rewards per signal since last updated. * - * Compound interest formula: `a = p(1 + r/n)^nt` - * The formula is simplified with `n = 1` as we apply the interest once every time step. - * The `r` is passed with +1 included. So for 10% instead of 0.1 it is 1.1 - * The simplified formula is `a = p * r^t` + * Linear formula: `x = r * t` * * Notation: * t: time steps are in blocks since last updated - * p: total supply of GRT tokens - * a: inflated amount of total supply for the period `t` when interest `r` is applied - * x: newly accrued rewards token for the period `t` + * x: newly accrued rewards tokens for the period `t` * - * @return newly accrued rewards per signal since last update + * @return newly accrued rewards per signal since last update, scaled by FIXED_POINT_SCALING_FACTOR */ function getNewRewardsPerSignal() public view override returns (uint256) { // Calculate time steps @@ -208,9 +201,8 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa if (t == 0) { return 0; } - - // Zero issuance under a rate of 1.0 - if (issuanceRate <= MIN_ISSUANCE_RATE) { + // ...or if issuance is zero + if (issuancePerBlock == 0) { return 0; } @@ -221,16 +213,11 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return 0; } - uint256 r = issuanceRate; - uint256 p = tokenSupplySnapshot; - uint256 a = p.mul(_pow(r, t, TOKEN_DECIMALS)).div(TOKEN_DECIMALS); - - // New issuance of tokens during time steps - uint256 x = a.sub(p); + uint256 x = issuancePerBlock.mul(t); // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number - return x.mul(TOKEN_DECIMALS).div(signalledTokens); + return x.mul(FIXED_POINT_SCALING_FACTOR).div(signalledTokens); } /** @@ -262,7 +249,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa ? getAccRewardsPerSignal() .sub(subgraph.accRewardsPerSignalSnapshot) .mul(subgraphSignalledTokens) - .div(TOKEN_DECIMALS) + .div(FIXED_POINT_SCALING_FACTOR) : 0; return subgraph.accRewardsForSubgraph.add(newRewards); } @@ -294,9 +281,9 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return (0, accRewardsForSubgraph); } - uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph.mul(TOKEN_DECIMALS).div( - subgraphAllocatedTokens - ); + uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph + .mul(FIXED_POINT_SCALING_FACTOR) + .div(subgraphAllocatedTokens); return ( subgraph.accRewardsPerAllocatedToken.add(newRewardsPerAllocatedToken), accRewardsForSubgraph @@ -307,14 +294,13 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Updates the accumulated rewards per signal and save checkpoint block number. - * Must be called before `issuanceRate` or `total signalled GRT` changes + * Must be called before `issuancePerBlock` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() * @return Accumulated rewards per signal */ function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; - tokenSupplySnapshot = graphToken().totalSupply(); return accRewardsPerSignal; } @@ -395,7 +381,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa uint256 _endAccRewardsPerAllocatedToken ) private pure returns (uint256) { uint256 newAccrued = _endAccRewardsPerAllocatedToken.sub(_startAccRewardsPerAllocatedToken); - return newAccrued.mul(_tokens).div(TOKEN_DECIMALS); + return newAccrued.mul(_tokens).div(FIXED_POINT_SCALING_FACTOR); } /** @@ -438,67 +424,4 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return rewards; } - - /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x - */ - function _pow( - uint256 x, - uint256 n, - uint256 base - ) private pure returns (uint256 z) { - assembly { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } - } - } } diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index 7626992da..821d93edb 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -8,7 +8,7 @@ import "../governance/Managed.sol"; contract RewardsManagerV1Storage is Managed { // -- State -- - uint256 public issuanceRate; + uint256 private issuanceRateDeprecated; uint256 public accRewardsPerSignal; uint256 public accRewardsPerSignalLastBlockUpdated; @@ -29,5 +29,10 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage { contract RewardsManagerV3Storage is RewardsManagerV2Storage { // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated - uint256 public tokenSupplySnapshot; + uint256 private tokenSupplySnapshotDeprecated; +} + +contract RewardsManagerV4Storage is RewardsManagerV3Storage { + // GRT issued for indexer rewards per block + uint256 public issuancePerBlock; } diff --git a/contracts/staking/IL1GraphTokenLockMigrator.sol b/contracts/staking/IL1GraphTokenLockMigrator.sol new file mode 100644 index 000000000..4184417a4 --- /dev/null +++ b/contracts/staking/IL1GraphTokenLockMigrator.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.6.12 <0.8.0; +pragma abicoder v2; + +/** + * @title Interface for the L1GraphTokenLockMigrator contract + * @dev This interface defines the function to get the migrated wallet address for a given L1 token lock wallet. + * The migrator contract is implemented in the token-distribution repo: https://github.com/graphprotocol/token-distribution/pull/64 + * and is only included here to provide support in L1Staking for the migration of stake and delegation + * owned by token lock contracts. See GIP-0046 for details: https://forum.thegraph.com/t/gip-0046-l2-migration-helpers/4023 + */ +interface IL1GraphTokenLockMigrator { + /** + * @notice Pulls ETH from an L1 wallet's account to use for L2 ticket gas. + * @dev This function is only callable by the staking contract. + * @param _l1Wallet Address of the L1 token lock wallet + * @param _amount Amount of ETH to pull from the migrator contract + */ + function pullETH(address _l1Wallet, uint256 _amount) external; + + /** + * @notice Get the L2 token lock wallet address for a given L1 token lock wallet + * @dev In the actual L1GraphTokenLockMigrator contract, this is simply the default getter for a public mapping variable. + * @param _l1Wallet Address of the L1 token lock wallet + * @return Address of the L2 token lock wallet if the wallet has an L2 counterpart, or address zero if + * the wallet doesn't have an L2 counterpart (or is not known to be a token lock wallet). + */ + function migratedWalletAddress(address _l1Wallet) external view returns (address); +} diff --git a/contracts/staking/IL1Staking.sol b/contracts/staking/IL1Staking.sol new file mode 100644 index 000000000..929218ea5 --- /dev/null +++ b/contracts/staking/IL1Staking.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.6.12 <0.8.0; +pragma abicoder v2; + +import { IStaking } from "./IStaking.sol"; +import { IL1StakingBase } from "./IL1StakingBase.sol"; + +/** + * @title Interface for the L1 Staking contract + * @notice This is the interface that should be used when interacting with the L1 Staking contract. + * It extends the IStaking interface with the functions that are specific to L1, adding the migration helpers + * to send stake and delegation to L2. + * @dev Note that L1Staking doesn't actually inherit this interface. This is because of + * the custom setup of the Staking contract where part of the functionality is implemented + * in a separate contract (StakingExtension) to which calls are delegated through the fallback function. + */ +interface IL1Staking is IStaking, IL1StakingBase { + // Nothing to see here +} diff --git a/contracts/staking/IL1StakingBase.sol b/contracts/staking/IL1StakingBase.sol new file mode 100644 index 000000000..3662acfe1 --- /dev/null +++ b/contracts/staking/IL1StakingBase.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.6.12 <0.8.0; +pragma abicoder v2; + +import { IL1GraphTokenLockMigrator } from "./IL1GraphTokenLockMigrator.sol"; + +/** + * @title Base interface for the L1Staking contract. + * @notice This interface is used to define the migration helpers that are implemented in L1Staking. + * @dev Note it includes only the L1-specific functionality, not the full IStaking interface. + */ +interface IL1StakingBase { + /// @dev Emitted when an indexer migrates their stake to L2. + /// This can happen several times as indexers can migrate partial stake. + event IndexerMigratedToL2( + address indexed indexer, + address indexed l2Indexer, + uint256 migratedStakeTokens + ); + + /// @dev Emitted when a delegator migrates their delegation to L2 + event DelegationMigratedToL2( + address indexed delegator, + address indexed l2Delegator, + address indexed indexer, + address l2Indexer, + uint256 migratedDelegationTokens + ); + + /// @dev Emitted when the L1GraphTokenLockMigrator is set + event L1GraphTokenLockMigratorSet(address l1GraphTokenLockMigrator); + + /// @dev Emitted when a delegator unlocks their tokens ahead of time because the indexer has migrated + event StakeDelegatedUnlockedDueToMigration(address indexed indexer, address indexed delegator); + + /** + * @notice Set the L1GraphTokenLockMigrator contract address + * @dev This function can only be called by the governor. + * @param _l1GraphTokenLockMigrator Address of the L1GraphTokenLockMigrator contract + */ + function setL1GraphTokenLockMigrator(IL1GraphTokenLockMigrator _l1GraphTokenLockMigrator) + external; + + /** + * @notice Send an indexer's stake to L2. + * @dev This function can only be called by the indexer (not an operator). + * It will validate that the remaining stake is sufficient to cover all the allocated + * stake, so the indexer might have to close some allocations before migrating. + * It will also check that the indexer's stake is not locked for withdrawal. + * Since the indexer address might be an L1-only contract, the function takes a beneficiary + * address that will be the indexer's address in L2. + * The caller must provide an amount of ETH to use for the L2 retryable ticket, that + * must be at least `_maxSubmissionCost + _gasPriceBid * _maxGas`. + * @param _l2Beneficiary Address of the indexer in L2. If the indexer has previously migrated stake, this must match the previously-used value. + * @param _amount Amount of stake GRT to migrate 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 + */ + function migrateStakeToL2( + address _l2Beneficiary, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external payable; + + /** + * @notice Send an indexer's stake to L2, from a GraphTokenLockWallet vesting contract. + * @dev This function can only be called by the indexer (not an operator). + * It will validate that the remaining stake is sufficient to cover all the allocated + * stake, so the indexer might have to close some allocations before migrating. + * It will also check that the indexer's stake is not locked for withdrawal. + * The L2 beneficiary for the stake will be determined by calling the L1GraphTokenLockMigrator contract, + * so the caller must have previously migrated tokens through that first + * (see GIP-0046 for details: https://forum.thegraph.com/t/gip-0046-l2-migration-helpers/4023). + * The ETH for the L2 gas will be pulled from the L1GraphTokenLockMigrator, so the owner of + * the GraphTokenLockWallet must have previously deposited at least `_maxSubmissionCost + _gasPriceBid * _maxGas` + * ETH into the L1GraphTokenLockMigrator contract (using its depositETH function). + * @param _amount Amount of stake GRT to migrate 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 + */ + function migrateLockedStakeToL2( + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external; + + /** + * @notice Send a delegator's delegated tokens to L2 + * @dev This function can only be called by the delegator. + * This function will validate that the indexer has migrated their stake using migrateStakeToL2, + * and that the delegation is not locked for undelegation. + * Since the delegator's address might be an L1-only contract, the function takes a beneficiary + * address that will be the delegator's address in L2. + * The caller must provide an amount of ETH to use for the L2 retryable ticket, that + * must be at least `_maxSubmissionCost + _gasPriceBid * _maxGas`. + * @param _indexer Address of the indexer (in L1, before migrating) + * @param _l2Beneficiary Address of the delegator 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 migrateDelegationToL2( + address _indexer, + address _l2Beneficiary, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external payable; + + /** + * @notice Send a delegator's delegated tokens to L2, for a GraphTokenLockWallet vesting contract + * @dev This function can only be called by the delegator. + * This function will validate that the indexer has migrated their stake using migrateStakeToL2, + * and that the delegation is not locked for undelegation. + * The L2 beneficiary for the delegation will be determined by calling the L1GraphTokenLockMigrator contract, + * so the caller must have previously migrated tokens through that first + * (see GIP-0046 for details: https://forum.thegraph.com/t/gip-0046-l2-migration-helpers/4023). + * The ETH for the L2 gas will be pulled from the L1GraphTokenLockMigrator, so the owner of + * the GraphTokenLockWallet must have previously deposited at least `_maxSubmissionCost + _gasPriceBid * _maxGas` + * ETH into the L1GraphTokenLockMigrator contract (using its depositETH function). + * @param _indexer Address of the indexer (in L1, before migrating) + * @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 migrateLockedDelegationToL2( + address _indexer, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external; + + /** + * @notice Unlock a delegator's delegated tokens, if the indexer has migrated + * @dev This function can only be called by the delegator. + * This function will validate that the indexer has migrated their stake using migrateStakeToL2, + * and that the indexer has no remaining stake in L1. + * The tokens must previously be locked for undelegation by calling `undelegate()`, + * and can be withdrawn with `withdrawDelegated()` immediately after calling this. + * @param _indexer Address of the indexer (in L1, before migrating) + */ + function unlockDelegationToMigratedIndexer(address _indexer) external; +} diff --git a/contracts/staking/IStaking.sol b/contracts/staking/IStaking.sol index 86942e4d1..53ec76646 100644 --- a/contracts/staking/IStaking.sol +++ b/contracts/staking/IStaking.sol @@ -3,158 +3,21 @@ pragma solidity >=0.6.12 <0.8.0; pragma abicoder v2; -import "./IStakingData.sol"; - -interface IStaking is IStakingData { - // -- Allocation Data -- - - /** - * @dev Possible states an allocation can be - * States: - * - Null = indexer == address(0) - * - Active = not Null && tokens > 0 - * - Closed = Active && closedAtEpoch != 0 - * - Finalized = Closed && closedAtEpoch + channelDisputeEpochs > now() - * - Claimed = not Null && tokens == 0 - */ - enum AllocationState { - Null, - Active, - Closed, - Finalized, - Claimed - } - - // -- Configuration -- - - function setMinimumIndexerStake(uint256 _minimumIndexerStake) external; - - function setThawingPeriod(uint32 _thawingPeriod) external; - - function setCurationPercentage(uint32 _percentage) external; - - function setProtocolPercentage(uint32 _percentage) external; - - function setChannelDisputeEpochs(uint32 _channelDisputeEpochs) external; - - function setMaxAllocationEpochs(uint32 _maxAllocationEpochs) external; - - function setRebateRatio(uint32 _alphaNumerator, uint32 _alphaDenominator) external; - - function setDelegationRatio(uint32 _delegationRatio) external; - - function setDelegationParameters( - uint32 _indexingRewardCut, - uint32 _queryFeeCut, - uint32 _cooldownBlocks - ) external; - - function setDelegationParametersCooldown(uint32 _blocks) external; - - function setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) external; - - function setDelegationTaxPercentage(uint32 _percentage) external; - - function setSlasher(address _slasher, bool _allowed) external; - - function setAssetHolder(address _assetHolder, bool _allowed) external; - - // -- Operation -- - - function setOperator(address _operator, bool _allowed) external; - - function isOperator(address _operator, address _indexer) external view returns (bool); - - // -- Staking -- - - function stake(uint256 _tokens) external; - - function stakeTo(address _indexer, uint256 _tokens) external; - - function unstake(uint256 _tokens) external; - - function slash( - address _indexer, - uint256 _tokens, - uint256 _reward, - address _beneficiary - ) external; - - function withdraw() external; - - function setRewardsDestination(address _destination) external; - - // -- Delegation -- - - function delegate(address _indexer, uint256 _tokens) external returns (uint256); - - function undelegate(address _indexer, uint256 _shares) external returns (uint256); - - function withdrawDelegated(address _indexer, address _newIndexer) external returns (uint256); - - // -- Channel management and allocations -- - - function allocate( - bytes32 _subgraphDeploymentID, - uint256 _tokens, - address _allocationID, - bytes32 _metadata, - bytes calldata _proof - ) external; - - function allocateFrom( - address _indexer, - bytes32 _subgraphDeploymentID, - uint256 _tokens, - address _allocationID, - bytes32 _metadata, - bytes calldata _proof - ) external; - - function closeAllocation(address _allocationID, bytes32 _poi) external; - - function closeAllocationMany(CloseAllocationRequest[] calldata _requests) external; - - function closeAndAllocate( - address _oldAllocationID, - bytes32 _poi, - address _indexer, - bytes32 _subgraphDeploymentID, - uint256 _tokens, - address _allocationID, - bytes32 _metadata, - bytes calldata _proof - ) external; - - function collect(uint256 _tokens, address _allocationID) external; - - function claim(address _allocationID, bool _restake) external; - - function claimMany(address[] calldata _allocationID, bool _restake) external; - - // -- Getters and calculations -- - - function hasStake(address _indexer) external view returns (bool); - - function getIndexerStakedTokens(address _indexer) external view returns (uint256); - - function getIndexerCapacity(address _indexer) external view returns (uint256); - - function getAllocation(address _allocationID) external view returns (Allocation memory); - - function getAllocationState(address _allocationID) external view returns (AllocationState); - - function isAllocation(address _allocationID) external view returns (bool); - - function getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentID) - external - view - returns (uint256); - - function getDelegation(address _indexer, address _delegator) - external - view - returns (Delegation memory); - - function isDelegator(address _indexer, address _delegator) external view returns (bool); +import { IStakingBase } from "./IStakingBase.sol"; +import { IStakingExtension } from "./IStakingExtension.sol"; +import { Stakes } from "./libs/Stakes.sol"; +import { IStakingData } from "./IStakingData.sol"; +import { Rebates } from "./libs/Rebates.sol"; +import { IMulticall } from "../base/IMulticall.sol"; +import { IManaged } from "../governance/IManaged.sol"; + +/** + * @title Interface for the Staking contract + * @notice This is the interface that should be used when interacting with the Staking contract. + * @dev Note that Staking doesn't actually inherit this interface. This is because of + * the custom setup of the Staking contract where part of the functionality is implemented + * in a separate contract (StakingExtension) to which calls are delegated through the fallback function. + */ +interface IStaking is IStakingBase, IStakingExtension, IMulticall, IManaged { + // Nothing to see here } diff --git a/contracts/staking/IStakingBase.sol b/contracts/staking/IStakingBase.sol new file mode 100644 index 000000000..406d29839 --- /dev/null +++ b/contracts/staking/IStakingBase.sol @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.6.12 <0.8.0; +pragma abicoder v2; + +import { IStakingData } from "./IStakingData.sol"; + +/** + * @title Base interface for the Staking contract. + * @dev This interface includes only what's implemented in the base Staking contract. + * It does not include the L1 and L2 specific functionality. It also does not include + * several functions that are implemented in the StakingExtension contract, and are called + * via delegatecall through the fallback function. See IStaking.sol for an interface + * that includes the full functionality. + */ +interface IStakingBase is IStakingData { + /** + * @dev Emitted when `indexer` stakes `tokens` amount. + */ + event StakeDeposited(address indexed indexer, uint256 tokens); + + /** + * @dev Emitted when `indexer` unstaked and locked `tokens` amount until `until` block. + */ + event StakeLocked(address indexed indexer, uint256 tokens, uint256 until); + + /** + * @dev Emitted when `indexer` withdrew `tokens` staked. + */ + event StakeWithdrawn(address indexed indexer, uint256 tokens); + + /** + * @dev Emitted when `indexer` allocated `tokens` amount to `subgraphDeploymentID` + * during `epoch`. + * `allocationID` indexer derived address used to identify the allocation. + * `metadata` additional information related to the allocation. + */ + event AllocationCreated( + address indexed indexer, + bytes32 indexed subgraphDeploymentID, + uint256 epoch, + uint256 tokens, + address indexed allocationID, + bytes32 metadata + ); + + /** + * @dev Emitted when `indexer` collected `tokens` amount in `epoch` for `allocationID`. + * These funds are related to `subgraphDeploymentID`. + * The `from` value is the sender of the collected funds. + */ + event AllocationCollected( + address indexed indexer, + bytes32 indexed subgraphDeploymentID, + uint256 epoch, + uint256 tokens, + address indexed allocationID, + address from, + uint256 curationFees, + uint256 rebateFees + ); + + /** + * @dev Emitted when `indexer` close an allocation in `epoch` for `allocationID`. + * An amount of `tokens` get unallocated from `subgraphDeploymentID`. + * The `effectiveAllocation` are the tokens allocated from creation to closing. + * This event also emits the POI (proof of indexing) submitted by the indexer. + * `isPublic` is true if the sender was someone other than the indexer. + */ + event AllocationClosed( + address indexed indexer, + bytes32 indexed subgraphDeploymentID, + uint256 epoch, + uint256 tokens, + address indexed allocationID, + uint256 effectiveAllocation, + address sender, + bytes32 poi, + bool isPublic + ); + + /** + * @dev Emitted when `indexer` claimed a rebate on `subgraphDeploymentID` during `epoch` + * related to the `forEpoch` rebate pool. + * The rebate is for `tokens` amount and `unclaimedAllocationsCount` are left for claim + * in the rebate pool. `delegationFees` collected and sent to delegation pool. + */ + event RebateClaimed( + address indexed indexer, + bytes32 indexed subgraphDeploymentID, + address indexed allocationID, + uint256 epoch, + uint256 forEpoch, + uint256 tokens, + uint256 unclaimedAllocationsCount, + uint256 delegationFees + ); + + /** + * @dev Emitted when `indexer` update the delegation parameters for its delegation pool. + */ + event DelegationParametersUpdated( + address indexed indexer, + uint32 indexingRewardCut, + uint32 queryFeeCut, + uint32 cooldownBlocks + ); + + /** + * @dev Emitted when `caller` set `assetHolder` address as `allowed` to send funds + * to staking contract. + */ + event AssetHolderUpdate(address indexed caller, address indexed assetHolder, bool allowed); + + /** + * @dev Emitted when `indexer` set `operator` access. + */ + event SetOperator(address indexed indexer, address indexed operator, bool allowed); + + /** + * @dev Emitted when `indexer` set an address to receive rewards. + */ + event SetRewardsDestination(address indexed indexer, address indexed destination); + + /** + * @dev Emitted when `extensionImpl` was set as the address of the StakingExtension contract + * to which extended functionality is delegated. + */ + event ExtensionImplementationSet(address extensionImpl); + + /** + * @dev Possible states an allocation can be. + * States: + * - Null = indexer == address(0) + * - Active = not Null && tokens > 0 + * - Closed = Active && closedAtEpoch != 0 + * - Finalized = Closed && closedAtEpoch + channelDisputeEpochs > now() + * - Claimed = not Null && tokens == 0 + */ + enum AllocationState { + Null, + Active, + Closed, + Finalized, + Claimed + } + + /** + * @notice Initialize this contract. + * @param _controller Address of the controller that manages this contract + * @param _minimumIndexerStake Minimum amount of tokens that an indexer must stake + * @param _thawingPeriod Number of epochs that tokens get locked after unstaking + * @param _protocolPercentage Percentage of query fees that are burned as protocol fee (in PPM) + * @param _curationPercentage Percentage of query fees that are given to curators (in PPM) + * @param _channelDisputeEpochs The period in epochs that needs to pass before fees in rebate pool can be claimed + * @param _maxAllocationEpochs The maximum number of epochs that an allocation can be active + * @param _delegationUnbondingPeriod The period in epochs that tokens get locked after undelegating + * @param _delegationRatio The ratio between an indexer's own stake and the delegation they can use + * @param _rebateAlphaNumerator The numerator of the alpha factor used to calculate the rebate + * @param _rebateAlphaDenominator The denominator of the alpha factor used to calculate the rebate + * @param _extensionImpl Address of the StakingExtension implementation + */ + function initialize( + address _controller, + uint256 _minimumIndexerStake, + uint32 _thawingPeriod, + uint32 _protocolPercentage, + uint32 _curationPercentage, + uint32 _channelDisputeEpochs, + uint32 _maxAllocationEpochs, + uint32 _delegationUnbondingPeriod, + uint32 _delegationRatio, + uint32 _rebateAlphaNumerator, + uint32 _rebateAlphaDenominator, + address _extensionImpl + ) external; + + /** + * @notice Set the address of the StakingExtension implementation. + * @dev This function can only be called by the governor. + * @param _extensionImpl Address of the StakingExtension implementation + */ + function setExtensionImpl(address _extensionImpl) external; + + /** + * @notice Set the address of the counterpart (L1 or L2) staking contract. + * @dev This function can only be called by the governor. + * @param _counterpart Address of the counterpart staking contract in the other chain, without any aliasing. + */ + function setCounterpartStakingAddress(address _counterpart) external; + + /** + * @notice Set the minimum stake needed to be an Indexer + * @dev This function can only be called by the governor. + * @param _minimumIndexerStake Minimum amount of tokens that an indexer must stake + */ + function setMinimumIndexerStake(uint256 _minimumIndexerStake) external; + + /** + * @notice Set the number of blocks that tokens get locked after unstaking + * @dev This function can only be called by the governor. + * @param _thawingPeriod Number of blocks that tokens get locked after unstaking + */ + function setThawingPeriod(uint32 _thawingPeriod) external; + + /** + * @notice Set the curation percentage of query fees sent to curators. + * @dev This function can only be called by the governor. + * @param _percentage Percentage of query fees sent to curators + */ + function setCurationPercentage(uint32 _percentage) external; + + /** + * @notice Set a protocol percentage to burn when collecting query fees. + * @dev This function can only be called by the governor. + * @param _percentage Percentage of query fees to burn as protocol fee + */ + function setProtocolPercentage(uint32 _percentage) external; + + /** + * @notice Set the period in epochs that need to pass before fees in rebate pool can be claimed. + * @dev This function can only be called by the governor. + * @param _channelDisputeEpochs Period in epochs + */ + function setChannelDisputeEpochs(uint32 _channelDisputeEpochs) external; + + /** + * @notice Set the max time allowed for indexers to allocate on a subgraph + * before others are allowed to close the allocation. + * @dev This function can only be called by the governor. + * @param _maxAllocationEpochs Allocation duration limit in epochs + */ + function setMaxAllocationEpochs(uint32 _maxAllocationEpochs) external; + + /** + * @notice Set the rebate ratio (fees to allocated stake). + * @dev This function can only be called by the governor. + * @param _alphaNumerator Numerator of `alpha` in the cobb-douglas function + * @param _alphaDenominator Denominator of `alpha` in the cobb-douglas function + */ + function setRebateRatio(uint32 _alphaNumerator, uint32 _alphaDenominator) external; + + /** + * @notice Set an address as allowed asset holder. + * @dev This function can only be called by the governor. + * @param _assetHolder Address of allowed source for state channel funds + * @param _allowed True if asset holder is allowed + */ + function setAssetHolder(address _assetHolder, bool _allowed) external; + + /** + * @notice Authorize or unauthorize an address to be an operator for the caller. + * @param _operator Address to authorize or unauthorize + * @param _allowed Whether the operator is authorized or not + */ + function setOperator(address _operator, bool _allowed) external; + + /** + * @notice Deposit tokens on the indexer's stake. + * The amount staked must be over the minimumIndexerStake. + * @param _tokens Amount of tokens to stake + */ + function stake(uint256 _tokens) external; + + /** + * @notice Deposit tokens on the Indexer stake, on behalf of the Indexer. + * The amount staked must be over the minimumIndexerStake. + * @param _indexer Address of the indexer + * @param _tokens Amount of tokens to stake + */ + function stakeTo(address _indexer, uint256 _tokens) external; + + /** + * @notice Unstake tokens from the indexer stake, lock them until the thawing period expires. + * @dev NOTE: The function accepts an amount greater than the currently staked tokens. + * If that happens, it will try to unstake the max amount of tokens it can. + * The reason for this behaviour is to avoid time conditions while the transaction + * is in flight. + * @param _tokens Amount of tokens to unstake + */ + function unstake(uint256 _tokens) external; + + /** + * @notice Withdraw indexer tokens once the thawing period has passed. + */ + function withdraw() external; + + /** + * @notice Set the destination where to send rewards for an indexer. + * @param _destination Rewards destination address. If set to zero, rewards will be restaked + */ + function setRewardsDestination(address _destination) external; + + /** + * @notice Set the delegation parameters for the caller. + * @param _indexingRewardCut Percentage of indexing rewards left for the indexer + * @param _queryFeeCut Percentage of query fees left for the indexer + * @param _cooldownBlocks Period that need to pass to update delegation parameters + */ + function setDelegationParameters( + uint32 _indexingRewardCut, + uint32 _queryFeeCut, + uint32 _cooldownBlocks + ) external; + + /** + * @notice Allocate available tokens to a subgraph deployment. + * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated + * @param _tokens Amount of tokens to allocate + * @param _allocationID The allocation identifier + * @param _metadata IPFS hash for additional information about the allocation + * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` + */ + function allocate( + bytes32 _subgraphDeploymentID, + uint256 _tokens, + address _allocationID, + bytes32 _metadata, + bytes calldata _proof + ) external; + + /** + * @notice Allocate available tokens to a subgraph deployment from and indexer's stake. + * The caller must be the indexer or the indexer's operator. + * @param _indexer Indexer address to allocate funds from. + * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated + * @param _tokens Amount of tokens to allocate + * @param _allocationID The allocation identifier + * @param _metadata IPFS hash for additional information about the allocation + * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` + */ + function allocateFrom( + address _indexer, + bytes32 _subgraphDeploymentID, + uint256 _tokens, + address _allocationID, + bytes32 _metadata, + bytes calldata _proof + ) external; + + /** + * @notice Close an allocation and free the staked tokens. + * To be eligible for rewards a proof of indexing must be presented. + * Presenting a bad proof is subject to slashable condition. + * To opt out of rewards set _poi to 0x0 + * @param _allocationID The allocation identifier + * @param _poi Proof of indexing submitted for the allocated period + */ + function closeAllocation(address _allocationID, bytes32 _poi) external; + + /** + * @notice Collect query fees from state channels and assign them to an allocation. + * Funds received are only accepted from a valid sender. + * @dev To avoid reverting on the withdrawal from channel flow this function will: + * 1) Accept calls with zero tokens. + * 2) Accept calls after an allocation passed the dispute period, in that case, all + * the received tokens are burned. + * @param _tokens Amount of tokens to collect + * @param _allocationID Allocation where the tokens will be assigned + */ + function collect(uint256 _tokens, address _allocationID) external; + + /** + * @notice Claim tokens from the rebate pool. + * @param _allocationID Allocation from where we are claiming tokens + * @param _restake True if restake fees instead of transfer to indexer + */ + function claim(address _allocationID, bool _restake) external; + + /** + * @dev Claim tokens from the rebate pool for many allocations. + * @param _allocationID Array of allocations from where we are claiming tokens + * @param _restake True if restake fees instead of transfer to indexer + */ + function claimMany(address[] calldata _allocationID, bool _restake) external; + + /** + * @notice Return true if operator is allowed for indexer. + * @param _operator Address of the operator + * @param _indexer Address of the indexer + * @return True if operator is allowed for indexer, false otherwise + */ + function isOperator(address _operator, address _indexer) external view returns (bool); + + /** + * @notice Getter that returns if an indexer has any stake. + * @param _indexer Address of the indexer + * @return True if indexer has staked tokens + */ + function hasStake(address _indexer) external view returns (bool); + + /** + * @notice Get the total amount of tokens staked by the indexer. + * @param _indexer Address of the indexer + * @return Amount of tokens staked by the indexer + */ + function getIndexerStakedTokens(address _indexer) external view returns (uint256); + + /** + * @notice Get the total amount of tokens available to use in allocations. + * This considers the indexer stake and delegated tokens according to delegation ratio + * @param _indexer Address of the indexer + * @return Amount of tokens available to allocate including delegation + */ + function getIndexerCapacity(address _indexer) external view returns (uint256); + + /** + * @notice Return the allocation by ID. + * @param _allocationID Address used as allocation identifier + * @return Allocation data + */ + function getAllocation(address _allocationID) external view returns (Allocation memory); + + /** + * @notice Return the current state of an allocation + * @param _allocationID Allocation identifier + * @return AllocationState enum with the state of the allocation + */ + function getAllocationState(address _allocationID) external view returns (AllocationState); + + /** + * @notice Return if allocationID is used. + * @param _allocationID Address used as signer by the indexer for an allocation + * @return True if allocationID already used + */ + function isAllocation(address _allocationID) external view returns (bool); + + /** + * @notice Return the total amount of tokens allocated to subgraph. + * @param _subgraphDeploymentID Deployment ID for the subgraph + * @return Total tokens allocated to subgraph + */ + function getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentID) + external + view + returns (uint256); +} diff --git a/contracts/staking/IStakingData.sol b/contracts/staking/IStakingData.sol index 348a5a7f9..6787bf76a 100644 --- a/contracts/staking/IStakingData.sol +++ b/contracts/staking/IStakingData.sol @@ -2,6 +2,10 @@ pragma solidity >=0.6.12 <0.8.0; +/** + * @title Staking Data interface + * @dev This interface defines some structures used by the Staking contract. + */ interface IStakingData { /** * @dev Allocate GRT tokens for the purpose of serving queries of a subgraph deployment @@ -19,7 +23,7 @@ interface IStakingData { } /** - * @dev Represents a request to close an allocation with a specific proof of indexing. + * @dev CloseAllocationRequest represents a request to close an allocation with a specific proof of indexing. * This is passed when calling closeAllocationMany to define the closing parameters for * each allocation. */ diff --git a/contracts/staking/IStakingExtension.sol b/contracts/staking/IStakingExtension.sol new file mode 100644 index 000000000..e63f5c035 --- /dev/null +++ b/contracts/staking/IStakingExtension.sol @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.6.12 <0.8.0; +pragma abicoder v2; + +import { IStakingData } from "./IStakingData.sol"; +import { Rebates } from "./libs/Rebates.sol"; +import { Stakes } from "./libs/Stakes.sol"; + +/** + * @title Interface for the StakingExtension contract + * @dev This interface defines the events and functions implemented + * in the StakingExtension contract, which is used to extend the functionality + * of the Staking contract while keeping it within the 24kB mainnet size limit. + * In particular, this interface includes delegation functions and various storage + * getters. + */ +interface IStakingExtension is IStakingData { + /** + * @dev DelegationPool struct as returned by delegationPools(), since + * the original DelegationPool in IStakingData.sol contains a nested mapping. + */ + struct DelegationPoolReturn { + uint32 cooldownBlocks; // Blocks to wait before updating parameters + uint32 indexingRewardCut; // in PPM + uint32 queryFeeCut; // in PPM + uint256 updatedAtBlock; // Block when the pool was last updated + uint256 tokens; // Total tokens as pool reserves + uint256 shares; // Total shares minted in the pool + } + + /** + * @dev Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator + * gets `shares` for the delegation pool proportionally to the tokens staked. + */ + event StakeDelegated( + address indexed indexer, + address indexed delegator, + uint256 tokens, + uint256 shares + ); + + /** + * @dev Emitted when `delegator` undelegated `tokens` from `indexer`. + * Tokens get locked for withdrawal after a period of time. + */ + event StakeDelegatedLocked( + address indexed indexer, + address indexed delegator, + uint256 tokens, + uint256 shares, + uint256 until + ); + + /** + * @dev Emitted when `delegator` withdrew delegated `tokens` from `indexer`. + */ + event StakeDelegatedWithdrawn( + address indexed indexer, + address indexed delegator, + uint256 tokens + ); + + /** + * @dev Emitted when `indexer` was slashed for a total of `tokens` amount. + * Tracks `reward` amount of tokens given to `beneficiary`. + */ + event StakeSlashed( + address indexed indexer, + uint256 tokens, + uint256 reward, + address beneficiary + ); + + /** + * @dev Emitted when `caller` set `slasher` address as `allowed` to slash stakes. + */ + event SlasherUpdate(address indexed caller, address indexed slasher, bool allowed); + + /** + * @notice Set the delegation ratio. + * If set to 10 it means the indexer can use up to 10x the indexer staked amount + * from their delegated tokens + * @dev This function is only callable by the governor + * @param _delegationRatio Delegation capacity multiplier + */ + function setDelegationRatio(uint32 _delegationRatio) external; + + /** + * @notice Set the minimum time in blocks an indexer needs to wait to change delegation parameters. + * Indexers can set a custom amount time for their own cooldown, but it must be greater than this. + * @dev This function is only callable by the governor + * @param _blocks Number of blocks to set the delegation parameters cooldown period + */ + function setDelegationParametersCooldown(uint32 _blocks) external; + + /** + * @notice Set the time, in epochs, a Delegator needs to wait to withdraw tokens after undelegating. + * @dev This function is only callable by the governor + * @param _delegationUnbondingPeriod Period in epochs to wait for token withdrawals after undelegating + */ + function setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) external; + + /** + * @notice Set a delegation tax percentage to burn when delegated funds are deposited. + * @dev This function is only callable by the governor + * @param _percentage Percentage of delegated tokens to burn as delegation tax, expressed in parts per million + */ + function setDelegationTaxPercentage(uint32 _percentage) external; + + /** + * @notice Set or unset an address as allowed slasher. + * @dev This function can only be called by the governor. + * @param _slasher Address of the party allowed to slash indexers + * @param _allowed True if slasher is allowed + */ + function setSlasher(address _slasher, bool _allowed) external; + + /** + * @notice Delegate tokens to an indexer. + * @param _indexer Address of the indexer to which tokens are delegated + * @param _tokens Amount of tokens to delegate + * @return Amount of shares issued from the delegation pool + */ + function delegate(address _indexer, uint256 _tokens) external returns (uint256); + + /** + * @notice Undelegate tokens from an indexer. Tokens will be locked for the unbonding period. + * @param _indexer Address of the indexer to which tokens had been delegated + * @param _shares Amount of shares to return and undelegate tokens + * @return Amount of tokens returned for the shares of the delegation pool + */ + function undelegate(address _indexer, uint256 _shares) external returns (uint256); + + /** + * @notice Withdraw undelegated tokens once the unbonding period has passed, and optionally + * re-delegate to a new indexer. + * @param _indexer Withdraw available tokens delegated to indexer + * @param _newIndexer Re-delegate to indexer address if non-zero, withdraw if zero address + */ + function withdrawDelegated(address _indexer, address _newIndexer) external returns (uint256); + + /** + * @notice Slash the indexer stake. Delegated tokens are not subject to slashing. + * @dev Can only be called by the slasher role. + * @param _indexer Address of indexer to slash + * @param _tokens Amount of tokens to slash from the indexer stake + * @param _reward Amount of reward tokens to send to a beneficiary + * @param _beneficiary Address of a beneficiary to receive a reward for the slashing + */ + function slash( + address _indexer, + uint256 _tokens, + uint256 _reward, + address _beneficiary + ) external; + + /** + * @notice Return the delegation from a delegator to an indexer. + * @param _indexer Address of the indexer where funds have been delegated + * @param _delegator Address of the delegator + * @return Delegation data + */ + function getDelegation(address _indexer, address _delegator) + external + view + returns (Delegation memory); + + /** + * @notice Return whether the delegator has delegated to the indexer. + * @param _indexer Address of the indexer where funds have been delegated + * @param _delegator Address of the delegator + * @return True if delegator has tokens delegated to the indexer + */ + function isDelegator(address _indexer, address _delegator) external view returns (bool); + + /** + * @notice Returns amount of delegated tokens ready to be withdrawn after unbonding period. + * @param _delegation Delegation of tokens from delegator to indexer + * @return Amount of tokens to withdraw + */ + function getWithdraweableDelegatedTokens(Delegation memory _delegation) + external + view + returns (uint256); + + /** + * @notice Getter for the delegationRatio, i.e. the delegation capacity multiplier: + * If delegation ratio is 100, and an Indexer has staked 5 GRT, + * then they can use up to 500 GRT from the delegated stake + * @return Delegation ratio + */ + function delegationRatio() external view returns (uint32); + + /** + * @notice Getter for delegationParametersCooldown: + * Minimum time in blocks an indexer needs to wait to change delegation parameters + * @return Delegation parameters cooldown in blocks + */ + function delegationParametersCooldown() external view returns (uint32); + + /** + * @notice Getter for delegationUnbondingPeriod: + * Time in epochs a delegator needs to wait to withdraw delegated stake + * @return Delegation unbonding period in epochs + */ + function delegationUnbondingPeriod() external view returns (uint32); + + /** + * @notice Getter for delegationTaxPercentage: + * Percentage of tokens to tax a delegation deposit, expressed in parts per million + * @return Delegation tax percentage in parts per million + */ + function delegationTaxPercentage() external view returns (uint32); + + /** + * @notice Getter for delegationPools[_indexer]: + * gets the delegation pool structure for a particular indexer. + * @param _indexer Address of the indexer for which to query the delegation pool + * @return Delegation pool as a DelegationPoolReturn struct + */ + function delegationPools(address _indexer) external view returns (DelegationPoolReturn memory); + + /** + * @notice Getter for operatorAuth[_indexer][_maybeOperator]: + * returns true if the operator is authorized to operate on behalf of the indexer. + * @param _indexer The indexer address for which to query authorization + * @param _maybeOperator The address that may or may not be an operator + * @return True if the operator is authorized to operate on behalf of the indexer + */ + function operatorAuth(address _indexer, address _maybeOperator) external view returns (bool); + + /** + * @notice Getter for rewardsDestination[_indexer]: + * returns the address where the indexer's rewards are sent. + * @param _indexer The indexer address for which to query the rewards destination + * @return The address where the indexer's rewards are sent, zero if none is set in which case rewards are re-staked + */ + function rewardsDestination(address _indexer) external view returns (address); + + /** + * @notice Getter for assetHolders[_maybeAssetHolder]: + * returns true if the address is an asset holder, i.e. an entity that can collect + * query fees into the Staking contract. + * @param _maybeAssetHolder The address that may or may not be an asset holder + * @return True if the address is an asset holder + */ + function assetHolders(address _maybeAssetHolder) external view returns (bool); + + /** + * @notice Getter for subgraphAllocations[_subgraphDeploymentId]: + * returns the amount of tokens allocated to a subgraph deployment. + * @param _subgraphDeploymentId The subgraph deployment for which to query the allocations + * @return The amount of tokens allocated to the subgraph deployment + */ + function subgraphAllocations(bytes32 _subgraphDeploymentId) external view returns (uint256); + + /** + * @notice Getter for rebates[_epoch]: + * gets the rebate pool for a particular epoch. + * @param _epoch Epoch for which to query the rebate pool + * @return Rebate pool for the specified epoch, as a Rebates.Pool struct + */ + function rebates(uint256 _epoch) external view returns (Rebates.Pool memory); + + /** + * @notice Getter for slashers[_maybeSlasher]: + * returns true if the address is a slasher, i.e. an entity that can slash indexers + * @param _maybeSlasher Address for which to check the slasher role + * @return True if the address is a slasher + */ + function slashers(address _maybeSlasher) external view returns (bool); + + /** + * @notice Getter for minimumIndexerStake: the minimum + * amount of GRT that an indexer needs to stake. + * @return Minimum indexer stake in GRT + */ + function minimumIndexerStake() external view returns (uint256); + + /** + * @notice Getter for thawingPeriod: the time in blocks an + * indexer needs to wait to unstake tokens. + * @return Thawing period in blocks + */ + function thawingPeriod() external view returns (uint32); + + /** + * @notice Getter for curationPercentage: the percentage of + * query fees that are distributed to curators. + * @return Curation percentage in parts per million + */ + function curationPercentage() external view returns (uint32); + + /** + * @notice Getter for protocolPercentage: the percentage of + * query fees that are burned as protocol fees. + * @return Protocol percentage in parts per million + */ + function protocolPercentage() external view returns (uint32); + + /** + * @notice Getter for channelDisputeEpochs: the time in epochs + * between closing an allocation and the moment it becomes finalized so + * query fees can be claimed. + * @return Channel dispute period in epochs + */ + function channelDisputeEpochs() external view returns (uint32); + + /** + * @notice Getter for maxAllocationEpochs: the maximum time in epochs + * that an allocation can be open before anyone is allowed to close it. This + * also caps the effective allocation when sending the allocation's query fees + * to the rebate pool. + * @return Maximum allocation period in epochs + */ + function maxAllocationEpochs() external view returns (uint32); + + /** + * @notice Getter for alphaNumerator: the numerator of the Cobb-Douglas + * rebate ratio. + * @return Rebate ratio numerator + */ + function alphaNumerator() external view returns (uint32); + + /** + * @notice Getter for alphaDenominator: the denominator of the Cobb-Douglas + * rebate ratio. + * @return Rebate ratio denominator + */ + function alphaDenominator() external view returns (uint32); + + /** + * @notice Getter for stakes[_indexer]: + * gets the stake information for an indexer as a Stakes.Indexer struct. + * @param _indexer Indexer address for which to query the stake information + * @return Stake information for the specified indexer, as a Stakes.Indexer struct + */ + function stakes(address _indexer) external view returns (Stakes.Indexer memory); + + /** + * @notice Getter for allocations[_allocationID]: + * gets an allocation's information as an IStakingData.Allocation struct. + * @param _allocationID Allocation ID for which to query the allocation information + * @return The specified allocation, as an IStakingData.Allocation struct + */ + function allocations(address _allocationID) + external + view + returns (IStakingData.Allocation memory); +} diff --git a/contracts/staking/L1Staking.sol b/contracts/staking/L1Staking.sol new file mode 100644 index 000000000..e1ec82e95 --- /dev/null +++ b/contracts/staking/L1Staking.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; +import { Staking } from "./Staking.sol"; +import { Stakes } from "./libs/Stakes.sol"; +import { IStakingData } from "./IStakingData.sol"; +import { IL2Staking } from "../l2/staking/IL2Staking.sol"; +import { L1StakingV1Storage } from "./L1StakingStorage.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { IL1StakingBase } from "./IL1StakingBase.sol"; +import { MathUtils } from "./libs/MathUtils.sol"; +import { IL1GraphTokenLockMigrator } from "./IL1GraphTokenLockMigrator.sol"; + +/** + * @title L1Staking contract + * @dev This contract is the L1 variant of the Staking contract. It adds functions + * to send an indexer's stake to L2, and to send delegation to L2 as well. + */ +contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { + using Stakes for Stakes.Indexer; + using SafeMath for uint256; + + /** + * @notice Receive ETH into the Staking contract + * @dev Only the L1GraphTokenLockMigrator can send ETH, as part of the + * migration of stake/delegation for vesting lock wallets. + */ + receive() external payable { + require(msg.sender == address(l1GraphTokenLockMigrator), "Only migrator can send ETH"); + } + + /** + * @notice Set the L1GraphTokenLockMigrator contract address + * @dev This function can only be called by the governor. + * @param _l1GraphTokenLockMigrator Address of the L1GraphTokenLockMigrator contract + */ + function setL1GraphTokenLockMigrator(IL1GraphTokenLockMigrator _l1GraphTokenLockMigrator) + external + override + onlyGovernor + { + l1GraphTokenLockMigrator = _l1GraphTokenLockMigrator; + emit L1GraphTokenLockMigratorSet(address(_l1GraphTokenLockMigrator)); + } + + /** + * @notice Send an indexer's stake to L2. + * @dev This function can only be called by the indexer (not an operator). + * It will validate that the remaining stake is sufficient to cover all the allocated + * stake, so the indexer might have to close some allocations before migrating. + * It will also check that the indexer's stake is not locked for withdrawal. + * Since the indexer address might be an L1-only contract, the function takes a beneficiary + * address that will be the indexer's address in L2. + * The caller must provide an amount of ETH to use for the L2 retryable ticket, that + * must be at least `_maxSubmissionCost + _gasPriceBid * _maxGas`. + * @param _l2Beneficiary Address of the indexer in L2. If the indexer has previously migrated stake, this must match the previously-used value. + * @param _amount Amount of stake GRT to migrate 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 + */ + function migrateStakeToL2( + address _l2Beneficiary, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external payable override { + _migrateStakeToL2( + msg.sender, + _l2Beneficiary, + _amount, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + msg.value + ); + } + + /** + * @notice Send an indexer's stake to L2, from a GraphTokenLockWallet vesting contract. + * @dev This function can only be called by the indexer (not an operator). + * It will validate that the remaining stake is sufficient to cover all the allocated + * stake, so the indexer might have to close some allocations before migrating. + * It will also check that the indexer's stake is not locked for withdrawal. + * The L2 beneficiary for the stake will be determined by calling the L1GraphTokenLockMigrator contract, + * so the caller must have previously migrated tokens through that first + * (see GIP-0046 for details: https://forum.thegraph.com/t/gip-0046-l2-migration-helpers/4023). + * The ETH for the L2 gas will be pulled from the L1GraphTokenLockMigrator, so the owner of + * the GraphTokenLockWallet must have previously deposited at least `_maxSubmissionCost + _gasPriceBid * _maxGas` + * ETH into the L1GraphTokenLockMigrator contract (using its depositETH function). + * @param _amount Amount of stake GRT to migrate 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 + */ + function migrateLockedStakeToL2( + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external override { + address l2Beneficiary = l1GraphTokenLockMigrator.migratedWalletAddress(msg.sender); + require(l2Beneficiary != address(0), "LOCK NOT MIGRATED"); + uint256 balance = address(this).balance; + uint256 ethAmount = _maxSubmissionCost.add(_maxGas.mul(_gasPriceBid)); + l1GraphTokenLockMigrator.pullETH(msg.sender, ethAmount); + require(address(this).balance == balance.add(ethAmount), "ETH TRANSFER FAILED"); + _migrateStakeToL2( + msg.sender, + l2Beneficiary, + _amount, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + ethAmount + ); + } + + /** + * @notice Send a delegator's delegated tokens to L2 + * @dev This function can only be called by the delegator. + * This function will validate that the indexer has migrated their stake using migrateStakeToL2, + * and that the delegation is not locked for undelegation. + * Since the delegator's address might be an L1-only contract, the function takes a beneficiary + * address that will be the delegator's address in L2. + * The caller must provide an amount of ETH to use for the L2 retryable ticket, that + * must be at least `_maxSubmissionCost + _gasPriceBid * _maxGas`. + * @param _indexer Address of the indexer (in L1, before migrating) + * @param _l2Beneficiary Address of the delegator 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 migrateDelegationToL2( + address _indexer, + address _l2Beneficiary, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external payable override { + _migrateDelegationToL2( + msg.sender, + _indexer, + _l2Beneficiary, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + msg.value + ); + } + + /** + * @notice Send a delegator's delegated tokens to L2, for a GraphTokenLockWallet vesting contract + * @dev This function can only be called by the delegator. + * This function will validate that the indexer has migrated their stake using migrateStakeToL2, + * and that the delegation is not locked for undelegation. + * The L2 beneficiary for the delegation will be determined by calling the L1GraphTokenLockMigrator contract, + * so the caller must have previously migrated tokens through that first + * (see GIP-0046 for details: https://forum.thegraph.com/t/gip-0046-l2-migration-helpers/4023). + * The ETH for the L2 gas will be pulled from the L1GraphTokenLockMigrator, so the owner of + * the GraphTokenLockWallet must have previously deposited at least `_maxSubmissionCost + _gasPriceBid * _maxGas` + * ETH into the L1GraphTokenLockMigrator contract (using its depositETH function). + * @param _indexer Address of the indexer (in L1, before migrating) + * @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 migrateLockedDelegationToL2( + address _indexer, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost + ) external override { + address l2Beneficiary = l1GraphTokenLockMigrator.migratedWalletAddress(msg.sender); + require(l2Beneficiary != address(0), "LOCK NOT MIGRATED"); + uint256 balance = address(this).balance; + uint256 ethAmount = _maxSubmissionCost.add(_maxGas.mul(_gasPriceBid)); + l1GraphTokenLockMigrator.pullETH(msg.sender, ethAmount); + require(address(this).balance == balance.add(ethAmount), "ETH TRANSFER FAILED"); + _migrateDelegationToL2( + msg.sender, + _indexer, + l2Beneficiary, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + ethAmount + ); + } + + /** + * @notice Unlock a delegator's delegated tokens, if the indexer has migrated + * @dev This function can only be called by the delegator. + * This function will validate that the indexer has migrated their stake using migrateStakeToL2, + * and that the indexer has no remaining stake in L1. + * The tokens must previously be locked for undelegation by calling `undelegate()`, + * and can be withdrawn with `withdrawDelegated()` immediately after calling this. + * @param _indexer Address of the indexer (in L1, before migrating) + */ + function unlockDelegationToMigratedIndexer(address _indexer) external override { + require( + indexerMigratedToL2[_indexer] != address(0) && __stakes[_indexer].tokensStaked == 0, + "indexer not migrated" + ); + + Delegation storage delegation = __delegationPools[_indexer].delegators[msg.sender]; + require(delegation.tokensLockedUntil != 0, "! locked"); + + // Unlock the delegation + delegation.tokensLockedUntil = epochManager().currentEpoch(); + + // After this, the delegator should be able to withdraw in the current block + emit StakeDelegatedUnlockedDueToMigration(_indexer, msg.sender); + } + + /** + * @dev Implements sending an indexer's stake to L2. + * This function can only be called by the indexer (not an operator). + * It will validate that the remaining stake is sufficient to cover all the allocated + * stake, so the indexer might have to close some allocations before migrating. + * It will also check that the indexer's stake is not locked for withdrawal. + * Since the indexer address might be an L1-only contract, the function takes a beneficiary + * address that will be the indexer's address in L2. + * @param _l2Beneficiary Address of the indexer in L2. If the indexer has previously migrated stake, this must match the previously-used value. + * @param _amount Amount of stake GRT to migrate 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 _ethAmount Amount of ETH to send with the retryable ticket + */ + function _migrateStakeToL2( + address _indexer, + address _l2Beneficiary, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _ethAmount + ) internal { + Stakes.Indexer storage indexerStake = __stakes[_indexer]; + require(indexerStake.tokensStaked != 0, "tokensStaked == 0"); + // Indexers shouldn't be trying to withdraw tokens before migrating to L2. + // Allowing this would complicate our accounting so we require that they have no + // tokens locked for withdrawal. + require(indexerStake.tokensLocked == 0, "tokensLocked != 0"); + + require(_l2Beneficiary != address(0), "l2Beneficiary == 0"); + if (indexerMigratedToL2[_indexer] != address(0)) { + require(indexerMigratedToL2[_indexer] == _l2Beneficiary, "l2Beneficiary != previous"); + } else { + indexerMigratedToL2[_indexer] = _l2Beneficiary; + require(_amount >= __minimumIndexerStake, "!minimumIndexerStake sent"); + } + // Ensure minimum stake + indexerStake.tokensStaked = indexerStake.tokensStaked.sub(_amount); + require( + indexerStake.tokensStaked == 0 || indexerStake.tokensStaked >= __minimumIndexerStake, + "!minimumIndexerStake remaining" + ); + + IStakingData.DelegationPool storage delegationPool = __delegationPools[_indexer]; + + if (indexerStake.tokensStaked == 0) { + // require that no allocations are open + require(indexerStake.tokensAllocated == 0, "allocated"); + } else { + // require that the indexer has enough stake to cover all allocations + uint256 tokensDelegatedCap = indexerStake.tokensStaked.mul(uint256(__delegationRatio)); + uint256 tokensDelegatedCapacity = MathUtils.min( + delegationPool.tokens, + tokensDelegatedCap + ); + require( + indexerStake.tokensUsed() <= indexerStake.tokensStaked.add(tokensDelegatedCapacity), + "! allocation capacity" + ); + } + + IL2Staking.ReceiveIndexerStakeData memory functionData; + functionData.indexer = _l2Beneficiary; + + bytes memory extraData = abi.encode( + uint8(IL2Staking.L1MessageCodes.RECEIVE_INDEXER_STAKE_CODE), + abi.encode(functionData) + ); + + _sendTokensAndMessageToL2Staking( + _amount, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _ethAmount, + extraData + ); + + emit IndexerMigratedToL2(_indexer, _l2Beneficiary, _amount); + } + + /** + * @dev Implements sending a delegator's delegated tokens to L2. + * This function can only be called by the delegator. + * This function will validate that the indexer has migrated their stake using migrateStakeToL2, + * and that the delegation is not locked for undelegation. + * Since the delegator's address might be an L1-only contract, the function takes a beneficiary + * address that will be the delegator's address in L2. + * @param _indexer Address of the indexer (in L1, before migrating) + * @param _l2Beneficiary Address of the delegator 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 + * @param _ethAmount Amount of ETH to send with the retryable ticket + */ + function _migrateDelegationToL2( + address _delegator, + address _indexer, + address _l2Beneficiary, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _ethAmount + ) internal { + require(_l2Beneficiary != address(0), "l2Beneficiary == 0"); + require(indexerMigratedToL2[_indexer] != address(0), "indexer not migrated"); + + // Get the delegation pool of the indexer + DelegationPool storage pool = __delegationPools[_indexer]; + Delegation storage delegation = pool.delegators[_delegator]; + + // Check that the delegation is not locked for undelegation + require(delegation.tokensLockedUntil == 0, "tokensLocked != 0"); + require(delegation.shares != 0, "delegation == 0"); + // Calculate tokens to get in exchange for the shares + uint256 tokensToSend = delegation.shares.mul(pool.tokens).div(pool.shares); + + // Update the delegation pool + pool.tokens = pool.tokens.sub(tokensToSend); + pool.shares = pool.shares.sub(delegation.shares); + + // Update the delegation + delegation.shares = 0; + bytes memory extraData; + { + IL2Staking.ReceiveDelegationData memory functionData; + functionData.indexer = indexerMigratedToL2[_indexer]; + functionData.delegator = _l2Beneficiary; + extraData = abi.encode( + uint8(IL2Staking.L1MessageCodes.RECEIVE_DELEGATION_CODE), + abi.encode(functionData) + ); + } + + _sendTokensAndMessageToL2Staking( + tokensToSend, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _ethAmount, + extraData + ); + emit DelegationMigratedToL2( + _delegator, + _l2Beneficiary, + _indexer, + indexerMigratedToL2[_indexer], + tokensToSend + ); + } + + /** + * @dev Sends a message to the L2Staking 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 _value Amount of ETH to send with the message + * @param _extraData Extra data for the callhook on L2Staking + */ + function _sendTokensAndMessageToL2Staking( + uint256 _tokens, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _value, + bytes memory _extraData + ) internal { + IGraphToken grt = graphToken(); + ITokenGateway gateway = graphTokenGateway(); + grt.approve(address(gateway), _tokens); + gateway.outboundTransfer{ value: _value }( + address(grt), + counterpartStakingAddress, + _tokens, + _maxGas, + _gasPriceBid, + abi.encode(_maxSubmissionCost, _extraData) + ); + } +} diff --git a/contracts/staking/L1StakingStorage.sol b/contracts/staking/L1StakingStorage.sol new file mode 100644 index 000000000..2fd180b13 --- /dev/null +++ b/contracts/staking/L1StakingStorage.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { IL1GraphTokenLockMigrator } from "./IL1GraphTokenLockMigrator.sol"; + +/** + * @title L1StakingV1Storage + * @notice This contract holds all the L1-specific storage variables for the L1Staking 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 L1StakingV1Storage { + /// If an indexer has migrated to L2, this mapping will hold the indexer's address in L2 + mapping(address => address) public indexerMigratedToL2; + /// @dev For locked indexers/delegations, this contract holds the mapping of L1 to L2 addresses + IL1GraphTokenLockMigrator internal l1GraphTokenLockMigrator; + /// @dev Storage gap to keep storage slots fixed in future versions + uint256[50] private __gap; +} diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index 2bcc8d74d..313ebd729 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -3,207 +3,93 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/cryptography/ECDSA.sol"; - -import "../base/Multicall.sol"; -import "../upgrades/GraphUpgradeable.sol"; -import "../utils/TokenUtils.sol"; - -import "./IStaking.sol"; -import "./StakingStorage.sol"; -import "./libs/MathUtils.sol"; -import "./libs/Rebates.sol"; -import "./libs/Stakes.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { ECDSA } from "@openzeppelin/contracts/cryptography/ECDSA.sol"; + +import { Multicall } from "../base/Multicall.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; +import { TokenUtils } from "../utils/TokenUtils.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { IStakingBase } from "./IStakingBase.sol"; +import { StakingV3Storage } from "./StakingStorage.sol"; +import { MathUtils } from "./libs/MathUtils.sol"; +import { Rebates } from "./libs/Rebates.sol"; +import { Stakes } from "./libs/Stakes.sol"; +import { Managed } from "../governance/Managed.sol"; +import { ICuration } from "../curation/ICuration.sol"; +import { IRewardsManager } from "../rewards/IRewardsManager.sol"; /** - * @title Staking contract + * @title Base Staking contract * @dev The Staking contract allows Indexers to Stake on Subgraphs. Indexers Stake by creating * Allocations on a Subgraph. It also allows Delegators to Delegate towards an Indexer. The * contract also has the slashing functionality. + * The contract is abstract as the implementation that is deployed depends on each layer: L1Staking on mainnet + * and L2Staking on Arbitrum. + * Note that this contract delegates part of its functionality to a StakingExtension contract. + * This is due to the 24kB contract size limit on Ethereum. */ -contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { +abstract contract Staking is StakingV3Storage, GraphUpgradeable, IStakingBase, Multicall { using SafeMath for uint256; using Stakes for Stakes.Indexer; using Rebates for Rebates.Pool; - // 100% in parts per million + /// @dev 100% in parts per million uint32 private constant MAX_PPM = 1000000; - // -- Events -- - - /** - * @dev Emitted when `indexer` update the delegation parameters for its delegation pool. - */ - event DelegationParametersUpdated( - address indexed indexer, - uint32 indexingRewardCut, - uint32 queryFeeCut, - uint32 cooldownBlocks - ); - - /** - * @dev Emitted when `indexer` stake `tokens` amount. - */ - event StakeDeposited(address indexed indexer, uint256 tokens); - - /** - * @dev Emitted when `indexer` unstaked and locked `tokens` amount `until` block. - */ - event StakeLocked(address indexed indexer, uint256 tokens, uint256 until); - - /** - * @dev Emitted when `indexer` withdrew `tokens` staked. - */ - event StakeWithdrawn(address indexed indexer, uint256 tokens); - - /** - * @dev Emitted when `indexer` was slashed for a total of `tokens` amount. - * Tracks `reward` amount of tokens given to `beneficiary`. - */ - event StakeSlashed( - address indexed indexer, - uint256 tokens, - uint256 reward, - address beneficiary - ); - - /** - * @dev Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator - * gets `shares` for the delegation pool proportionally to the tokens staked. - */ - event StakeDelegated( - address indexed indexer, - address indexed delegator, - uint256 tokens, - uint256 shares - ); - - /** - * @dev Emitted when `delegator` undelegated `tokens` from `indexer`. - * Tokens get locked for withdrawal after a period of time. - */ - event StakeDelegatedLocked( - address indexed indexer, - address indexed delegator, - uint256 tokens, - uint256 shares, - uint256 until - ); - - /** - * @dev Emitted when `delegator` withdrew delegated `tokens` from `indexer`. - */ - event StakeDelegatedWithdrawn( - address indexed indexer, - address indexed delegator, - uint256 tokens - ); - - /** - * @dev Emitted when `indexer` allocated `tokens` amount to `subgraphDeploymentID` - * during `epoch`. - * `allocationID` indexer derived address used to identify the allocation. - * `metadata` additional information related to the allocation. - */ - event AllocationCreated( - address indexed indexer, - bytes32 indexed subgraphDeploymentID, - uint256 epoch, - uint256 tokens, - address indexed allocationID, - bytes32 metadata - ); - - /** - * @dev Emitted when `indexer` collected `tokens` amount in `epoch` for `allocationID`. - * These funds are related to `subgraphDeploymentID`. - * The `from` value is the sender of the collected funds. - */ - event AllocationCollected( - address indexed indexer, - bytes32 indexed subgraphDeploymentID, - uint256 epoch, - uint256 tokens, - address indexed allocationID, - address from, - uint256 curationFees, - uint256 rebateFees - ); - - /** - * @dev Emitted when `indexer` close an allocation in `epoch` for `allocationID`. - * An amount of `tokens` get unallocated from `subgraphDeploymentID`. - * The `effectiveAllocation` are the tokens allocated from creation to closing. - * This event also emits the POI (proof of indexing) submitted by the indexer. - * `isPublic` is true if the sender was someone other than the indexer. - */ - event AllocationClosed( - address indexed indexer, - bytes32 indexed subgraphDeploymentID, - uint256 epoch, - uint256 tokens, - address indexed allocationID, - uint256 effectiveAllocation, - address sender, - bytes32 poi, - bool isPublic - ); - - /** - * @dev Emitted when `indexer` claimed a rebate on `subgraphDeploymentID` during `epoch` - * related to the `forEpoch` rebate pool. - * The rebate is for `tokens` amount and `unclaimedAllocationsCount` are left for claim - * in the rebate pool. `delegationFees` collected and sent to delegation pool. - */ - event RebateClaimed( - address indexed indexer, - bytes32 indexed subgraphDeploymentID, - address indexed allocationID, - uint256 epoch, - uint256 forEpoch, - uint256 tokens, - uint256 unclaimedAllocationsCount, - uint256 delegationFees - ); + // -- Events are declared in IStakingBase -- // /** - * @dev Emitted when `caller` set `slasher` address as `allowed` to slash stakes. + * @notice Delegates the current call to the StakingExtension implementation. + * @dev This function does not return to its internal call site, it will return directly to the + * external caller. */ - event SlasherUpdate(address indexed caller, address indexed slasher, bool allowed); + // solhint-disable-next-line payable-fallback, no-complex-fallback + fallback() external { + require(_implementation() != address(0), "only through proxy"); + // solhint-disable-next-line no-inline-assembly + assembly { + // (a) get free memory pointer + let ptr := mload(0x40) - /** - * @dev Emitted when `caller` set `assetHolder` address as `allowed` to send funds - * to staking contract. - */ - event AssetHolderUpdate(address indexed caller, address indexed assetHolder, bool allowed); + // (b) get address of the implementation + let impl := and(sload(extensionImpl.slot), 0xffffffffffffffffffffffffffffffffffffffff) - /** - * @dev Emitted when `indexer` set `operator` access. - */ - event SetOperator(address indexed indexer, address indexed operator, bool allowed); + // (1) copy incoming call data + calldatacopy(ptr, 0, calldatasize()) - /** - * @dev Emitted when `indexer` set an address to receive rewards. - */ - event SetRewardsDestination(address indexed indexer, address indexed destination); + // (2) forward call to logic contract + let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0) + let size := returndatasize() - /** - * @dev Check if the caller is the slasher. - */ - modifier onlySlasher() { - require(slashers[msg.sender] == true, "!slasher"); - _; - } + // (3) retrieve return data + returndatacopy(ptr, 0, size) - /** - * @dev Check if the caller is authorized (indexer or operator) - */ - function _isAuth(address _indexer) private view returns (bool) { - return msg.sender == _indexer || isOperator(msg.sender, _indexer) == true; + // (4) forward return data back to caller + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } } /** - * @dev Initialize this contract. + * @notice Initialize this contract. + * @param _controller Address of the controller that manages this contract + * @param _minimumIndexerStake Minimum amount of tokens that an indexer must stake + * @param _thawingPeriod Number of epochs that tokens get locked after unstaking + * @param _protocolPercentage Percentage of query fees that are burned as protocol fee (in PPM) + * @param _curationPercentage Percentage of query fees that are given to curators (in PPM) + * @param _channelDisputeEpochs The period in epochs that needs to pass before fees in rebate pool can be claimed + * @param _maxAllocationEpochs The maximum number of epochs that an allocation can be active + * @param _delegationUnbondingPeriod The period in epochs that tokens get locked after undelegating + * @param _delegationRatio The ratio between an indexer's own stake and the delegation they can use + * @param _rebateAlphaNumerator The numerator of the alpha factor used to calculate the rebate + * @param _rebateAlphaDenominator The denominator of the alpha factor used to calculate the rebate + * @param _extensionImpl Address of the StakingExtension implementation */ function initialize( address _controller, @@ -216,8 +102,9 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { uint32 _delegationUnbondingPeriod, uint32 _delegationRatio, uint32 _rebateAlphaNumerator, - uint32 _rebateAlphaDenominator - ) external onlyImpl { + uint32 _rebateAlphaDenominator, + address _extensionImpl + ) external override onlyImpl { Managed._initialize(_controller); // Settings @@ -230,52 +117,62 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { _setChannelDisputeEpochs(_channelDisputeEpochs); _setMaxAllocationEpochs(_maxAllocationEpochs); - _setDelegationUnbondingPeriod(_delegationUnbondingPeriod); - _setDelegationRatio(_delegationRatio); - _setDelegationParametersCooldown(0); - _setDelegationTaxPercentage(0); - _setRebateRatio(_rebateAlphaNumerator, _rebateAlphaDenominator); + + extensionImpl = _extensionImpl; + + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = extensionImpl.delegatecall( + abi.encodeWithSignature( + "initialize(uint32,uint32,uint32,uint32)", + _delegationUnbondingPeriod, + 0, + _delegationRatio, + 0 + ) + ); + require(success, "Extension init failed"); + emit ExtensionImplementationSet(_extensionImpl); } /** - * @dev Set the minimum indexer stake required to. - * @param _minimumIndexerStake Minimum indexer stake + * @notice Set the address of the StakingExtension implementation. + * @dev This function can only be called by the governor. + * @param _extensionImpl Address of the StakingExtension implementation */ - function setMinimumIndexerStake(uint256 _minimumIndexerStake) external override onlyGovernor { - _setMinimumIndexerStake(_minimumIndexerStake); + function setExtensionImpl(address _extensionImpl) external override onlyGovernor { + extensionImpl = _extensionImpl; + emit ExtensionImplementationSet(_extensionImpl); } /** - * @dev Internal: Set the minimum indexer stake required. - * @param _minimumIndexerStake Minimum indexer stake + * @notice Set the address of the counterpart (L1 or L2) staking contract. + * @dev This function can only be called by the governor. + * @param _counterpart Address of the counterpart staking contract in the other chain, without any aliasing. */ - function _setMinimumIndexerStake(uint256 _minimumIndexerStake) private { - require(_minimumIndexerStake > 0, "!minimumIndexerStake"); - minimumIndexerStake = _minimumIndexerStake; - emit ParameterUpdated("minimumIndexerStake"); + function setCounterpartStakingAddress(address _counterpart) external override onlyGovernor { + counterpartStakingAddress = _counterpart; + emit ParameterUpdated("counterpartStakingAddress"); } /** - * @dev Set the thawing period for unstaking. - * @param _thawingPeriod Period in blocks to wait for token withdrawals after unstaking + * @notice Set the minimum stake required to be an indexer. + * @param _minimumIndexerStake Minimum indexer stake */ - function setThawingPeriod(uint32 _thawingPeriod) external override onlyGovernor { - _setThawingPeriod(_thawingPeriod); + function setMinimumIndexerStake(uint256 _minimumIndexerStake) external override onlyGovernor { + _setMinimumIndexerStake(_minimumIndexerStake); } /** - * @dev Internal: Set the thawing period for unstaking. + * @notice Set the thawing period for unstaking. * @param _thawingPeriod Period in blocks to wait for token withdrawals after unstaking */ - function _setThawingPeriod(uint32 _thawingPeriod) private { - require(_thawingPeriod > 0, "!thawingPeriod"); - thawingPeriod = _thawingPeriod; - emit ParameterUpdated("thawingPeriod"); + function setThawingPeriod(uint32 _thawingPeriod) external override onlyGovernor { + _setThawingPeriod(_thawingPeriod); } /** - * @dev Set the curation percentage of query fees sent to curators. + * @notice Set the curation percentage of query fees sent to curators. * @param _percentage Percentage of query fees sent to curators */ function setCurationPercentage(uint32 _percentage) external override onlyGovernor { @@ -283,18 +180,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } /** - * @dev Internal: Set the curation percentage of query fees sent to curators. - * @param _percentage Percentage of query fees sent to curators - */ - function _setCurationPercentage(uint32 _percentage) private { - // Must be within 0% to 100% (inclusive) - require(_percentage <= MAX_PPM, ">percentage"); - curationPercentage = _percentage; - emit ParameterUpdated("curationPercentage"); - } - - /** - * @dev Set a protocol percentage to burn when collecting query fees. + * @notice Set a protocol percentage to burn when collecting query fees. * @param _percentage Percentage of query fees to burn as protocol fee */ function setProtocolPercentage(uint32 _percentage) external override onlyGovernor { @@ -302,18 +188,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } /** - * @dev Internal: Set a protocol percentage to burn when collecting query fees. - * @param _percentage Percentage of query fees to burn as protocol fee - */ - function _setProtocolPercentage(uint32 _percentage) private { - // Must be within 0% to 100% (inclusive) - require(_percentage <= MAX_PPM, ">percentage"); - protocolPercentage = _percentage; - emit ParameterUpdated("protocolPercentage"); - } - - /** - * @dev Set the period in epochs that need to pass before fees in rebate pool can be claimed. + * @notice Set the period in epochs that need to pass before fees in rebate pool can be claimed. * @param _channelDisputeEpochs Period in epochs */ function setChannelDisputeEpochs(uint32 _channelDisputeEpochs) external override onlyGovernor { @@ -321,17 +196,8 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } /** - * @dev Internal: Set the period in epochs that need to pass before fees in rebate pool can be claimed. - * @param _channelDisputeEpochs Period in epochs - */ - function _setChannelDisputeEpochs(uint32 _channelDisputeEpochs) private { - require(_channelDisputeEpochs > 0, "!channelDisputeEpochs"); - channelDisputeEpochs = _channelDisputeEpochs; - emit ParameterUpdated("channelDisputeEpochs"); - } - - /** - * @dev Set the max time allowed for indexers stake on allocations. + * @notice Set the max time allowed for indexers to allocate on a subgraph + * before others are allowed to close the allocation. * @param _maxAllocationEpochs Allocation duration limit in epochs */ function setMaxAllocationEpochs(uint32 _maxAllocationEpochs) external override onlyGovernor { @@ -339,16 +205,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } /** - * @dev Internal: Set the max time allowed for indexers stake on allocations. - * @param _maxAllocationEpochs Allocation duration limit in epochs - */ - function _setMaxAllocationEpochs(uint32 _maxAllocationEpochs) private { - maxAllocationEpochs = _maxAllocationEpochs; - emit ParameterUpdated("maxAllocationEpochs"); - } - - /** - * @dev Set the rebate ratio (fees to allocated stake). + * @notice Set the rebate ratio (fees to allocated stake). * @param _alphaNumerator Numerator of `alpha` in the cobb-douglas function * @param _alphaDenominator Denominator of `alpha` in the cobb-douglas function */ @@ -361,176 +218,239 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } /** - * @dev Set the rebate ratio (fees to allocated stake). - * @param _alphaNumerator Numerator of `alpha` in the cobb-douglas function - * @param _alphaDenominator Denominator of `alpha` in the cobb-douglas function + * @notice Set an address as allowed asset holder. + * @param _assetHolder Address of allowed source for state channel funds + * @param _allowed True if asset holder is allowed */ - function _setRebateRatio(uint32 _alphaNumerator, uint32 _alphaDenominator) private { - require(_alphaNumerator > 0 && _alphaDenominator > 0, "!alpha"); - alphaNumerator = _alphaNumerator; - alphaDenominator = _alphaDenominator; - emit ParameterUpdated("rebateRatio"); + function setAssetHolder(address _assetHolder, bool _allowed) external override onlyGovernor { + require(_assetHolder != address(0), "!assetHolder"); + __assetHolders[_assetHolder] = _allowed; + emit AssetHolderUpdate(msg.sender, _assetHolder, _allowed); } /** - * @dev Set the delegation ratio. - * If set to 10 it means the indexer can use up to 10x the indexer staked amount - * from their delegated tokens - * @param _delegationRatio Delegation capacity multiplier + * @notice Authorize or unauthorize an address to be an operator for the caller. + * @param _operator Address to authorize or unauthorize + * @param _allowed Whether the operator is authorized or not */ - function setDelegationRatio(uint32 _delegationRatio) external override onlyGovernor { - _setDelegationRatio(_delegationRatio); + function setOperator(address _operator, bool _allowed) external override { + require(_operator != msg.sender, "operator == sender"); + __operatorAuth[msg.sender][_operator] = _allowed; + emit SetOperator(msg.sender, _operator, _allowed); } /** - * @dev Internal: Set the delegation ratio. - * If set to 10 it means the indexer can use up to 10x the indexer staked amount - * from their delegated tokens - * @param _delegationRatio Delegation capacity multiplier + * @notice Deposit tokens on the indexer's stake. + * The amount staked must be over the minimumIndexerStake. + * @param _tokens Amount of tokens to stake */ - function _setDelegationRatio(uint32 _delegationRatio) private { - delegationRatio = _delegationRatio; - emit ParameterUpdated("delegationRatio"); + function stake(uint256 _tokens) external override { + stakeTo(msg.sender, _tokens); } /** - * @dev Set the delegation parameters for the caller. - * @param _indexingRewardCut Percentage of indexing rewards left for delegators - * @param _queryFeeCut Percentage of query fees left for delegators - * @param _cooldownBlocks Period that need to pass to update delegation parameters + * @notice Unstake tokens from the indexer stake, lock them until the thawing period expires. + * @dev NOTE: The function accepts an amount greater than the currently staked tokens. + * If that happens, it will try to unstake the max amount of tokens it can. + * The reason for this behaviour is to avoid time conditions while the transaction + * is in flight. + * @param _tokens Amount of tokens to unstake */ - function setDelegationParameters( - uint32 _indexingRewardCut, - uint32 _queryFeeCut, - uint32 _cooldownBlocks - ) public override { - _setDelegationParameters(msg.sender, _indexingRewardCut, _queryFeeCut, _cooldownBlocks); - } + function unstake(uint256 _tokens) external override notPartialPaused { + address indexer = msg.sender; + Stakes.Indexer storage indexerStake = __stakes[indexer]; - /** - * @dev Set the delegation parameters for a particular indexer. - * @param _indexer Indexer to set delegation parameters - * @param _indexingRewardCut Percentage of indexing rewards left for delegators - * @param _queryFeeCut Percentage of query fees left for delegators - * @param _cooldownBlocks Period that need to pass to update delegation parameters - */ - function _setDelegationParameters( - address _indexer, - uint32 _indexingRewardCut, - uint32 _queryFeeCut, - uint32 _cooldownBlocks - ) private { - // Incentives must be within bounds - require(_queryFeeCut <= MAX_PPM, ">queryFeeCut"); - require(_indexingRewardCut <= MAX_PPM, ">indexingRewardCut"); + require(indexerStake.tokensStaked > 0, "!stake"); - // Cooldown period set by indexer cannot be below protocol global setting - require(_cooldownBlocks >= delegationParametersCooldown, " 0, "!stake-avail"); - // Verify the cooldown period passed - DelegationPool storage pool = delegationPools[_indexer]; - require( - pool.updatedAtBlock == 0 || - pool.updatedAtBlock.add(uint256(pool.cooldownBlocks)) <= block.number, - "!cooldown" - ); + // Ensure minimum stake + uint256 newStake = indexerStake.tokensSecureStake().sub(tokensToLock); + require(newStake == 0 || newStake >= __minimumIndexerStake, "!minimumIndexerStake"); - // Update delegation params - pool.indexingRewardCut = _indexingRewardCut; - pool.queryFeeCut = _queryFeeCut; - pool.cooldownBlocks = _cooldownBlocks; - pool.updatedAtBlock = block.number; + // Before locking more tokens, withdraw any unlocked ones if possible + uint256 tokensToWithdraw = indexerStake.tokensWithdrawable(); + if (tokensToWithdraw > 0) { + _withdraw(indexer); + } - emit DelegationParametersUpdated( - _indexer, - _indexingRewardCut, - _queryFeeCut, - _cooldownBlocks - ); - } + // Update the indexer stake locking tokens + indexerStake.lockTokens(tokensToLock, __thawingPeriod); - /** - * @dev Set the time in blocks an indexer needs to wait to change delegation parameters. - * @param _blocks Number of blocks to set the delegation parameters cooldown period - */ - function setDelegationParametersCooldown(uint32 _blocks) external override onlyGovernor { - _setDelegationParametersCooldown(_blocks); + emit StakeLocked(indexer, indexerStake.tokensLocked, indexerStake.tokensLockedUntil); } /** - * @dev Internal: Set the time in blocks an indexer needs to wait to change delegation parameters. - * @param _blocks Number of blocks to set the delegation parameters cooldown period + * @notice Withdraw indexer tokens once the thawing period has passed. */ - function _setDelegationParametersCooldown(uint32 _blocks) private { - delegationParametersCooldown = _blocks; - emit ParameterUpdated("delegationParametersCooldown"); + function withdraw() external override notPaused { + _withdraw(msg.sender); } /** - * @dev Set the period for undelegation of stake from indexer. - * @param _delegationUnbondingPeriod Period in epochs to wait for token withdrawals after undelegating + * @notice Set the destination where to send rewards for an indexer. + * @param _destination Rewards destination address. If set to zero, rewards will be restaked */ - function setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) - external - override - onlyGovernor - { - _setDelegationUnbondingPeriod(_delegationUnbondingPeriod); + function setRewardsDestination(address _destination) external override { + __rewardsDestination[msg.sender] = _destination; + emit SetRewardsDestination(msg.sender, _destination); } /** - * @dev Internal: Set the period for undelegation of stake from indexer. - * @param _delegationUnbondingPeriod Period in epochs to wait for token withdrawals after undelegating + * @notice Allocate available tokens to a subgraph deployment. + * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated + * @param _tokens Amount of tokens to allocate + * @param _allocationID The allocation identifier + * @param _metadata IPFS hash for additional information about the allocation + * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` */ - function _setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) private { - require(_delegationUnbondingPeriod > 0, "!delegationUnbondingPeriod"); - delegationUnbondingPeriod = _delegationUnbondingPeriod; - emit ParameterUpdated("delegationUnbondingPeriod"); + function allocate( + bytes32 _subgraphDeploymentID, + uint256 _tokens, + address _allocationID, + bytes32 _metadata, + bytes calldata _proof + ) external override notPaused { + _allocate(msg.sender, _subgraphDeploymentID, _tokens, _allocationID, _metadata, _proof); } /** - * @dev Set a delegation tax percentage to burn when delegated funds are deposited. - * @param _percentage Percentage of delegated tokens to burn as delegation tax + * @notice Allocate available tokens to a subgraph deployment from and indexer's stake. + * The caller must be the indexer or the indexer's operator. + * @param _indexer Indexer address to allocate funds from. + * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated + * @param _tokens Amount of tokens to allocate + * @param _allocationID The allocation identifier + * @param _metadata IPFS hash for additional information about the allocation + * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` */ - function setDelegationTaxPercentage(uint32 _percentage) external override onlyGovernor { - _setDelegationTaxPercentage(_percentage); + function allocateFrom( + address _indexer, + bytes32 _subgraphDeploymentID, + uint256 _tokens, + address _allocationID, + bytes32 _metadata, + bytes calldata _proof + ) external override notPaused { + _allocate(_indexer, _subgraphDeploymentID, _tokens, _allocationID, _metadata, _proof); } /** - * @dev Internal: Set a delegation tax percentage to burn when delegated funds are deposited. - * @param _percentage Percentage of delegated tokens to burn as delegation tax + * @notice Close an allocation and free the staked tokens. + * To be eligible for rewards a proof of indexing must be presented. + * Presenting a bad proof is subject to slashable condition. + * To opt out of rewards set _poi to 0x0 + * @param _allocationID The allocation identifier + * @param _poi Proof of indexing submitted for the allocated period */ - function _setDelegationTaxPercentage(uint32 _percentage) private { - // Must be within 0% to 100% (inclusive) - require(_percentage <= MAX_PPM, ">percentage"); - delegationTaxPercentage = _percentage; - emit ParameterUpdated("delegationTaxPercentage"); + function closeAllocation(address _allocationID, bytes32 _poi) external override notPaused { + _closeAllocation(_allocationID, _poi); } /** - * @dev Set or unset an address as allowed slasher. - * @param _slasher Address of the party allowed to slash indexers - * @param _allowed True if slasher is allowed + * @notice Collect query fees from state channels and assign them to an allocation. + * Funds received are only accepted from a valid sender. + * @dev To avoid reverting on the withdrawal from channel flow this function will: + * 1) Accept calls with zero tokens. + * 2) Accept calls after an allocation passed the dispute period, in that case, all + * the received tokens are burned. + * @param _tokens Amount of tokens to collect + * @param _allocationID Allocation where the tokens will be assigned */ - function setSlasher(address _slasher, bool _allowed) external override onlyGovernor { - require(_slasher != address(0), "!slasher"); - slashers[_slasher] = _allowed; - emit SlasherUpdate(msg.sender, _slasher, _allowed); - } + function collect(uint256 _tokens, address _allocationID) external override { + // Allocation identifier validation + require(_allocationID != address(0), "!alloc"); - /** - * @dev Set an address as allowed asset holder. - * @param _assetHolder Address of allowed source for state channel funds - * @param _allowed True if asset holder is allowed + // The contract caller must be an authorized asset holder + require(__assetHolders[msg.sender] == true, "!assetHolder"); + + // Allocation must exist + AllocationState allocState = _getAllocationState(_allocationID); + require(allocState != AllocationState.Null, "!collect"); + + // Get allocation + Allocation storage alloc = __allocations[_allocationID]; + uint256 queryFees = _tokens; + uint256 curationFees = 0; + bytes32 subgraphDeploymentID = alloc.subgraphDeploymentID; + + // Process query fees only if non-zero amount + if (queryFees > 0) { + // Pull tokens to collect from the authorized sender + IGraphToken graphToken = graphToken(); + TokenUtils.pullTokens(graphToken, msg.sender, _tokens); + + // -- Collect protocol tax -- + // If the Allocation is not active or closed we are going to charge a 100% protocol tax + uint256 usedProtocolPercentage = (allocState == AllocationState.Active || + allocState == AllocationState.Closed) + ? __protocolPercentage + : MAX_PPM; + uint256 protocolTax = _collectTax(graphToken, queryFees, usedProtocolPercentage); + queryFees = queryFees.sub(protocolTax); + + // -- Collect curation fees -- + // Only if the subgraph deployment is curated + curationFees = _collectCurationFees( + graphToken, + subgraphDeploymentID, + queryFees, + __curationPercentage + ); + queryFees = queryFees.sub(curationFees); + + // Add funds to the allocation + alloc.collectedFees = alloc.collectedFees.add(queryFees); + + // When allocation is closed redirect funds to the rebate pool + // This way we can keep collecting tokens even after the allocation is closed and + // before it gets to the finalized state. + if (allocState == AllocationState.Closed) { + Rebates.Pool storage rebatePool = __rebates[alloc.closedAtEpoch]; + rebatePool.fees = rebatePool.fees.add(queryFees); + } + } + + emit AllocationCollected( + alloc.indexer, + subgraphDeploymentID, + epochManager().currentEpoch(), + _tokens, + _allocationID, + msg.sender, + curationFees, + queryFees + ); + } + + /** + * @notice Claim tokens from the rebate pool. + * @param _allocationID Allocation from where we are claiming tokens + * @param _restake True if restake fees instead of transfer to indexer */ - function setAssetHolder(address _assetHolder, bool _allowed) external override onlyGovernor { - require(_assetHolder != address(0), "!assetHolder"); - assetHolders[_assetHolder] = _allowed; - emit AssetHolderUpdate(msg.sender, _assetHolder, _allowed); + function claim(address _allocationID, bool _restake) external override notPaused { + _claim(_allocationID, _restake); } /** - * @dev Return if allocationID is used. + * @dev Claim tokens from the rebate pool for many allocations. + * @param _allocationID Array of allocations from where we are claiming tokens + * @param _restake True if restake fees instead of transfer to indexer + */ + function claimMany(address[] calldata _allocationID, bool _restake) + external + override + notPaused + { + for (uint256 i = 0; i < _allocationID.length; i++) { + _claim(_allocationID[i], _restake); + } + } + + /** + * @notice Return if allocationID is used. * @param _allocationID Address used as signer by the indexer for an allocation * @return True if allocationID already used */ @@ -539,16 +459,16 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } /** - * @dev Getter that returns if an indexer has any stake. + * @notice Getter that returns if an indexer has any stake. * @param _indexer Address of the indexer * @return True if indexer has staked tokens */ function hasStake(address _indexer) external view override returns (bool) { - return stakes[_indexer].tokensStaked > 0; + return __stakes[_indexer].tokensStaked > 0; } /** - * @dev Return the allocation by ID. + * @notice Return the allocation by ID. * @param _allocationID Address used as allocation identifier * @return Allocation data */ @@ -558,13 +478,13 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { override returns (Allocation memory) { - return allocations[_allocationID]; + return __allocations[_allocationID]; } /** - * @dev Return the current state of an allocation. - * @param _allocationID Address used as the allocation identifier - * @return AllocationState + * @notice Return the current state of an allocation + * @param _allocationID Allocation identifier + * @return AllocationState enum with the state of the allocation */ function getAllocationState(address _allocationID) external @@ -576,8 +496,8 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } /** - * @dev Return the total amount of tokens allocated to subgraph. - * @param _subgraphDeploymentID Address used as the allocation identifier + * @notice Return the total amount of tokens allocated to subgraph. + * @param _subgraphDeploymentID Deployment ID for the subgraph * @return Total tokens allocated to subgraph */ function getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentID) @@ -586,107 +506,21 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { override returns (uint256) { - return subgraphAllocations[_subgraphDeploymentID]; - } - - /** - * @dev Return the delegation from a delegator to an indexer. - * @param _indexer Address of the indexer where funds have been delegated - * @param _delegator Address of the delegator - * @return Delegation data - */ - function getDelegation(address _indexer, address _delegator) - external - view - override - returns (Delegation memory) - { - return delegationPools[_indexer].delegators[_delegator]; - } - - /** - * @dev Return whether the delegator has delegated to the indexer. - * @param _indexer Address of the indexer where funds have been delegated - * @param _delegator Address of the delegator - * @return True if delegator of indexer - */ - function isDelegator(address _indexer, address _delegator) public view override returns (bool) { - return delegationPools[_indexer].delegators[_delegator].shares > 0; + return __subgraphAllocations[_subgraphDeploymentID]; } /** - * @dev Get the total amount of tokens staked by the indexer. + * @notice Get the total amount of tokens staked by the indexer. * @param _indexer Address of the indexer * @return Amount of tokens staked by the indexer */ function getIndexerStakedTokens(address _indexer) external view override returns (uint256) { - return stakes[_indexer].tokensStaked; - } - - /** - * @dev Get the total amount of tokens available to use in allocations. - * This considers the indexer stake and delegated tokens according to delegation ratio - * @param _indexer Address of the indexer - * @return Amount of tokens staked by the indexer - */ - function getIndexerCapacity(address _indexer) public view override returns (uint256) { - Stakes.Indexer memory indexerStake = stakes[_indexer]; - uint256 tokensDelegated = delegationPools[_indexer].tokens; - - uint256 tokensDelegatedCap = indexerStake.tokensSecureStake().mul(uint256(delegationRatio)); - uint256 tokensDelegatedCapacity = MathUtils.min(tokensDelegated, tokensDelegatedCap); - - return indexerStake.tokensAvailableWithDelegation(tokensDelegatedCapacity); - } - - /** - * @dev Returns amount of delegated tokens ready to be withdrawn after unbonding period. - * @param _delegation Delegation of tokens from delegator to indexer - * @return Amount of tokens to withdraw - */ - function getWithdraweableDelegatedTokens(Delegation memory _delegation) - public - view - returns (uint256) - { - // There must be locked tokens and period passed - uint256 currentEpoch = epochManager().currentEpoch(); - if (_delegation.tokensLockedUntil > 0 && currentEpoch >= _delegation.tokensLockedUntil) { - return _delegation.tokensLocked; - } - return 0; - } - - /** - * @dev Authorize or unauthorize an address to be an operator. - * @param _operator Address to authorize - * @param _allowed Whether authorized or not - */ - function setOperator(address _operator, bool _allowed) external override { - require(_operator != msg.sender, "operator == sender"); - operatorAuth[msg.sender][_operator] = _allowed; - emit SetOperator(msg.sender, _operator, _allowed); + return __stakes[_indexer].tokensStaked; } /** - * @dev Return true if operator is allowed for indexer. - * @param _operator Address of the operator - * @param _indexer Address of the indexer - */ - function isOperator(address _operator, address _indexer) public view override returns (bool) { - return operatorAuth[_indexer][_operator]; - } - - /** - * @dev Deposit tokens on the indexer stake. - * @param _tokens Amount of tokens to stake - */ - function stake(uint256 _tokens) external override { - stakeTo(msg.sender, _tokens); - } - - /** - * @dev Deposit tokens on the indexer stake. + * @notice Deposit tokens on the Indexer stake, on behalf of the Indexer. + * The amount staked must be over the minimumIndexerStake. * @param _indexer Address of the indexer * @param _tokens Amount of tokens to stake */ @@ -695,7 +529,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { // Ensure minimum stake require( - stakes[_indexer].tokensSecureStake().add(_tokens) >= minimumIndexerStake, + __stakes[_indexer].tokensSecureStake().add(_tokens) >= __minimumIndexerStake, "!minimumIndexerStake" ); @@ -707,349 +541,160 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } /** - * @dev Unstake tokens from the indexer stake, lock them until thawing period expires. - * NOTE: The function accepts an amount greater than the currently staked tokens. - * If that happens, it will try to unstake the max amount of tokens it can. - * The reason for this behaviour is to avoid time conditions while the transaction - * is in flight. - * @param _tokens Amount of tokens to unstake - */ - function unstake(uint256 _tokens) external override notPartialPaused { - address indexer = msg.sender; - Stakes.Indexer storage indexerStake = stakes[indexer]; - - require(indexerStake.tokensStaked > 0, "!stake"); - - // Tokens to lock is capped to the available tokens - uint256 tokensToLock = MathUtils.min(indexerStake.tokensAvailable(), _tokens); - require(tokensToLock > 0, "!stake-avail"); - - // Ensure minimum stake - uint256 newStake = indexerStake.tokensSecureStake().sub(tokensToLock); - require(newStake == 0 || newStake >= minimumIndexerStake, "!minimumIndexerStake"); - - // Before locking more tokens, withdraw any unlocked ones if possible - uint256 tokensToWithdraw = indexerStake.tokensWithdrawable(); - if (tokensToWithdraw > 0) { - _withdraw(indexer); - } - - // Update the indexer stake locking tokens - indexerStake.lockTokens(tokensToLock, thawingPeriod); - - emit StakeLocked(indexer, indexerStake.tokensLocked, indexerStake.tokensLockedUntil); - } - - /** - * @dev Withdraw indexer tokens once the thawing period has passed. - */ - function withdraw() external override notPaused { - _withdraw(msg.sender); - } - - /** - * @dev Set the destination where to send rewards. - * @param _destination Rewards destination address. If set to zero, rewards will be restaked - */ - function setRewardsDestination(address _destination) external override { - rewardsDestination[msg.sender] = _destination; - emit SetRewardsDestination(msg.sender, _destination); - } - - /** - * @dev Slash the indexer stake. Delegated tokens are not subject to slashing. - * Can only be called by the slasher role. - * @param _indexer Address of indexer to slash - * @param _tokens Amount of tokens to slash from the indexer stake - * @param _reward Amount of reward tokens to send to a beneficiary - * @param _beneficiary Address of a beneficiary to receive a reward for the slashing - */ - function slash( - address _indexer, - uint256 _tokens, - uint256 _reward, - address _beneficiary - ) external override onlySlasher notPartialPaused { - Stakes.Indexer storage indexerStake = stakes[_indexer]; - - // Only able to slash a non-zero number of tokens - require(_tokens > 0, "!tokens"); - - // Rewards comes from tokens slashed balance - require(_tokens >= _reward, "rewards>slash"); - - // Cannot slash stake of an indexer without any or enough stake - require(indexerStake.tokensStaked > 0, "!stake"); - require(_tokens <= indexerStake.tokensStaked, "slash>stake"); - - // Validate beneficiary of slashed tokens - require(_beneficiary != address(0), "!beneficiary"); - - // Slashing more tokens than freely available (over allocation condition) - // Unlock locked tokens to avoid the indexer to withdraw them - if (_tokens > indexerStake.tokensAvailable() && indexerStake.tokensLocked > 0) { - uint256 tokensOverAllocated = _tokens.sub(indexerStake.tokensAvailable()); - uint256 tokensToUnlock = MathUtils.min(tokensOverAllocated, indexerStake.tokensLocked); - indexerStake.unlockTokens(tokensToUnlock); - } - - // Remove tokens to slash from the stake - indexerStake.release(_tokens); - - // -- Interactions -- - - IGraphToken graphToken = graphToken(); - - // Set apart the reward for the beneficiary and burn remaining slashed stake - TokenUtils.burnTokens(graphToken, _tokens.sub(_reward)); - - // Give the beneficiary a reward for slashing - TokenUtils.pushTokens(graphToken, _beneficiary, _reward); - - emit StakeSlashed(_indexer, _tokens, _reward, _beneficiary); - } - - /** - * @dev Delegate tokens to an indexer. - * @param _indexer Address of the indexer to delegate tokens to - * @param _tokens Amount of tokens to delegate - * @return Amount of shares issued of the delegation pool - */ - function delegate(address _indexer, uint256 _tokens) - external - override - notPartialPaused - returns (uint256) - { - address delegator = msg.sender; - - // Transfer tokens to delegate to this contract - TokenUtils.pullTokens(graphToken(), delegator, _tokens); - - // Update state - return _delegate(delegator, _indexer, _tokens); - } - - /** - * @dev Undelegate tokens from an indexer. - * @param _indexer Address of the indexer where tokens had been delegated - * @param _shares Amount of shares to return and undelegate tokens - * @return Amount of tokens returned for the shares of the delegation pool - */ - function undelegate(address _indexer, uint256 _shares) - external - override - notPartialPaused - returns (uint256) - { - return _undelegate(msg.sender, _indexer, _shares); - } - - /** - * @dev Withdraw delegated tokens once the unbonding period has passed. - * @param _indexer Withdraw available tokens delegated to indexer - * @param _delegateToIndexer Re-delegate to indexer address if non-zero, withdraw if zero address + * @notice Set the delegation parameters for the caller. + * @param _indexingRewardCut Percentage of indexing rewards left for the indexer + * @param _queryFeeCut Percentage of query fees left for the indexer + * @param _cooldownBlocks Period that need to pass to update delegation parameters */ - function withdrawDelegated(address _indexer, address _delegateToIndexer) - external - override - notPaused - returns (uint256) - { - return _withdrawDelegated(msg.sender, _indexer, _delegateToIndexer); + function setDelegationParameters( + uint32 _indexingRewardCut, + uint32 _queryFeeCut, + uint32 _cooldownBlocks + ) public override { + _setDelegationParameters(msg.sender, _indexingRewardCut, _queryFeeCut, _cooldownBlocks); } /** - * @dev Allocate available tokens to a subgraph deployment. - * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated - * @param _tokens Amount of tokens to allocate - * @param _allocationID The allocation identifier - * @param _metadata IPFS hash for additional information about the allocation - * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` + * @notice Get the total amount of tokens available to use in allocations. + * This considers the indexer stake and delegated tokens according to delegation ratio + * @param _indexer Address of the indexer + * @return Amount of tokens available to allocate including delegation */ - function allocate( - bytes32 _subgraphDeploymentID, - uint256 _tokens, - address _allocationID, - bytes32 _metadata, - bytes calldata _proof - ) external override notPaused { - _allocate(msg.sender, _subgraphDeploymentID, _tokens, _allocationID, _metadata, _proof); - } + function getIndexerCapacity(address _indexer) public view override returns (uint256) { + Stakes.Indexer memory indexerStake = __stakes[_indexer]; + uint256 tokensDelegated = __delegationPools[_indexer].tokens; - /** - * @dev Allocate available tokens to a subgraph deployment. - * @param _indexer Indexer address to allocate funds from. - * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated - * @param _tokens Amount of tokens to allocate - * @param _allocationID The allocation identifier - * @param _metadata IPFS hash for additional information about the allocation - * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` - */ - function allocateFrom( - address _indexer, - bytes32 _subgraphDeploymentID, - uint256 _tokens, - address _allocationID, - bytes32 _metadata, - bytes calldata _proof - ) external override notPaused { - _allocate(_indexer, _subgraphDeploymentID, _tokens, _allocationID, _metadata, _proof); - } + uint256 tokensDelegatedCap = indexerStake.tokensSecureStake().mul( + uint256(__delegationRatio) + ); + uint256 tokensDelegatedCapacity = MathUtils.min(tokensDelegated, tokensDelegatedCap); - /** - * @dev Close an allocation and free the staked tokens. - * To be eligible for rewards a proof of indexing must be presented. - * Presenting a bad proof is subject to slashable condition. - * To opt out for rewards set _poi to 0x0 - * @param _allocationID The allocation identifier - * @param _poi Proof of indexing submitted for the allocated period - */ - function closeAllocation(address _allocationID, bytes32 _poi) external override notPaused { - _closeAllocation(_allocationID, _poi); + return indexerStake.tokensAvailableWithDelegation(tokensDelegatedCapacity); } /** - * @dev Close multiple allocations and free the staked tokens. - * To be eligible for rewards a proof of indexing must be presented. - * Presenting a bad proof is subject to slashable condition. - * To opt out for rewards set _poi to 0x0 - * @param _requests An array of CloseAllocationRequest + * @notice Return true if operator is allowed for indexer. + * @param _operator Address of the operator + * @param _indexer Address of the indexer + * @return True if operator is allowed for indexer, false otherwise */ - function closeAllocationMany(CloseAllocationRequest[] calldata _requests) - external - override - notPaused - { - for (uint256 i = 0; i < _requests.length; i++) { - _closeAllocation(_requests[i].allocationID, _requests[i].poi); - } + function isOperator(address _operator, address _indexer) public view override returns (bool) { + return __operatorAuth[_indexer][_operator]; } /** - * @dev Close and allocate. This will perform a close and then create a new Allocation - * atomically on the same transaction. - * @param _closingAllocationID The identifier of the allocation to be closed - * @param _poi Proof of indexing submitted for the allocated period - * @param _indexer Indexer address to allocate funds from. - * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated - * @param _tokens Amount of tokens to allocate - * @param _allocationID The allocation identifier - * @param _metadata IPFS hash for additional information about the allocation - * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` + * @dev Internal: Set the minimum indexer stake required. + * @param _minimumIndexerStake Minimum indexer stake */ - function closeAndAllocate( - address _closingAllocationID, - bytes32 _poi, - address _indexer, - bytes32 _subgraphDeploymentID, - uint256 _tokens, - address _allocationID, - bytes32 _metadata, - bytes calldata _proof - ) external override notPaused { - _closeAllocation(_closingAllocationID, _poi); - _allocate(_indexer, _subgraphDeploymentID, _tokens, _allocationID, _metadata, _proof); + function _setMinimumIndexerStake(uint256 _minimumIndexerStake) private { + require(_minimumIndexerStake > 0, "!minimumIndexerStake"); + __minimumIndexerStake = _minimumIndexerStake; + emit ParameterUpdated("minimumIndexerStake"); } /** - * @dev Collect query fees from state channels and assign them to an allocation. - * Funds received are only accepted from a valid sender. - * To avoid reverting on the withdrawal from channel flow this function will: - * 1) Accept calls with zero tokens. - * 2) Accept calls after an allocation passed the dispute period, in that case, all - * the received tokens are burned. - * @param _tokens Amount of tokens to collect - * @param _allocationID Allocation where the tokens will be assigned + * @dev Internal: Set the thawing period for unstaking. + * @param _thawingPeriod Period in blocks to wait for token withdrawals after unstaking */ - function collect(uint256 _tokens, address _allocationID) external override { - // Allocation identifier validation - require(_allocationID != address(0), "!alloc"); - - // The contract caller must be an authorized asset holder - require(assetHolders[msg.sender] == true, "!assetHolder"); - - // Allocation must exist - AllocationState allocState = _getAllocationState(_allocationID); - require(allocState != AllocationState.Null, "!collect"); - - // Get allocation - Allocation storage alloc = allocations[_allocationID]; - uint256 queryFees = _tokens; - uint256 curationFees = 0; - bytes32 subgraphDeploymentID = alloc.subgraphDeploymentID; - - // Process query fees only if non-zero amount - if (queryFees > 0) { - // Pull tokens to collect from the authorized sender - IGraphToken graphToken = graphToken(); - TokenUtils.pullTokens(graphToken, msg.sender, _tokens); - - // -- Collect protocol tax -- - // If the Allocation is not active or closed we are going to charge a 100% protocol tax - uint256 usedProtocolPercentage = (allocState == AllocationState.Active || - allocState == AllocationState.Closed) - ? protocolPercentage - : MAX_PPM; - uint256 protocolTax = _collectTax(graphToken, queryFees, usedProtocolPercentage); - queryFees = queryFees.sub(protocolTax); + function _setThawingPeriod(uint32 _thawingPeriod) private { + require(_thawingPeriod > 0, "!thawingPeriod"); + __thawingPeriod = _thawingPeriod; + emit ParameterUpdated("thawingPeriod"); + } - // -- Collect curation fees -- - // Only if the subgraph deployment is curated - curationFees = _collectCurationFees( - graphToken, - subgraphDeploymentID, - queryFees, - curationPercentage - ); - queryFees = queryFees.sub(curationFees); + /** + * @dev Internal: Set the curation percentage of query fees sent to curators. + * @param _percentage Percentage of query fees sent to curators + */ + function _setCurationPercentage(uint32 _percentage) private { + // Must be within 0% to 100% (inclusive) + require(_percentage <= MAX_PPM, ">percentage"); + __curationPercentage = _percentage; + emit ParameterUpdated("curationPercentage"); + } - // Add funds to the allocation - alloc.collectedFees = alloc.collectedFees.add(queryFees); + /** + * @dev Internal: Set a protocol percentage to burn when collecting query fees. + * @param _percentage Percentage of query fees to burn as protocol fee + */ + function _setProtocolPercentage(uint32 _percentage) private { + // Must be within 0% to 100% (inclusive) + require(_percentage <= MAX_PPM, ">percentage"); + __protocolPercentage = _percentage; + emit ParameterUpdated("protocolPercentage"); + } - // When allocation is closed redirect funds to the rebate pool - // This way we can keep collecting tokens even after the allocation is closed and - // before it gets to the finalized state. - if (allocState == AllocationState.Closed) { - Rebates.Pool storage rebatePool = rebates[alloc.closedAtEpoch]; - rebatePool.fees = rebatePool.fees.add(queryFees); - } - } + /** + * @dev Internal: Set the period in epochs that need to pass before fees in rebate pool can be claimed. + * @param _channelDisputeEpochs Period in epochs + */ + function _setChannelDisputeEpochs(uint32 _channelDisputeEpochs) private { + require(_channelDisputeEpochs > 0, "!channelDisputeEpochs"); + __channelDisputeEpochs = _channelDisputeEpochs; + emit ParameterUpdated("channelDisputeEpochs"); + } - emit AllocationCollected( - alloc.indexer, - subgraphDeploymentID, - epochManager().currentEpoch(), - _tokens, - _allocationID, - msg.sender, - curationFees, - queryFees - ); + /** + * @dev Internal: Set the max time allowed for indexers stake on allocations. + * @param _maxAllocationEpochs Allocation duration limit in epochs + */ + function _setMaxAllocationEpochs(uint32 _maxAllocationEpochs) private { + __maxAllocationEpochs = _maxAllocationEpochs; + emit ParameterUpdated("maxAllocationEpochs"); } /** - * @dev Claim tokens from the rebate pool. - * @param _allocationID Allocation from where we are claiming tokens - * @param _restake True if restake fees instead of transfer to indexer + * @dev Set the rebate ratio (fees to allocated stake). + * @param _alphaNumerator Numerator of `alpha` in the cobb-douglas function + * @param _alphaDenominator Denominator of `alpha` in the cobb-douglas function */ - function claim(address _allocationID, bool _restake) external override notPaused { - _claim(_allocationID, _restake); + function _setRebateRatio(uint32 _alphaNumerator, uint32 _alphaDenominator) private { + require(_alphaNumerator > 0 && _alphaDenominator > 0, "!alpha"); + __alphaNumerator = _alphaNumerator; + __alphaDenominator = _alphaDenominator; + emit ParameterUpdated("rebateRatio"); } /** - * @dev Claim tokens from the rebate pool for many allocations. - * @param _allocationID Array of allocations from where we are claiming tokens - * @param _restake True if restake fees instead of transfer to indexer + * @dev Set the delegation parameters for a particular indexer. + * @param _indexer Indexer to set delegation parameters + * @param _indexingRewardCut Percentage of indexing rewards left for delegators + * @param _queryFeeCut Percentage of query fees left for delegators + * @param _cooldownBlocks Period that need to pass to update delegation parameters */ - function claimMany(address[] calldata _allocationID, bool _restake) - external - override - notPaused - { - for (uint256 i = 0; i < _allocationID.length; i++) { - _claim(_allocationID[i], _restake); - } + function _setDelegationParameters( + address _indexer, + uint32 _indexingRewardCut, + uint32 _queryFeeCut, + uint32 _cooldownBlocks + ) private { + // Incentives must be within bounds + require(_queryFeeCut <= MAX_PPM, ">queryFeeCut"); + require(_indexingRewardCut <= MAX_PPM, ">indexingRewardCut"); + + // Cooldown period set by indexer cannot be below protocol global setting + require(_cooldownBlocks >= __delegationParametersCooldown, " 0, "!tokens"); // Return tokens to the indexer @@ -1116,15 +761,13 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { bytes32 digest = ECDSA.toEthSignedMessageHash(messageHash); require(ECDSA.recover(digest, _proof) == _allocationID, "!proof"); + require( + __stakes[_indexer].tokensSecureStake() >= __minimumIndexerStake, + "!minimumIndexerStake" + ); if (_tokens > 0) { // Needs to have free capacity not used for other purposes to allocate require(getIndexerCapacity(_indexer) >= _tokens, "!capacity"); - } else { - // Allocating zero-tokens still needs to comply with stake requirements - require( - stakes[_indexer].tokensSecureStake() >= minimumIndexerStake, - "!minimumIndexerStake" - ); } // Creates an allocation @@ -1140,18 +783,18 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { 0, // Initialize effective allocation (_tokens > 0) ? _updateRewards(_subgraphDeploymentID) : 0 // Initialize accumulated rewards per stake allocated ); - allocations[_allocationID] = alloc; + __allocations[_allocationID] = alloc; // -- Rewards Distribution -- // Process non-zero-allocation rewards tracking if (_tokens > 0) { // Mark allocated tokens as used - stakes[_indexer].allocate(alloc.tokens); + __stakes[_indexer].allocate(alloc.tokens); // Track total allocations per subgraph // Used for rewards calculations - subgraphAllocations[alloc.subgraphDeploymentID] = subgraphAllocations[ + __subgraphAllocations[alloc.subgraphDeploymentID] = __subgraphAllocations[ alloc.subgraphDeploymentID ].add(alloc.tokens); } @@ -1177,7 +820,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { require(allocState == AllocationState.Active, "!active"); // Get allocation - Allocation memory alloc = allocations[_allocationID]; + Allocation memory alloc = __allocations[_allocationID]; // Validate that an allocation cannot be closed before one epoch alloc.closedAtEpoch = epochManager().currentEpoch(); @@ -1189,28 +832,28 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { // - After maxAllocationEpochs passed // - When the allocation is for non-zero amount of tokens bool isIndexer = _isAuth(alloc.indexer); - if (epochs <= maxAllocationEpochs || alloc.tokens == 0) { + if (epochs <= __maxAllocationEpochs || alloc.tokens == 0) { require(isIndexer, "!auth"); } // Close the allocation and start counting a period to settle remaining payments from // state channels. - allocations[_allocationID].closedAtEpoch = alloc.closedAtEpoch; + __allocations[_allocationID].closedAtEpoch = alloc.closedAtEpoch; // -- Rebate Pool -- // Calculate effective allocation for the amount of epochs it remained allocated alloc.effectiveAllocation = _getEffectiveAllocation( - maxAllocationEpochs, + __maxAllocationEpochs, alloc.tokens, epochs ); - allocations[_allocationID].effectiveAllocation = alloc.effectiveAllocation; + __allocations[_allocationID].effectiveAllocation = alloc.effectiveAllocation; // Account collected fees and effective allocation in rebate pool for the epoch - Rebates.Pool storage rebatePool = rebates[alloc.closedAtEpoch]; + Rebates.Pool storage rebatePool = __rebates[alloc.closedAtEpoch]; if (!rebatePool.exists()) { - rebatePool.init(alphaNumerator, alphaDenominator); + rebatePool.init(__alphaNumerator, __alphaDenominator); } rebatePool.addToPool(alloc.collectedFees, alloc.effectiveAllocation); @@ -1226,11 +869,11 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } // Free allocated tokens from use - stakes[alloc.indexer].unallocate(alloc.tokens); + __stakes[alloc.indexer].unallocate(alloc.tokens); // Track total allocations per subgraph // Used for rewards calculations - subgraphAllocations[alloc.subgraphDeploymentID] = subgraphAllocations[ + __subgraphAllocations[alloc.subgraphDeploymentID] = __subgraphAllocations[ alloc.subgraphDeploymentID ].sub(alloc.tokens); } @@ -1259,13 +902,13 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { require(allocState == AllocationState.Finalized, "!finalized"); // Get allocation - Allocation memory alloc = allocations[_allocationID]; + Allocation memory alloc = __allocations[_allocationID]; // Only the indexer or operator can decide if to restake bool restake = _isAuth(alloc.indexer) ? _restake : false; // Process rebate reward - Rebates.Pool storage rebatePool = rebates[alloc.closedAtEpoch]; + Rebates.Pool storage rebatePool = __rebates[alloc.closedAtEpoch]; uint256 tokensToClaim = rebatePool.redeem(alloc.collectedFees, alloc.effectiveAllocation); // Add delegation rewards to the delegation pool @@ -1275,12 +918,12 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { // Purge allocation data except for: // - indexer: used in disputes and to avoid reusing an allocationID // - subgraphDeploymentID: used in disputes - allocations[_allocationID].tokens = 0; - allocations[_allocationID].createdAtEpoch = 0; // This avoid collect(), close() and claim() to be called - allocations[_allocationID].closedAtEpoch = 0; - allocations[_allocationID].collectedFees = 0; - allocations[_allocationID].effectiveAllocation = 0; - allocations[_allocationID].accRewardsPerAllocatedToken = 0; + __allocations[_allocationID].tokens = 0; + __allocations[_allocationID].createdAtEpoch = 0; // This avoid collect(), close() and claim() to be called + __allocations[_allocationID].closedAtEpoch = 0; + __allocations[_allocationID].collectedFees = 0; + __allocations[_allocationID].effectiveAllocation = 0; + __allocations[_allocationID].accRewardsPerAllocatedToken = 0; // -- Interactions -- @@ -1289,7 +932,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { // When all allocations processed then burn unclaimed fees and prune rebate pool if (rebatePool.unclaimedAllocationsCount == 0) { TokenUtils.burnTokens(graphToken, rebatePool.unclaimedFees()); - delete rebates[alloc.closedAtEpoch]; + delete __rebates[alloc.closedAtEpoch]; } // When there are tokens to claim from the rebate pool, transfer or restake @@ -1307,139 +950,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { ); } - /** - * @dev Delegate tokens to an indexer. - * @param _delegator Address of the delegator - * @param _indexer Address of the indexer to delegate tokens to - * @param _tokens Amount of tokens to delegate - * @return Amount of shares issued of the delegation pool - */ - function _delegate( - address _delegator, - address _indexer, - uint256 _tokens - ) private returns (uint256) { - // Only delegate a non-zero amount of tokens - require(_tokens > 0, "!tokens"); - // Only delegate to non-empty address - require(_indexer != address(0), "!indexer"); - // Only delegate to staked indexer - require(stakes[_indexer].tokensStaked > 0, "!stake"); - - // Get the delegation pool of the indexer - DelegationPool storage pool = delegationPools[_indexer]; - Delegation storage delegation = pool.delegators[_delegator]; - - // Collect delegation tax - uint256 delegationTax = _collectTax(graphToken(), _tokens, delegationTaxPercentage); - uint256 delegatedTokens = _tokens.sub(delegationTax); - - // Calculate shares to issue - uint256 shares = (pool.tokens == 0) - ? delegatedTokens - : delegatedTokens.mul(pool.shares).div(pool.tokens); - require(shares > 0, "!shares"); - - // Update the delegation pool - pool.tokens = pool.tokens.add(delegatedTokens); - pool.shares = pool.shares.add(shares); - - // Update the individual delegation - delegation.shares = delegation.shares.add(shares); - - emit StakeDelegated(_indexer, _delegator, delegatedTokens, shares); - - return shares; - } - - /** - * @dev Undelegate tokens from an indexer. - * @param _delegator Address of the delegator - * @param _indexer Address of the indexer where tokens had been delegated - * @param _shares Amount of shares to return and undelegate tokens - * @return Amount of tokens returned for the shares of the delegation pool - */ - function _undelegate( - address _delegator, - address _indexer, - uint256 _shares - ) private returns (uint256) { - // Can only undelegate a non-zero amount of shares - require(_shares > 0, "!shares"); - - // Get the delegation pool of the indexer - DelegationPool storage pool = delegationPools[_indexer]; - Delegation storage delegation = pool.delegators[_delegator]; - - // Delegator need to have enough shares in the pool to undelegate - require(delegation.shares >= _shares, "!shares-avail"); - - // Withdraw tokens if available - if (getWithdraweableDelegatedTokens(delegation) > 0) { - _withdrawDelegated(_delegator, _indexer, address(0)); - } - - // Calculate tokens to get in exchange for the shares - uint256 tokens = _shares.mul(pool.tokens).div(pool.shares); - - // Update the delegation pool - pool.tokens = pool.tokens.sub(tokens); - pool.shares = pool.shares.sub(_shares); - - // Update the delegation - delegation.shares = delegation.shares.sub(_shares); - delegation.tokensLocked = delegation.tokensLocked.add(tokens); - delegation.tokensLockedUntil = epochManager().currentEpoch().add(delegationUnbondingPeriod); - - emit StakeDelegatedLocked( - _indexer, - _delegator, - tokens, - _shares, - delegation.tokensLockedUntil - ); - - return tokens; - } - - /** - * @dev Withdraw delegated tokens once the unbonding period has passed. - * @param _delegator Delegator that is withdrawing tokens - * @param _indexer Withdraw available tokens delegated to indexer - * @param _delegateToIndexer Re-delegate to indexer address if non-zero, withdraw if zero address - */ - function _withdrawDelegated( - address _delegator, - address _indexer, - address _delegateToIndexer - ) private returns (uint256) { - // Get the delegation pool of the indexer - DelegationPool storage pool = delegationPools[_indexer]; - Delegation storage delegation = pool.delegators[_delegator]; - - // Validation - uint256 tokensToWithdraw = getWithdraweableDelegatedTokens(delegation); - require(tokensToWithdraw > 0, "!tokens"); - - // Reset lock - delegation.tokensLocked = 0; - delegation.tokensLockedUntil = 0; - - emit StakeDelegatedWithdrawn(_indexer, _delegator, tokensToWithdraw); - - // -- Interactions -- - - if (_delegateToIndexer != address(0)) { - // Re-delegate tokens to a new indexer - _delegate(_delegator, _delegateToIndexer, tokensToWithdraw); - } else { - // Return tokens to the delegator - TokenUtils.pushTokens(graphToken(), _delegator, tokensToWithdraw); - } - - return tokensToWithdraw; - } - /** * @dev Collect the delegation rewards for query fees. * This function will assign the collected fees to the delegation pool. @@ -1452,7 +962,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { returns (uint256) { uint256 delegationRewards = 0; - DelegationPool storage pool = delegationPools[_indexer]; + DelegationPool storage pool = __delegationPools[_indexer]; if (pool.tokens > 0 && pool.queryFeeCut < MAX_PPM) { uint256 indexerCut = uint256(pool.queryFeeCut).mul(_tokens).div(MAX_PPM); delegationRewards = _tokens.sub(indexerCut); @@ -1473,7 +983,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { returns (uint256) { uint256 delegationRewards = 0; - DelegationPool storage pool = delegationPools[_indexer]; + DelegationPool storage pool = __delegationPools[_indexer]; if (pool.tokens > 0 && pool.indexingRewardCut < MAX_PPM) { uint256 indexerCut = uint256(pool.indexingRewardCut).mul(_tokens).div(MAX_PPM); delegationRewards = _tokens.sub(indexerCut); @@ -1535,52 +1045,10 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { return tax; } - /** - * @dev Return the current state of an allocation - * @param _allocationID Allocation identifier - * @return AllocationState - */ - function _getAllocationState(address _allocationID) private view returns (AllocationState) { - Allocation storage alloc = allocations[_allocationID]; - - if (alloc.indexer == address(0)) { - return AllocationState.Null; - } - if (alloc.createdAtEpoch == 0) { - return AllocationState.Claimed; - } - - uint256 closedAtEpoch = alloc.closedAtEpoch; - if (closedAtEpoch == 0) { - return AllocationState.Active; - } - - uint256 epochs = epochManager().epochsSince(closedAtEpoch); - if (epochs >= channelDisputeEpochs) { - return AllocationState.Finalized; - } - return AllocationState.Closed; - } - - /** - * @dev Get the effective stake allocation considering epochs from allocation to closing. - * @param _maxAllocationEpochs Max amount of epochs to cap the allocated stake - * @param _tokens Amount of tokens allocated - * @param _numEpochs Number of epochs that passed from allocation to closing - * @return Effective allocated tokens across epochs - */ - function _getEffectiveAllocation( - uint256 _maxAllocationEpochs, - uint256 _tokens, - uint256 _numEpochs - ) private pure returns (uint256) { - bool shouldCap = _maxAllocationEpochs > 0 && _numEpochs > _maxAllocationEpochs; - return _tokens.mul((shouldCap) ? _maxAllocationEpochs : _numEpochs); - } - /** * @dev Triggers an update of rewards due to a change in allocations. * @param _subgraphDeploymentID Subgraph deployment updated + * @return Accumulated rewards per allocated token for the subgraph deployment */ function _updateRewards(bytes32 _subgraphDeploymentID) private returns (uint256) { IRewardsManager rewardsManager = rewardsManager(); @@ -1593,6 +1061,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { /** * @dev Assign rewards for the closed allocation to indexer and delegators. * @param _allocationID Allocation + * @param _indexer Address of the indexer that did the allocation */ function _distributeRewards(address _allocationID, address _indexer) private { IRewardsManager rewardsManager = rewardsManager(); @@ -1617,7 +1086,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { graphToken(), indexerRewards, _indexer, - rewardsDestination[_indexer] == address(0) + __rewardsDestination[_indexer] == address(0) ); } @@ -1641,7 +1110,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { _stake(_beneficiary, _amount); } else { // Transfer funds to the beneficiary's designated rewards destination if set - address destination = rewardsDestination[_beneficiary]; + address destination = __rewardsDestination[_beneficiary]; TokenUtils.pushTokens( _graphToken, destination == address(0) ? _beneficiary : destination, @@ -1649,4 +1118,57 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { ); } } + + /** + * @dev Check if the caller is authorized to operate on behalf of + * an indexer (i.e. the caller is the indexer or an operator) + * @param _indexer Indexer address + * @return True if the caller is authorized to operate on behalf of the indexer + */ + function _isAuth(address _indexer) private view returns (bool) { + return msg.sender == _indexer || isOperator(msg.sender, _indexer) == true; + } + + /** + * @dev Return the current state of an allocation + * @param _allocationID Allocation identifier + * @return AllocationState enum with the state of the allocation + */ + function _getAllocationState(address _allocationID) private view returns (AllocationState) { + Allocation storage alloc = __allocations[_allocationID]; + + if (alloc.indexer == address(0)) { + return AllocationState.Null; + } + if (alloc.createdAtEpoch == 0) { + return AllocationState.Claimed; + } + + uint256 closedAtEpoch = alloc.closedAtEpoch; + if (closedAtEpoch == 0) { + return AllocationState.Active; + } + + uint256 epochs = epochManager().epochsSince(closedAtEpoch); + if (epochs >= __channelDisputeEpochs) { + return AllocationState.Finalized; + } + return AllocationState.Closed; + } + + /** + * @dev Get the effective stake allocation considering epochs from allocation to closing. + * @param _maxAllocationEpochs Max amount of epochs to cap the allocated stake + * @param _tokens Amount of tokens allocated + * @param _numEpochs Number of epochs that passed from allocation to closing + * @return Effective allocated tokens across epochs + */ + function _getEffectiveAllocation( + uint256 _maxAllocationEpochs, + uint256 _tokens, + uint256 _numEpochs + ) private pure returns (uint256) { + bool shouldCap = _maxAllocationEpochs > 0 && _numEpochs > _maxAllocationEpochs; + return _tokens.mul((shouldCap) ? _maxAllocationEpochs : _numEpochs); + } } diff --git a/contracts/staking/StakingExtension.sol b/contracts/staking/StakingExtension.sol new file mode 100644 index 000000000..8b0657f19 --- /dev/null +++ b/contracts/staking/StakingExtension.sol @@ -0,0 +1,689 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { StakingV3Storage } from "./StakingStorage.sol"; +import { IStakingExtension } from "./IStakingExtension.sol"; +import { TokenUtils } from "../utils/TokenUtils.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; +import { Rebates } from "./libs/Rebates.sol"; +import { Stakes } from "./libs/Stakes.sol"; +import { IStakingData } from "./IStakingData.sol"; +import { MathUtils } from "./libs/MathUtils.sol"; + +/** + * @title StakingExtension contract + * @dev This contract provides the logic to manage delegations and other Staking + * extension features (e.g. storage getters). It is meant to be called through delegatecall from the + * Staking contract, and is only kept separate to keep the Staking contract size + * within limits. + */ +contract StakingExtension is StakingV3Storage, GraphUpgradeable, IStakingExtension { + using SafeMath for uint256; + using Stakes for Stakes.Indexer; + + /// @dev 100% in parts per million + uint32 private constant MAX_PPM = 1000000; + + /** + * @dev Check if the caller is the slasher. + */ + modifier onlySlasher() { + require(__slashers[msg.sender] == true, "!slasher"); + _; + } + + /** + * @notice Initialize the StakingExtension contract + * @dev This function is meant to be delegatecalled from the Staking contract's + * initialize() function, so it uses the same access control check to ensure it is + * being called by the Staking implementation as part of the proxy upgrade process. + * @param _delegationUnbondingPeriod Delegation unbonding period in blocks + * @param _cooldownBlocks Minimum time between changes to delegation parameters, in blocks + * @param _delegationRatio Delegation capacity multiplier (e.g. 10 means 10x the indexer stake) + * @param _delegationTaxPercentage Percentage of delegated tokens to burn as delegation tax, expressed in parts per million + */ + function initialize( + uint32 _delegationUnbondingPeriod, + uint32 _cooldownBlocks, + uint32 _delegationRatio, + uint32 _delegationTaxPercentage + ) external onlyImpl { + _setDelegationUnbondingPeriod(_delegationUnbondingPeriod); + _setDelegationParametersCooldown(_cooldownBlocks); + _setDelegationRatio(_delegationRatio); + _setDelegationTaxPercentage(_delegationTaxPercentage); + } + + /** + * @notice Set a delegation tax percentage to burn when delegated funds are deposited. + * @dev This function is only callable by the governor + * @param _percentage Percentage of delegated tokens to burn as delegation tax, expressed in parts per million + */ + function setDelegationTaxPercentage(uint32 _percentage) external override onlyGovernor { + _setDelegationTaxPercentage(_percentage); + } + + /** + * @notice Set the delegation ratio. + * If set to 10 it means the indexer can use up to 10x the indexer staked amount + * from their delegated tokens + * @dev This function is only callable by the governor + * @param _delegationRatio Delegation capacity multiplier + */ + function setDelegationRatio(uint32 _delegationRatio) external override onlyGovernor { + _setDelegationRatio(_delegationRatio); + } + + /** + * @notice Set the minimum time in blocks an indexer needs to wait to change delegation parameters. + * Indexers can set a custom amount time for their own cooldown, but it must be greater than this. + * @dev This function is only callable by the governor + * @param _blocks Number of blocks to set the delegation parameters cooldown period + */ + function setDelegationParametersCooldown(uint32 _blocks) external override onlyGovernor { + _setDelegationParametersCooldown(_blocks); + } + + /** + * @notice Set the time, in epochs, a Delegator needs to wait to withdraw tokens after undelegating. + * @dev This function is only callable by the governor + * @param _delegationUnbondingPeriod Period in epochs to wait for token withdrawals after undelegating + */ + function setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) + external + override + onlyGovernor + { + _setDelegationUnbondingPeriod(_delegationUnbondingPeriod); + } + + /** + * @notice Set or unset an address as allowed slasher. + * @param _slasher Address of the party allowed to slash indexers + * @param _allowed True if slasher is allowed + */ + function setSlasher(address _slasher, bool _allowed) external override onlyGovernor { + require(_slasher != address(0), "!slasher"); + __slashers[_slasher] = _allowed; + emit SlasherUpdate(msg.sender, _slasher, _allowed); + } + + /** + * @notice Delegate tokens to an indexer. + * @param _indexer Address of the indexer to which tokens are delegated + * @param _tokens Amount of tokens to delegate + * @return Amount of shares issued from the delegation pool + */ + function delegate(address _indexer, uint256 _tokens) + external + override + notPartialPaused + returns (uint256) + { + address delegator = msg.sender; + + // Transfer tokens to delegate to this contract + TokenUtils.pullTokens(graphToken(), delegator, _tokens); + + // Update state + return _delegate(delegator, _indexer, _tokens); + } + + /** + * @notice Undelegate tokens from an indexer. Tokens will be locked for the unbonding period. + * @param _indexer Address of the indexer to which tokens had been delegated + * @param _shares Amount of shares to return and undelegate tokens + * @return Amount of tokens returned for the shares of the delegation pool + */ + function undelegate(address _indexer, uint256 _shares) + external + override + notPartialPaused + returns (uint256) + { + return _undelegate(msg.sender, _indexer, _shares); + } + + /** + * @notice Withdraw undelegated tokens once the unbonding period has passed, and optionally + * re-delegate to a new indexer. + * @param _indexer Withdraw available tokens delegated to indexer + * @param _newIndexer Re-delegate to indexer address if non-zero, withdraw if zero address + */ + function withdrawDelegated(address _indexer, address _newIndexer) + external + override + notPaused + returns (uint256) + { + return _withdrawDelegated(msg.sender, _indexer, _newIndexer); + } + + /** + * @notice Slash the indexer stake. Delegated tokens are not subject to slashing. + * @dev Can only be called by the slasher role. + * @param _indexer Address of indexer to slash + * @param _tokens Amount of tokens to slash from the indexer stake + * @param _reward Amount of reward tokens to send to a beneficiary + * @param _beneficiary Address of a beneficiary to receive a reward for the slashing + */ + function slash( + address _indexer, + uint256 _tokens, + uint256 _reward, + address _beneficiary + ) external override onlySlasher notPartialPaused { + Stakes.Indexer storage indexerStake = __stakes[_indexer]; + + // Only able to slash a non-zero number of tokens + require(_tokens > 0, "!tokens"); + + // Rewards comes from tokens slashed balance + require(_tokens >= _reward, "rewards>slash"); + + // Cannot slash stake of an indexer without any or enough stake + require(indexerStake.tokensStaked > 0, "!stake"); + require(_tokens <= indexerStake.tokensStaked, "slash>stake"); + + // Validate beneficiary of slashed tokens + require(_beneficiary != address(0), "!beneficiary"); + + // Slashing more tokens than freely available (over allocation condition) + // Unlock locked tokens to avoid the indexer to withdraw them + if (_tokens > indexerStake.tokensAvailable() && indexerStake.tokensLocked > 0) { + uint256 tokensOverAllocated = _tokens.sub(indexerStake.tokensAvailable()); + uint256 tokensToUnlock = MathUtils.min(tokensOverAllocated, indexerStake.tokensLocked); + indexerStake.unlockTokens(tokensToUnlock); + } + + // Remove tokens to slash from the stake + indexerStake.release(_tokens); + + // -- Interactions -- + + IGraphToken graphToken = graphToken(); + + // Set apart the reward for the beneficiary and burn remaining slashed stake + TokenUtils.burnTokens(graphToken, _tokens.sub(_reward)); + + // Give the beneficiary a reward for slashing + TokenUtils.pushTokens(graphToken, _beneficiary, _reward); + + emit StakeSlashed(_indexer, _tokens, _reward, _beneficiary); + } + + /** + * @notice Return the delegation from a delegator to an indexer. + * @param _indexer Address of the indexer where funds have been delegated + * @param _delegator Address of the delegator + * @return Delegation data + */ + function getDelegation(address _indexer, address _delegator) + external + view + override + returns (Delegation memory) + { + return __delegationPools[_indexer].delegators[_delegator]; + } + + /** + * @notice Getter for the delegationRatio, i.e. the delegation capacity multiplier: + * If delegation ratio is 100, and an Indexer has staked 5 GRT, + * then they can use up to 500 GRT from the delegated stake + * @return Delegation ratio + */ + function delegationRatio() external view override returns (uint32) { + return __delegationRatio; + } + + /** + * @notice Getter for delegationParametersCooldown: + * Minimum time in blocks an indexer needs to wait to change delegation parameters + * @return Delegation parameters cooldown in blocks + */ + function delegationParametersCooldown() external view override returns (uint32) { + return __delegationParametersCooldown; + } + + /** + * @notice Getter for delegationUnbondingPeriod: + * Time in epochs a delegator needs to wait to withdraw delegated stake + * @return Delegation unbonding period in epochs + */ + function delegationUnbondingPeriod() external view override returns (uint32) { + return __delegationUnbondingPeriod; + } + + /** + * @notice Getter for delegationTaxPercentage: + * Percentage of tokens to tax a delegation deposit, expressed in parts per million + * @return Delegation tax percentage in parts per million + */ + function delegationTaxPercentage() external view override returns (uint32) { + return __delegationTaxPercentage; + } + + /** + * @notice Getter for delegationPools[_indexer]: + * gets the delegation pool structure for a particular indexer. + * @param _indexer Address of the indexer for which to query the delegation pool + * @return Delegation pool as a DelegationPoolReturn struct + */ + function delegationPools(address _indexer) + external + view + override + returns (DelegationPoolReturn memory) + { + DelegationPool storage pool = __delegationPools[_indexer]; + return + DelegationPoolReturn( + pool.cooldownBlocks, // Blocks to wait before updating parameters + pool.indexingRewardCut, // in PPM + pool.queryFeeCut, // in PPM + pool.updatedAtBlock, // Block when the pool was last updated + pool.tokens, // Total tokens as pool reserves + pool.shares // Total shares minted in the pool + ); + } + + /** + * @notice Getter for rewardsDestination[_indexer]: + * returns the address where the indexer's rewards are sent. + * @param _indexer The indexer address for which to query the rewards destination + * @return The address where the indexer's rewards are sent, zero if none is set in which case rewards are re-staked + */ + function rewardsDestination(address _indexer) external view override returns (address) { + return __rewardsDestination[_indexer]; + } + + /** + * @notice Getter for assetHolders[_maybeAssetHolder]: + * returns true if the address is an asset holder, i.e. an entity that can collect + * query fees into the Staking contract. + * @param _maybeAssetHolder The address that may or may not be an asset holder + * @return True if the address is an asset holder + */ + function assetHolders(address _maybeAssetHolder) external view override returns (bool) { + return __assetHolders[_maybeAssetHolder]; + } + + /** + * @notice Getter for operatorAuth[_indexer][_maybeOperator]: + * returns true if the operator is authorized to operate on behalf of the indexer. + * @param _indexer The indexer address for which to query authorization + * @param _maybeOperator The address that may or may not be an operator + * @return True if the operator is authorized to operate on behalf of the indexer + */ + function operatorAuth(address _indexer, address _maybeOperator) + external + view + override + returns (bool) + { + return __operatorAuth[_indexer][_maybeOperator]; + } + + /** + * @notice Getter for subgraphAllocations[_subgraphDeploymentId]: + * returns the amount of tokens allocated to a subgraph deployment. + * @param _subgraphDeploymentId The subgraph deployment for which to query the allocations + * @return The amount of tokens allocated to the subgraph deployment + */ + function subgraphAllocations(bytes32 _subgraphDeploymentId) + external + view + override + returns (uint256) + { + return __subgraphAllocations[_subgraphDeploymentId]; + } + + /** + * @notice Getter for rebates[_epoch]: + * gets the rebate pool for a particular epoch. + * @param _epoch Epoch for which to query the rebate pool + * @return Rebate pool for the specified epoch, as a Rebates.Pool struct + */ + function rebates(uint256 _epoch) external view override returns (Rebates.Pool memory) { + return __rebates[_epoch]; + } + + /** + * @notice Getter for slashers[_maybeSlasher]: + * returns true if the address is a slasher, i.e. an entity that can slash indexers + * @param _maybeSlasher Address for which to check the slasher role + * @return True if the address is a slasher + */ + function slashers(address _maybeSlasher) external view override returns (bool) { + return __slashers[_maybeSlasher]; + } + + /** + * @notice Getter for minimumIndexerStake: the minimum + * amount of GRT that an indexer needs to stake. + * @return Minimum indexer stake in GRT + */ + function minimumIndexerStake() external view override returns (uint256) { + return __minimumIndexerStake; + } + + /** + * @notice Getter for thawingPeriod: the time in blocks an + * indexer needs to wait to unstake tokens. + * @return Thawing period in blocks + */ + function thawingPeriod() external view override returns (uint32) { + return __thawingPeriod; + } + + /** + * @notice Getter for curationPercentage: the percentage of + * query fees that are distributed to curators. + * @return Curation percentage in parts per million + */ + function curationPercentage() external view override returns (uint32) { + return __curationPercentage; + } + + /** + * @notice Getter for protocolPercentage: the percentage of + * query fees that are burned as protocol fees. + * @return Protocol percentage in parts per million + */ + function protocolPercentage() external view override returns (uint32) { + return __protocolPercentage; + } + + /** + * @notice Getter for channelDisputeEpochs: the time in epochs + * between closing an allocation and the moment it becomes finalized so + * query fees can be claimed. + * @return Channel dispute period in epochs + */ + function channelDisputeEpochs() external view override returns (uint32) { + return __channelDisputeEpochs; + } + + /** + * @notice Getter for maxAllocationEpochs: the maximum time in epochs + * that an allocation can be open before anyone is allowed to close it. This + * also caps the effective allocation when sending the allocation's query fees + * to the rebate pool. + * @return Maximum allocation period in epochs + */ + function maxAllocationEpochs() external view override returns (uint32) { + return __maxAllocationEpochs; + } + + /** + * @notice Getter for alphaNumerator: the numerator of the Cobb-Douglas + * rebate ratio. + * @return Rebate ratio numerator + */ + function alphaNumerator() external view override returns (uint32) { + return __alphaNumerator; + } + + /** + * @notice Getter for alphaDenominator: the denominator of the Cobb-Douglas + * rebate ratio. + * @return Rebate ratio denominator + */ + function alphaDenominator() external view override returns (uint32) { + return __alphaDenominator; + } + + /** + * @notice Getter for stakes[_indexer]: + * gets the stake information for an indexer as a Stakes.Indexer struct. + * @param _indexer Indexer address for which to query the stake information + * @return Stake information for the specified indexer, as a Stakes.Indexer struct + */ + function stakes(address _indexer) external view override returns (Stakes.Indexer memory) { + return __stakes[_indexer]; + } + + /** + * @notice Getter for allocations[_allocationID]: + * gets an allocation's information as an IStakingData.Allocation struct. + * @param _allocationID Allocation ID for which to query the allocation information + * @return The specified allocation, as an IStakingData.Allocation struct + */ + function allocations(address _allocationID) + external + view + override + returns (IStakingData.Allocation memory) + { + return __allocations[_allocationID]; + } + + /** + * @notice Return whether the delegator has delegated to the indexer. + * @param _indexer Address of the indexer where funds have been delegated + * @param _delegator Address of the delegator + * @return True if delegator has tokens delegated to the indexer + */ + function isDelegator(address _indexer, address _delegator) public view override returns (bool) { + return __delegationPools[_indexer].delegators[_delegator].shares > 0; + } + + /** + * @notice Returns amount of delegated tokens ready to be withdrawn after unbonding period. + * @param _delegation Delegation of tokens from delegator to indexer + * @return Amount of tokens to withdraw + */ + function getWithdraweableDelegatedTokens(Delegation memory _delegation) + public + view + override + returns (uint256) + { + // There must be locked tokens and period passed + uint256 currentEpoch = epochManager().currentEpoch(); + if (_delegation.tokensLockedUntil > 0 && currentEpoch >= _delegation.tokensLockedUntil) { + return _delegation.tokensLocked; + } + return 0; + } + + /** + * @dev Internal: Set a delegation tax percentage to burn when delegated funds are deposited. + * @param _percentage Percentage of delegated tokens to burn as delegation tax + */ + function _setDelegationTaxPercentage(uint32 _percentage) private { + // Must be within 0% to 100% (inclusive) + require(_percentage <= MAX_PPM, ">percentage"); + __delegationTaxPercentage = _percentage; + emit ParameterUpdated("delegationTaxPercentage"); + } + + /** + * @dev Internal: Set the delegation ratio. + * If set to 10 it means the indexer can use up to 10x the indexer staked amount + * from their delegated tokens + * @param _delegationRatio Delegation capacity multiplier + */ + function _setDelegationRatio(uint32 _delegationRatio) private { + __delegationRatio = _delegationRatio; + emit ParameterUpdated("delegationRatio"); + } + + /** + * @dev Internal: Set the time in blocks an indexer needs to wait to change delegation parameters. + * @param _blocks Number of blocks to set the delegation parameters cooldown period + */ + function _setDelegationParametersCooldown(uint32 _blocks) private { + __delegationParametersCooldown = _blocks; + emit ParameterUpdated("delegationParametersCooldown"); + } + + /** + * @dev Internal: Set the period for undelegation of stake from indexer. + * @param _delegationUnbondingPeriod Period in epochs to wait for token withdrawals after undelegating + */ + function _setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) private { + require(_delegationUnbondingPeriod > 0, "!delegationUnbondingPeriod"); + __delegationUnbondingPeriod = _delegationUnbondingPeriod; + emit ParameterUpdated("delegationUnbondingPeriod"); + } + + /** + * @dev Delegate tokens to an indexer. + * @param _delegator Address of the delegator + * @param _indexer Address of the indexer to delegate tokens to + * @param _tokens Amount of tokens to delegate + * @return Amount of shares issued of the delegation pool + */ + function _delegate( + address _delegator, + address _indexer, + uint256 _tokens + ) private returns (uint256) { + // Only delegate a non-zero amount of tokens + require(_tokens > 0, "!tokens"); + // Only delegate to non-empty address + require(_indexer != address(0), "!indexer"); + // Only delegate to staked indexer + require(__stakes[_indexer].tokensStaked > 0, "!stake"); + + // Get the delegation pool of the indexer + DelegationPool storage pool = __delegationPools[_indexer]; + Delegation storage delegation = pool.delegators[_delegator]; + + // Collect delegation tax + uint256 delegationTax = _collectTax(graphToken(), _tokens, __delegationTaxPercentage); + uint256 delegatedTokens = _tokens.sub(delegationTax); + + // Calculate shares to issue + uint256 shares = (pool.tokens == 0) + ? delegatedTokens + : delegatedTokens.mul(pool.shares).div(pool.tokens); + require(shares > 0, "!shares"); + + // Update the delegation pool + pool.tokens = pool.tokens.add(delegatedTokens); + pool.shares = pool.shares.add(shares); + + // Update the individual delegation + delegation.shares = delegation.shares.add(shares); + + emit StakeDelegated(_indexer, _delegator, delegatedTokens, shares); + + return shares; + } + + /** + * @dev Undelegate tokens from an indexer. + * @param _delegator Address of the delegator + * @param _indexer Address of the indexer where tokens had been delegated + * @param _shares Amount of shares to return and undelegate tokens + * @return Amount of tokens returned for the shares of the delegation pool + */ + function _undelegate( + address _delegator, + address _indexer, + uint256 _shares + ) private returns (uint256) { + // Can only undelegate a non-zero amount of shares + require(_shares > 0, "!shares"); + + // Get the delegation pool of the indexer + DelegationPool storage pool = __delegationPools[_indexer]; + Delegation storage delegation = pool.delegators[_delegator]; + + // Delegator need to have enough shares in the pool to undelegate + require(delegation.shares >= _shares, "!shares-avail"); + + // Withdraw tokens if available + if (getWithdraweableDelegatedTokens(delegation) > 0) { + _withdrawDelegated(_delegator, _indexer, address(0)); + } + + // Calculate tokens to get in exchange for the shares + uint256 tokens = _shares.mul(pool.tokens).div(pool.shares); + + // Update the delegation pool + pool.tokens = pool.tokens.sub(tokens); + pool.shares = pool.shares.sub(_shares); + + // Update the delegation + delegation.shares = delegation.shares.sub(_shares); + delegation.tokensLocked = delegation.tokensLocked.add(tokens); + delegation.tokensLockedUntil = epochManager().currentEpoch().add( + __delegationUnbondingPeriod + ); + + emit StakeDelegatedLocked( + _indexer, + _delegator, + tokens, + _shares, + delegation.tokensLockedUntil + ); + + return tokens; + } + + /** + * @dev Withdraw delegated tokens once the unbonding period has passed. + * @param _delegator Delegator that is withdrawing tokens + * @param _indexer Withdraw available tokens delegated to indexer + * @param _delegateToIndexer Re-delegate to indexer address if non-zero, withdraw if zero address + * @return Amount of tokens withdrawn or re-delegated + */ + function _withdrawDelegated( + address _delegator, + address _indexer, + address _delegateToIndexer + ) private returns (uint256) { + // Get the delegation pool of the indexer + DelegationPool storage pool = __delegationPools[_indexer]; + Delegation storage delegation = pool.delegators[_delegator]; + + // Validation + uint256 tokensToWithdraw = getWithdraweableDelegatedTokens(delegation); + require(tokensToWithdraw > 0, "!tokens"); + + // Reset lock + delegation.tokensLocked = 0; + delegation.tokensLockedUntil = 0; + + emit StakeDelegatedWithdrawn(_indexer, _delegator, tokensToWithdraw); + + // -- Interactions -- + + if (_delegateToIndexer != address(0)) { + // Re-delegate tokens to a new indexer + _delegate(_delegator, _delegateToIndexer, tokensToWithdraw); + } else { + // Return tokens to the delegator + TokenUtils.pushTokens(graphToken(), _delegator, tokensToWithdraw); + } + + return tokensToWithdraw; + } + + /** + * @dev Collect tax to burn for an amount of tokens. + * @param _graphToken Token to burn + * @param _tokens Total tokens received used to calculate the amount of tax to collect + * @param _percentage Percentage of tokens to burn as tax + * @return Amount of tax charged + */ + function _collectTax( + IGraphToken _graphToken, + uint256 _tokens, + uint256 _percentage + ) private returns (uint256) { + uint256 tax = uint256(_percentage).mul(_tokens).div(MAX_PPM); + TokenUtils.burnTokens(_graphToken, tax); // Burn tax if any + return tax; + } +} diff --git a/contracts/staking/StakingStorage.sol b/contracts/staking/StakingStorage.sol index d629cf8a8..e95356992 100644 --- a/contracts/staking/StakingStorage.sol +++ b/contracts/staking/StakingStorage.sol @@ -2,88 +2,119 @@ pragma solidity ^0.7.6; -import "../governance/Managed.sol"; - -import "./IStakingData.sol"; -import "./libs/Rebates.sol"; -import "./libs/Stakes.sol"; - +import { Managed } from "../governance/Managed.sol"; + +import { IStakingData } from "./IStakingData.sol"; +import { Rebates } from "./libs/Rebates.sol"; +import { Stakes } from "./libs/Stakes.sol"; + +/** + * @title StakingV1Storage + * @notice This contract holds all the storage variables for the Staking contract, version 1 + * @dev Note that we use a double underscore prefix for variable names; this prefix identifies + * variables that used to be public but are now internal, getters can be found on StakingExtension.sol. + */ +// solhint-disable-next-line max-states-count contract StakingV1Storage is Managed { // -- Staking -- - // Minimum amount of tokens an indexer needs to stake - uint256 public minimumIndexerStake; + /// @dev Minimum amount of tokens an indexer needs to stake + uint256 internal __minimumIndexerStake; + + /// @dev Time in blocks to unstake + uint32 internal __thawingPeriod; // in blocks - // Time in blocks to unstake - uint32 public thawingPeriod; // in blocks + /// @dev Percentage of fees going to curators + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + uint32 internal __curationPercentage; - // Percentage of fees going to curators - // Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) - uint32 public curationPercentage; + /// @dev Percentage of fees burned as protocol fee + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + uint32 internal __protocolPercentage; - // Percentage of fees burned as protocol fee - // Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) - uint32 public protocolPercentage; + /// @dev Period for allocation to be finalized + uint32 internal __channelDisputeEpochs; - // Period for allocation to be finalized - uint32 public channelDisputeEpochs; + /// @dev Maximum allocation time + uint32 internal __maxAllocationEpochs; - // Maximum allocation time - uint32 public maxAllocationEpochs; + /// @dev Rebate ratio numerator + uint32 internal __alphaNumerator; - // Rebate ratio - uint32 public alphaNumerator; - uint32 public alphaDenominator; + /// @dev Rebate ratio denominator + uint32 internal __alphaDenominator; - // Indexer stakes : indexer => Stake - mapping(address => Stakes.Indexer) public stakes; + /// @dev Indexer stakes : indexer => Stake + mapping(address => Stakes.Indexer) internal __stakes; - // Allocations : allocationID => Allocation - mapping(address => IStakingData.Allocation) public allocations; + /// @dev Allocations : allocationID => Allocation + mapping(address => IStakingData.Allocation) internal __allocations; - // Subgraph Allocations: subgraphDeploymentID => tokens - mapping(bytes32 => uint256) public subgraphAllocations; + /// @dev Subgraph Allocations: subgraphDeploymentID => tokens + mapping(bytes32 => uint256) internal __subgraphAllocations; - // Rebate pools : epoch => Pool - mapping(uint256 => Rebates.Pool) public rebates; + /// @dev Rebate pools : epoch => Pool + mapping(uint256 => Rebates.Pool) internal __rebates; // -- Slashing -- - // List of addresses allowed to slash stakes - mapping(address => bool) public slashers; + /// @dev List of addresses allowed to slash stakes + mapping(address => bool) internal __slashers; // -- Delegation -- - // Set the delegation capacity multiplier defined by the delegation ratio - // If delegation ratio is 100, and an Indexer has staked 5 GRT, - // then they can use up to 500 GRT from the delegated stake - uint32 public delegationRatio; + /// @dev Set the delegation capacity multiplier defined by the delegation ratio + /// If delegation ratio is 100, and an Indexer has staked 5 GRT, + /// then they can use up to 500 GRT from the delegated stake + uint32 internal __delegationRatio; - // Time in blocks an indexer needs to wait to change delegation parameters - uint32 public delegationParametersCooldown; + /// @dev Time in blocks an indexer needs to wait to change delegation parameters + uint32 internal __delegationParametersCooldown; - // Time in epochs a delegator needs to wait to withdraw delegated stake - uint32 public delegationUnbondingPeriod; // in epochs + /// @dev Time in epochs a delegator needs to wait to withdraw delegated stake + uint32 internal __delegationUnbondingPeriod; // in epochs - // Percentage of tokens to tax a delegation deposit - // Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) - uint32 public delegationTaxPercentage; + /// @dev Percentage of tokens to tax a delegation deposit + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + uint32 internal __delegationTaxPercentage; - // Delegation pools : indexer => DelegationPool - mapping(address => IStakingData.DelegationPool) public delegationPools; + /// @dev Delegation pools : indexer => DelegationPool + mapping(address => IStakingData.DelegationPool) internal __delegationPools; // -- Operators -- - // Operator auth : indexer => operator - mapping(address => mapping(address => bool)) public operatorAuth; + /// @dev Operator auth : indexer => operator => is authorized + mapping(address => mapping(address => bool)) internal __operatorAuth; // -- Asset Holders -- - // Allowed AssetHolders: assetHolder => is allowed - mapping(address => bool) public assetHolders; + /// @dev Allowed AssetHolders that can collect query fees: assetHolder => is allowed + mapping(address => bool) internal __assetHolders; } +/** + * @title StakingV2Storage + * @notice This contract holds all the storage variables for the Staking contract, version 2 + * @dev Note that we use a double underscore prefix for variable names; this prefix identifies + * variables that used to be public but are now internal, getters can be found on StakingExtension.sol. + */ contract StakingV2Storage is StakingV1Storage { - // Destination of accrued rewards : beneficiary => rewards destination - mapping(address => address) public rewardsDestination; + /// @dev Destination of accrued rewards : beneficiary => rewards destination + mapping(address => address) internal __rewardsDestination; +} + +/** + * @title StakingV3Storage + * @notice This contract holds all the storage variables for the base Staking 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. + */ +contract StakingV3Storage is StakingV2Storage { + /// @dev Address of the counterpart Staking contract on L1/L2 + address internal counterpartStakingAddress; + /// @dev Address of the StakingExtension implementation + address internal extensionImpl; + /// @dev Gap to allow adding variables in future upgrades (since L1Staking and L2Staking can have their own storage as well) + uint256[50] private __gap; } diff --git a/contracts/tests/L1GraphTokenLockMigratorMock.sol b/contracts/tests/L1GraphTokenLockMigratorMock.sol new file mode 100644 index 000000000..ba8607f35 --- /dev/null +++ b/contracts/tests/L1GraphTokenLockMigratorMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.7.6; +pragma experimental ABIEncoderV2; + +contract L1GraphTokenLockMigratorMock { + mapping(address => address) public migratedWalletAddress; + + function setMigratedAddress(address _l1Address, address _l2Address) external { + migratedWalletAddress[_l1Address] = _l2Address; + } + + function pullETH(address _l1Wallet, uint256 _amount) external { + require( + migratedWalletAddress[_l1Wallet] != address(0), + "L1GraphTokenLockMigratorMock: unknown L1 wallet" + ); + (bool success, ) = payable(msg.sender).call{ value: _amount }(""); + require(success, "L1GraphTokenLockMigratorMock: ETH pull failed"); + } +} diff --git a/contracts/tests/LegacyGNSMock.sol b/contracts/tests/LegacyGNSMock.sol new file mode 100644 index 000000000..f8878db8a --- /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 (both to NFT-based subgraphs and 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/contracts/tests/RewardsManagerMock.sol b/contracts/tests/RewardsManagerMock.sol deleted file mode 100644 index cbd57b2d3..000000000 --- a/contracts/tests/RewardsManagerMock.sol +++ /dev/null @@ -1,68 +0,0 @@ -pragma solidity ^0.7.6; -pragma abicoder v2; - -// Mock contract used for testing rewards -contract RewardsManagerMock { - /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x - */ - function pow( - uint256 x, - uint256 n, - uint256 base - ) public pure returns (uint256 z) { - assembly { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } - } - } -} diff --git a/e2e/deployment/config/controller.test.ts b/e2e/deployment/config/controller.test.ts index 647cb19f5..5bc4e6c04 100644 --- a/e2e/deployment/config/controller.test.ts +++ b/e2e/deployment/config/controller.test.ts @@ -13,7 +13,7 @@ describe('Controller configuration', () => { 'DisputeManager', 'EpochManager', 'RewardsManager', - 'Staking', + 'L1Staking', 'GraphToken', 'L1GraphTokenGateway', ] @@ -24,7 +24,7 @@ describe('Controller configuration', () => { 'DisputeManager', 'EpochManager', 'RewardsManager', - 'Staking', + 'L2Staking', 'L2GraphToken', 'L2GraphTokenGateway', ] 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/l1/rewardsManager.test.ts b/e2e/deployment/config/l1/rewardsManager.test.ts index b3ee0ffd7..d46f15397 100644 --- a/e2e/deployment/config/l1/rewardsManager.test.ts +++ b/e2e/deployment/config/l1/rewardsManager.test.ts @@ -10,8 +10,8 @@ describe('[L1] RewardsManager configuration', () => { if (GraphChain.isL2(graph.chainId)) this.skip() }) - it('issuanceRate should match "issuanceRate" in the config file', async function () { - const value = await RewardsManager.issuanceRate() - expect(value).eq('1000000011247641700') // hardcoded as it's set with a function call rather than init parameter + it('issuancePerBlock should match "issuancePerBlock" in the config file', async function () { + const value = await RewardsManager.issuancePerBlock() + expect(value).eq('114155251141552511415') // hardcoded as it's set with a function call rather than init parameter }) }) 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/deployment/config/l2/l2GraphToken.test.ts b/e2e/deployment/config/l2/l2GraphToken.test.ts index a4ccdaec8..6b5f03c92 100644 --- a/e2e/deployment/config/l2/l2GraphToken.test.ts +++ b/e2e/deployment/config/l2/l2GraphToken.test.ts @@ -46,9 +46,9 @@ describe('[L2] L2GraphToken', () => { await expect(tx).revertedWith('Only Governor can call') }) - it('RewardsManager should not be minter (for now)', async function () { + it('RewardsManager should be minter', async function () { const rewardsMgrIsMinter = await L2GraphToken.isMinter(RewardsManager.address) - expect(rewardsMgrIsMinter).eq(false) + expect(rewardsMgrIsMinter).eq(true) }) }) }) diff --git a/e2e/deployment/config/l2/rewardsManager.test.ts b/e2e/deployment/config/l2/rewardsManager.test.ts index a5e2e7cbf..5329abec8 100644 --- a/e2e/deployment/config/l2/rewardsManager.test.ts +++ b/e2e/deployment/config/l2/rewardsManager.test.ts @@ -10,8 +10,8 @@ describe('[L2] RewardsManager configuration', () => { if (GraphChain.isL1(graph.chainId)) this.skip() }) - it('issuanceRate should be zero', async function () { - const value = await RewardsManager.issuanceRate() - expect(value).eq('0') + it('issuancePerBlock should be zero', async function () { + const value = await RewardsManager.issuancePerBlock() + expect(value).eq('0') // hardcoded as it's set with a function call rather than init parameter }) }) diff --git a/e2e/deployment/config/staking.test.ts b/e2e/deployment/config/staking.test.ts index e2b1fe5e9..b5eb2c400 100644 --- a/e2e/deployment/config/staking.test.ts +++ b/e2e/deployment/config/staking.test.ts @@ -1,12 +1,20 @@ import { expect } from 'chai' import hre from 'hardhat' import { getItemValue } from '../../../cli/config' +import GraphChain from '../../../gre/helpers/chain' describe('Staking configuration', () => { const { graphConfig, contracts: { Staking, Controller, DisputeManager, AllocationExchange }, + chainId, } = hre.graph() + let contractName: string + if (GraphChain.isL2(chainId)) { + contractName = 'L2Staking' + } else { + contractName = 'L1Staking' + } it('should be controlled by Controller', async function () { const controller = await Staking.controller() @@ -25,61 +33,73 @@ describe('Staking configuration', () => { it('minimumIndexerStake should match "minimumIndexerStake" in the config file', async function () { const value = await Staking.minimumIndexerStake() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/minimumIndexerStake') + const expected = getItemValue(graphConfig, `contracts/${contractName}/init/minimumIndexerStake`) expect(value).eq(expected) }) it('thawingPeriod should match "thawingPeriod" in the config file', async function () { const value = await Staking.thawingPeriod() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/thawingPeriod') + const expected = getItemValue(graphConfig, `contracts/${contractName}/init/thawingPeriod`) expect(value).eq(expected) }) it('protocolPercentage should match "protocolPercentage" in the config file', async function () { const value = await Staking.protocolPercentage() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/protocolPercentage') + const expected = getItemValue(graphConfig, `contracts/${contractName}/init/protocolPercentage`) expect(value).eq(expected) }) it('curationPercentage should match "curationPercentage" in the config file', async function () { const value = await Staking.curationPercentage() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/curationPercentage') + const expected = getItemValue(graphConfig, `contracts/${contractName}/init/curationPercentage`) expect(value).eq(expected) }) it('channelDisputeEpochs should match "channelDisputeEpochs" in the config file', async function () { const value = await Staking.channelDisputeEpochs() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/channelDisputeEpochs') + const expected = getItemValue( + graphConfig, + `contracts/${contractName}/init/channelDisputeEpochs`, + ) expect(value).eq(expected) }) it('maxAllocationEpochs should match "maxAllocationEpochs" in the config file', async function () { const value = await Staking.maxAllocationEpochs() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/maxAllocationEpochs') + const expected = getItemValue(graphConfig, `contracts/${contractName}/init/maxAllocationEpochs`) expect(value).eq(expected) }) it('delegationUnbondingPeriod should match "delegationUnbondingPeriod" in the config file', async function () { const value = await Staking.delegationUnbondingPeriod() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/delegationUnbondingPeriod') + const expected = getItemValue( + graphConfig, + `contracts/${contractName}/init/delegationUnbondingPeriod`, + ) expect(value).eq(expected) }) it('delegationRatio should match "delegationRatio" in the config file', async function () { const value = await Staking.delegationRatio() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/delegationRatio') + const expected = getItemValue(graphConfig, `contracts/${contractName}/init/delegationRatio`) expect(value).eq(expected) }) it('alphaNumerator should match "rebateAlphaNumerator" in the config file', async function () { const value = await Staking.alphaNumerator() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/rebateAlphaNumerator') + const expected = getItemValue( + graphConfig, + `contracts/${contractName}/init/rebateAlphaNumerator`, + ) expect(value).eq(expected) }) it('alphaDenominator should match "rebateAlphaDenominator" in the config file', async function () { const value = await Staking.alphaDenominator() - const expected = getItemValue(graphConfig, 'contracts/Staking/init/rebateAlphaDenominator') + const expected = getItemValue( + graphConfig, + `contracts/${contractName}/init/rebateAlphaDenominator`, + ) 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/gre/config.ts b/gre/config.ts index c642f068d..90c8c6d76 100644 --- a/gre/config.ts +++ b/gre/config.ts @@ -136,8 +136,8 @@ export function getGraphConfigPaths( let l1GraphConfigPath = opts.l1GraphConfig ?? (isHHL1 ? opts.graphConfig : undefined) ?? - l1Network?.graphConfig ?? - hre.config.graph.l1GraphConfig + hre.config.graph.l1GraphConfig ?? + l1Network?.graphConfig logDebug(`> L1 graph config`) logDebug(`1) opts.l1GraphConfig: ${opts.l1GraphConfig}`) @@ -156,8 +156,8 @@ export function getGraphConfigPaths( let l2GraphConfigPath = opts.l2GraphConfig ?? (!isHHL1 ? opts.graphConfig : undefined) ?? - l2Network?.graphConfig ?? - hre.config.graph.l2GraphConfig + hre.config.graph.l2GraphConfig ?? + l2Network?.graphConfig logDebug(`> L2 graph config`) logDebug(`1) opts.l2GraphConfig: ${opts.l2GraphConfig}`) diff --git a/package.json b/package.json index e6ae3b97f..84901f977 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@graphprotocol/contracts", - "version": "2.3.0", + "version": "v3.0.0-scratch3r1", "description": "Contracts for the Graph Protocol", "directories": { "test": "test" diff --git a/tasks/deployment/config.ts b/tasks/deployment/config.ts index 123afed53..f3992e652 100644 --- a/tasks/deployment/config.ts +++ b/tasks/deployment/config.ts @@ -67,7 +67,7 @@ const staking: Contract = { const rewardsManager: Contract = { name: 'RewardsManager', - initParams: [{ name: 'issuanceRate', type: 'BigNumber' }], + initParams: [{ name: 'issuancePerBlock', type: 'BigNumber' }], } const contractList: Contract[] = [epochManager, curation, disputeManager, staking, rewardsManager] 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/disputes/poi.test.ts b/test/disputes/poi.test.ts index cded4d91e..4bc26e4f9 100644 --- a/test/disputes/poi.test.ts +++ b/test/disputes/poi.test.ts @@ -4,7 +4,7 @@ import { utils } from 'ethers' import { DisputeManager } from '../../build/types/DisputeManager' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { NetworkFixture } from '../lib/fixtures' import { @@ -35,7 +35,7 @@ describe('DisputeManager:POI', async () => { let disputeManager: DisputeManager let epochManager: EpochManager let grt: GraphToken - let staking: Staking + let staking: IStaking // Derive some channel keys for each indexer used to sign attestations const indexerChannelKey = deriveChannelKey() diff --git a/test/disputes/query.test.ts b/test/disputes/query.test.ts index b7548e842..595b502cb 100644 --- a/test/disputes/query.test.ts +++ b/test/disputes/query.test.ts @@ -5,7 +5,7 @@ import { createAttestation, Receipt } from '@graphprotocol/common-ts' import { DisputeManager } from '../../build/types/DisputeManager' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { NetworkFixture } from '../lib/fixtures' import { @@ -42,7 +42,7 @@ describe('DisputeManager:Query', async () => { let disputeManager: DisputeManager let epochManager: EpochManager let grt: GraphToken - let staking: Staking + let staking: IStaking // Derive some channel keys for each indexer used to sign attestations const indexer1ChannelKey = deriveChannelKey() diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index 76b00ee6b..86a4326a5 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -16,6 +16,7 @@ import { toGRT, Account, applyL1ToL2Alias, + advanceBlocks, provider, } from '../lib/testHelpers' import { BridgeEscrow } from '../../build/types/BridgeEscrow' @@ -30,6 +31,8 @@ describe('L1GraphTokenGateway', () => { let mockL2GRT: Account let mockL2Gateway: Account let pauseGuardian: Account + let mockL2GNS: Account + let mockL2Staking: Account let fixture: NetworkFixture let grt: GraphToken @@ -63,8 +66,17 @@ describe('L1GraphTokenGateway', () => { ) before(async function () { - ;[governor, tokenSender, l2Receiver, mockRouter, mockL2GRT, mockL2Gateway, pauseGuardian] = - await getAccounts() + ;[ + governor, + tokenSender, + l2Receiver, + mockRouter, + mockL2GRT, + mockL2Gateway, + pauseGuardian, + mockL2GNS, + mockL2Staking, + ] = await getAccounts() // Dummy code on the mock router so that it appears as a contract await provider().send('hardhat_setCode', [mockRouter.address, '0x1234']) @@ -291,6 +303,8 @@ describe('L1GraphTokenGateway', () => { mockRouter.address, mockL2GRT.address, mockL2Gateway.address, + mockL2GNS.address, + mockL2Staking.address, ) let tx = l1GraphTokenGateway.connect(governor.signer).setPaused(true) await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) @@ -322,6 +336,8 @@ describe('L1GraphTokenGateway', () => { mockRouter.address, mockL2GRT.address, mockL2Gateway.address, + mockL2GNS.address, + mockL2Staking.address, ) await l1GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) let tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) @@ -416,8 +432,8 @@ describe('L1GraphTokenGateway', () => { ) const escrowBalance = await grt.balanceOf(bridgeEscrow.address) const senderBalance = await grt.balanceOf(tokenSender.address) - await expect(escrowBalance).eq(toGRT('10')) - await expect(senderBalance).eq(toGRT('990')) + expect(escrowBalance).eq(toGRT('10')) + expect(senderBalance).eq(toGRT('990')) } before(async function () { await fixture.configureL1Bridge( @@ -427,9 +443,188 @@ describe('L1GraphTokenGateway', () => { mockRouter.address, mockL2GRT.address, mockL2Gateway.address, + mockL2GNS.address, + mockL2Staking.address, ) }) + describe('updateL2MintAllowance', function () { + it('rejects calls that are not from the governor', async function () { + const tx = l1GraphTokenGateway + .connect(pauseGuardian.address) + .updateL2MintAllowance(toGRT('1'), await latestBlock()) + await expect(tx).revertedWith('Only Controller governor') + }) + it('does not allow using a future or current block number', async function () { + const issuancePerBlock = toGRT('120') + let issuanceUpdatedAtBlock = (await latestBlock()).add(2) + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx1).revertedWith('BLOCK_MUST_BE_PAST') + issuanceUpdatedAtBlock = (await latestBlock()).add(1) // This will be block.number in our next tx + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx2).revertedWith('BLOCK_MUST_BE_PAST') + issuanceUpdatedAtBlock = await latestBlock() // This will be block.number-1 in our next tx + const tx3 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx3) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + }) + it('does not allow using a block number lower than or equal to the previous one', async function () { + const issuancePerBlock = toGRT('120') + const issuanceUpdatedAtBlock = await latestBlock() + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx1) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx2).revertedWith('BLOCK_MUST_BE_INCREMENTING') + const tx3 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock.sub(1)) + await expect(tx3).revertedWith('BLOCK_MUST_BE_INCREMENTING') + const tx4 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock.add(1)) + await expect(tx4) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(issuancePerBlock, issuancePerBlock, issuanceUpdatedAtBlock.add(1)) + }) + it('updates the snapshot and issuance to follow a new linear function, accumulating up to the specified block', async function () { + const issuancePerBlock = toGRT('120') + const issuanceUpdatedAtBlock = (await latestBlock()).sub(2) + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx1) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + // Now the mint allowance should be issuancePerBlock * 3 + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(issuancePerBlock.mul(3)) + expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(0) + expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(issuancePerBlock) + expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq( + issuanceUpdatedAtBlock, + ) + + await advanceBlocks(10) + + const newIssuancePerBlock = toGRT('200') + const newIssuanceUpdatedAtBlock = (await latestBlock()).sub(1) + + const expectedAccumulatedSnapshot = issuancePerBlock.mul( + newIssuanceUpdatedAtBlock.sub(issuanceUpdatedAtBlock), + ) + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(newIssuancePerBlock, newIssuanceUpdatedAtBlock) + await expect(tx2) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(expectedAccumulatedSnapshot, newIssuancePerBlock, newIssuanceUpdatedAtBlock) + + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(expectedAccumulatedSnapshot.add(newIssuancePerBlock.mul(2))) + expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq( + expectedAccumulatedSnapshot, + ) + expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(newIssuancePerBlock) + expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq( + newIssuanceUpdatedAtBlock, + ) + }) + }) + describe('setL2MintAllowanceParametersManual', function () { + it('rejects calls that are not from the governor', async function () { + const tx = l1GraphTokenGateway + .connect(pauseGuardian.address) + .setL2MintAllowanceParametersManual(toGRT('0'), toGRT('1'), await latestBlock()) + await expect(tx).revertedWith('Only Controller governor') + }) + it('does not allow using a future or current block number', async function () { + const issuancePerBlock = toGRT('120') + let issuanceUpdatedAtBlock = (await latestBlock()).add(2) + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx1).revertedWith('BLOCK_MUST_BE_PAST') + issuanceUpdatedAtBlock = (await latestBlock()).add(1) // This will be block.number in our next tx + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx2).revertedWith('BLOCK_MUST_BE_PAST') + issuanceUpdatedAtBlock = await latestBlock() // This will be block.number-1 in our next tx + const tx3 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx3) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + }) + it('updates the snapshot and issuance to follow a new linear function, manually setting the snapshot value', async function () { + const issuancePerBlock = toGRT('120') + const issuanceUpdatedAtBlock = (await latestBlock()).sub(2) + const snapshotValue = toGRT('10') + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual( + snapshotValue, + issuancePerBlock, + issuanceUpdatedAtBlock, + ) + await expect(tx1) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(snapshotValue, issuancePerBlock, issuanceUpdatedAtBlock) + // Now the mint allowance should be 10 + issuancePerBlock * 3 + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(snapshotValue.add(issuancePerBlock.mul(3))) + expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(snapshotValue) + expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(issuancePerBlock) + expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq( + issuanceUpdatedAtBlock, + ) + + await advanceBlocks(10) + + const newIssuancePerBlock = toGRT('200') + const newIssuanceUpdatedAtBlock = (await latestBlock()).sub(1) + const newSnapshotValue = toGRT('10') + + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual( + newSnapshotValue, + newIssuancePerBlock, + newIssuanceUpdatedAtBlock, + ) + await expect(tx2) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(newSnapshotValue, newIssuancePerBlock, newIssuanceUpdatedAtBlock) + + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(newSnapshotValue.add(newIssuancePerBlock.mul(2))) + expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq( + newSnapshotValue, + ) + expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(newIssuancePerBlock) + expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq( + newIssuanceUpdatedAtBlock, + ) + }) + }) describe('calculateL2TokenAddress', function () { it('returns the L2 token address', async function () { expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(mockL2GRT.address) @@ -577,7 +772,7 @@ describe('L1GraphTokenGateway', () => { ) await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') }) - it('reverts if the gateway does not have tokens', async function () { + it('reverts if the gateway does not have tokens or allowance', async function () { // This scenario should never really happen, but we still // test that the gateway reverts in this case const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( @@ -607,7 +802,7 @@ describe('L1GraphTokenGateway', () => { toBN('0'), encodedCalldata, ) - await expect(tx).revertedWith('BRIDGE_OUT_OF_FUNDS') + await expect(tx).revertedWith('INVALID_L2_MINT_AMOUNT') }) it('reverts if the gateway is revoked from escrow', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) @@ -680,8 +875,105 @@ describe('L1GraphTokenGateway', () => { .withArgs(grt.address, l2Receiver.address, tokenSender.address, toBN('0'), toGRT('8')) const escrowBalance = await grt.balanceOf(bridgeEscrow.address) const senderBalance = await grt.balanceOf(tokenSender.address) - await expect(escrowBalance).eq(toGRT('2')) - await expect(senderBalance).eq(toGRT('998')) + expect(escrowBalance).eq(toGRT('2')) + expect(senderBalance).eq(toGRT('998')) + }) + it('mints tokens up to the L2 mint allowance', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) + + // Start accruing L2 mint allowance at 2 GRT per block + await l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(toGRT('2'), await latestBlock()) + await advanceBlocks(2) + // Now it's been three blocks since the lastL2MintAllowanceUpdateBlock, so + // there should be 8 GRT allowed to be minted from L2 in the next block. + + // At this point, the gateway holds 10 GRT in escrow + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('18'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'WithdrawalFinalized') + .withArgs(grt.address, l2Receiver.address, tokenSender.address, toBN('0'), toGRT('18')) + .emit(l1GraphTokenGateway, 'TokensMintedFromL2') + .withArgs(toGRT('8')) + expect(await l1GraphTokenGateway.totalMintedFromL2()).to.eq(toGRT('8')) + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(toGRT('8')) + + const escrowBalance = await grt.balanceOf(bridgeEscrow.address) + const senderBalance = await grt.balanceOf(tokenSender.address) + expect(escrowBalance).eq(toGRT('0')) + expect(senderBalance).eq(toGRT('1008')) + }) + it('reverts if the amount to mint is over the allowance', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) + + // Start accruing L2 mint allowance at 2 GRT per block + await l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(toGRT('2'), await latestBlock()) + await advanceBlocks(2) + // Now it's been three blocks since the lastL2MintAllowanceUpdateBlock, so + // there should be 8 GRT allowed to be minted from L2 in the next block. + + // At this point, the gateway holds 10 GRT in escrow + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('18.001'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx).revertedWith('INVALID_L2_MINT_AMOUNT') }) }) }) diff --git a/test/gns.test.ts b/test/gns.test.ts index 1ee46002a..c40c57013 100644 --- a/test/gns.test.ts +++ b/test/gns.test.ts @@ -1,61 +1,80 @@ 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 mockL2Staking: 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 +85,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 +139,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 +197,79 @@ 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, + mockL2Staking, + ] = 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, + mockL2Staking.address, + ) }) beforeEach(async function () { @@ -550,6 +301,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 +360,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 +382,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 +412,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 +478,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 +492,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 +504,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 +579,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 +598,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 +613,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 +658,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 +673,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 +697,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 +712,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 +737,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 +750,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 +778,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 +793,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 +813,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 +838,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 +875,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 +899,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 +911,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 +923,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 +938,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 +948,709 @@ 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 migration 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 ( + beforeMigrationCallback?: (subgraphID: string) => Promise, + ): Promise { + const subgraph0 = await publishAndCurateOnSubgraph() + + if (beforeMigrationCallback != null) { + await beforeMigrationCallback(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 migrated = await gns.subgraphMigratedToL2(subgraph0.id) + expect(migrated).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 migrated = await legacyGNSMock.subgraphMigratedToL2(subgraphID) + expect(migrated).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('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 migrating, 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('!MIGRATED') + }) + + 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('!MIGRATED') + }) + 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 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/governance/pausing.test.ts b/test/governance/pausing.test.ts index 71aa3dc3d..372509c67 100644 --- a/test/governance/pausing.test.ts +++ b/test/governance/pausing.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import { constants } from 'ethers' import { Controller } from '../../build/types/Controller' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { getAccounts, Account, toGRT } from '../lib/testHelpers' import { NetworkFixture } from '../lib/fixtures' @@ -14,7 +14,7 @@ describe('Pausing', () => { let fixture: NetworkFixture - let staking: Staking + let staking: IStaking let controller: Controller const setPartialPause = async (account: Account, setValue: boolean) => { diff --git a/test/l2/l2Curation.test.ts b/test/l2/l2Curation.test.ts new file mode 100644 index 000000000..b98afe462 --- /dev/null +++ b/test/l2/l2Curation.test.ts @@ -0,0 +1,783 @@ +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.toFixed(12) + +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.minimumCurationDeposit) + + // 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('10.0') + const subgraphDeploymentID = randomHexBytes() + const curatorTokens = toGRT('1000000000') + const tokensToDeposit = toGRT('1000') + const tokensToCollect = toGRT('2000') + + async function calcBondingCurve( + supply: BigNumber, + reserveBalance: BigNumber, + reserveRatio: number, + 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 defaultReserveRatio = await curation.defaultReserveRatio() + const minSupply = toGRT('1') + return ( + (await calcBondingCurve( + minSupply, + minDeposit, + defaultReserveRatio, + depositAmount.sub(minDeposit), + )) + toFloat(minSupply) + ) + } + // Calculate bonding curve in the test + return ( + toFloat(supply) * + ((1 + toFloat(depositAmount) / toFloat(reserveBalance)) ** (reserveRatio / 1000000) - 1) + ) + } + + 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('1') + 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 () { + 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 = toGRT('1') + 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 () { + 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 = toGRT('1') + 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('100') + 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 () { + // 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 calcBondingCurve( + await curation.getCurationPoolSignal(subgraphDeploymentID), + await curation.getCurationPoolTokens(subgraphDeploymentID), + await curation.defaultReserveRatio(), + tokensToDeposit, + ) + // SIGNAL_PER_MINIMUM_DEPOSIT should always give the same ratio + expect(tokensToDeposit.div(toGRT(expectedSignal))).eq(100) + + 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(expectedSignal)).eq(toRound(toFloat(signal))) + } + }) + + it('should mint when using the edge case of a 1:1 linear function', async function () { + this.timeout(60000) // increase timeout for test runner + + // Setup edge case like linear function: 1 GRT = 1 GCS + 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 less GCS due to bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + + // 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) // we compare 1:1 ratio + } + }) + }) +}) diff --git a/test/l2/l2GNS.test.ts b/test/l2/l2GNS.test.ts new file mode 100644 index 000000000..5f8064e6f --- /dev/null +++ b/test/l2/l2GNS.test.ts @@ -0,0 +1,783 @@ +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 mockL1Staking: 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 migrateMockSubgraphFromL1 = 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) + + await gns + .connect(me.signer) + .finishSubgraphMigrationFromL1( + l1SubgraphId, + newSubgraph0.subgraphDeploymentID, + subgraphMetadata, + versionMetadata, + ) + } + + before(async function () { + newSubgraph0 = buildSubgraph() + ;[me, other, governor, mockRouter, mockL1GRT, mockL1Gateway, mockL1GNS, mockL1Staking] = + 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, + mockL1Staking.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, + ) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1GNS.address, gns.address, curatedTokens) + await expect(tx) + .emit(gns, 'SubgraphReceivedFromL1') + .withArgs(l1SubgraphId, me.address, curatedTokens) + + const migrationData = await gns.subgraphL2MigrationData(l1SubgraphId) + const subgraphData = await gns.subgraphs(l1SubgraphId) + + expect(migrationData.tokens).eq(curatedTokens) + expect(migrationData.l2Done).eq(false) + expect(migrationData.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(l1SubgraphId)).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, + ) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1GNS.address, gns.address, curatedTokens) + await expect(tx) + .emit(gns, 'SubgraphReceivedFromL1') + .withArgs(l1SubgraphId, me.address, curatedTokens) + + const migrationData = await gns.subgraphL2MigrationData(l1SubgraphId) + const subgraphData = await gns.subgraphs(l1SubgraphId) + + expect(migrationData.tokens).eq(curatedTokens) + expect(migrationData.l2Done).eq(false) + expect(migrationData.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(l1SubgraphId)).eq(me.address) + + expect(l2Subgraph.id).not.eq(l1SubgraphId) + 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 migration from L1', function () { + it('publishes the migrated 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 tx = gns + .connect(me.signer) + .finishSubgraphMigrationFromL1( + l1SubgraphId, + newSubgraph0.subgraphDeploymentID, + subgraphMetadata, + versionMetadata, + ) + await expect(tx) + .emit(gns, 'SubgraphPublished') + .withArgs(l1SubgraphId, newSubgraph0.subgraphDeploymentID, DEFAULT_RESERVE_RATIO) + await expect(tx).emit(gns, 'SubgraphMetadataUpdated').withArgs(l1SubgraphId, subgraphMetadata) + await expect(tx) + .emit(gns, 'SubgraphUpgraded') + .withArgs(l1SubgraphId, expectedSignal, curatedTokens, newSubgraph0.subgraphDeploymentID) + await expect(tx) + .emit(gns, 'SubgraphVersionUpdated') + .withArgs(l1SubgraphId, newSubgraph0.subgraphDeploymentID, versionMetadata) + await expect(tx).emit(gns, 'SubgraphMigrationFinalized').withArgs(l1SubgraphId) + + const subgraphAfter = await gns.subgraphs(l1SubgraphId) + const migrationDataAfter = await gns.subgraphL2MigrationData(l1SubgraphId) + expect(subgraphAfter.vSignal).eq(expectedSignal) + expect(migrationDataAfter.l2Done).eq(true) + expect(subgraphAfter.disabled).eq(false) + expect(subgraphAfter.subgraphDeploymentID).eq(newSubgraph0.subgraphDeploymentID) + const expectedNSignal = await gns.vSignalToNSignal(l1SubgraphId, expectedSignal) + expect(await gns.getCuratorSignal(l1SubgraphId, 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 tx = gns + .connect(other.signer) + .finishSubgraphMigrationFromL1( + l1SubgraphId, + 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 tx = gns + .connect(me.signer) + .finishSubgraphMigrationFromL1( + l1SubgraphId, + newSubgraph0.subgraphDeploymentID, + metadata, + metadata, + ) + await expect(tx).revertedWith('ERC721: owner query for nonexistent token') + }) + it('rejects calls for a subgraph that was not migrated', async function () { + const l2Subgraph = await publishNewSubgraph(me, newSubgraph0, gns) + const metadata = randomHexBytes() + + const tx = gns + .connect(me.signer) + .finishSubgraphMigrationFromL1( + 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) + + // 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) + .finishSubgraphMigrationFromL1( + l1SubgraphId, + newSubgraph0.subgraphDeploymentID, + subgraphMetadata, + versionMetadata, + ) + await expect(tx) + .emit(gns, 'SubgraphPublished') + .withArgs(l1SubgraphId, newSubgraph0.subgraphDeploymentID, DEFAULT_RESERVE_RATIO) + await expect(tx).emit(gns, 'SubgraphMetadataUpdated').withArgs(l1SubgraphId, subgraphMetadata) + await expect(tx) + .emit(gns, 'SubgraphUpgraded') + .withArgs(l1SubgraphId, expectedSignal, curatedTokens, newSubgraph0.subgraphDeploymentID) + await expect(tx) + .emit(gns, 'SubgraphVersionUpdated') + .withArgs(l1SubgraphId, newSubgraph0.subgraphDeploymentID, versionMetadata) + await expect(tx).emit(gns, 'SubgraphMigrationFinalized').withArgs(l1SubgraphId) + + const subgraphAfter = await gns.subgraphs(l1SubgraphId) + const migrationDataAfter = await gns.subgraphL2MigrationData(l1SubgraphId) + expect(subgraphAfter.vSignal).eq(expectedSignal) + expect(migrationDataAfter.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 tx = gns + .connect(me.signer) + .finishSubgraphMigrationFromL1(l1SubgraphId, HashZero, metadata, metadata) + await expect(tx).revertedWith('GNS: deploymentID != 0') + }) + it('rejects calls if the subgraph migration 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) + + await gns + .connect(me.signer) + .finishSubgraphMigrationFromL1( + l1SubgraphId, + newSubgraph0.subgraphDeploymentID, + metadata, + metadata, + ) + + const tx = gns + .connect(me.signer) + .finishSubgraphMigrationFromL1( + l1SubgraphId, + 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 migrateMockSubgraphFromL1( + l1SubgraphId, + curatedTokens, + subgraphMetadata, + versionMetadata, + ) + + const l2OwnerSignalBefore = await gns.getCuratorSignal(l1SubgraphId, 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, other.address, newCuratorTokens) + + const l2NewCuratorSignal = await gns.getCuratorSignal(l1SubgraphId, other.address) + const expectedNewCuratorSignal = await gns.vSignalToNSignal( + l1SubgraphId, + await curation.tokensToSignalNoTax(newSubgraph0.subgraphDeploymentID, newCuratorTokens), + ) + const l2OwnerSignalAfter = await gns.getCuratorSignal(l1SubgraphId, 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 migrateMockSubgraphFromL1( + l1SubgraphId, + curatedTokens, + subgraphMetadata, + versionMetadata, + ) + + 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(l1SubgraphId, toGRT('10'), toBN(0)) + const prevSignal = await gns.getCuratorSignal(l1SubgraphId, 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, other.address, newCuratorTokens) + + const expectedNewCuratorSignal = await gns.vSignalToNSignal( + l1SubgraphId, + await curation.tokensToSignalNoTax(newSubgraph0.subgraphDeploymentID, newCuratorTokens), + ) + const l2CuratorBalance = await gns.getCuratorSignal(l1SubgraphId, 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 migrateMockSubgraphFromL1( + 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 migrateMockSubgraphFromL1( + 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 migration 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 migration 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 migration, 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 migrateMockSubgraphFromL1( + l1SubgraphId, + curatedTokens, + subgraphMetadata, + versionMetadata, + ) + + await gns.connect(me.signer).deprecateSubgraph(l1SubgraphId) + + // SG was migrated, 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') + }) + }) +}) diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index 236817afd..1a3078938 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -28,6 +28,8 @@ describe('L2GraphTokenGateway', () => { let mockL1GRT: Account let mockL1Gateway: Account let pauseGuardian: Account + let mockL1GNS: Account + let mockL1Staking: Account let fixture: NetworkFixture let arbSysMock: FakeContract @@ -55,6 +57,8 @@ describe('L2GraphTokenGateway', () => { mockL1Gateway, l2Receiver, pauseGuardian, + mockL1GNS, + mockL1Staking, ] = await getAccounts() fixture = new NetworkFixture() @@ -188,6 +192,8 @@ describe('L2GraphTokenGateway', () => { mockRouter.address, mockL1GRT.address, mockL1Gateway.address, + mockL1GNS.address, + mockL1Staking.address, ) let tx = l2GraphTokenGateway.connect(governor.signer).setPaused(true) await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) @@ -218,6 +224,8 @@ describe('L2GraphTokenGateway', () => { mockRouter.address, mockL1GRT.address, mockL1Gateway.address, + mockL1GNS.address, + mockL1Staking.address, ) await l2GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) let tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) @@ -280,6 +288,8 @@ describe('L2GraphTokenGateway', () => { mockRouter.address, mockL1GRT.address, mockL1Gateway.address, + mockL1GNS.address, + mockL1Staking.address, ) }) diff --git a/test/l2/l2Staking.test.ts b/test/l2/l2Staking.test.ts new file mode 100644 index 000000000..2d967bfcb --- /dev/null +++ b/test/l2/l2Staking.test.ts @@ -0,0 +1,291 @@ +import { expect } from 'chai' +import { ethers, ContractTransaction, BigNumber } from 'ethers' +import { defaultAbiCoder, parseEther } from 'ethers/lib/utils' + +import { + getAccounts, + Account, + toGRT, + getL2SignerFromL1, + setAccountBalance, + latestBlock, + advanceBlocks, +} from '../lib/testHelpers' +import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' +import { toBN } from '../lib/testHelpers' + +import { IL2Staking } from '../../build/types/IL2Staking' +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { GraphToken } from '../../build/types/GraphToken' + +const { AddressZero } = ethers.constants + +describe('L2Staking', () => { + let me: Account + let other: Account + let another: Account + let governor: Account + let mockRouter: Account + let mockL1GRT: Account + let mockL1Gateway: Account + let mockL1GNS: Account + let mockL1Staking: Account + let fixture: NetworkFixture + + let fixtureContracts: L2FixtureContracts + let l2GraphTokenGateway: L2GraphTokenGateway + let staking: IL2Staking + let grt: GraphToken + + const tokens10k = toGRT('10000') + const tokens100k = toGRT('100000') + const tokens1m = toGRT('1000000') + + 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 + } + + before(async function () { + ;[ + me, + other, + another, + governor, + mockRouter, + mockL1GRT, + mockL1Gateway, + mockL1GNS, + mockL1Staking, + ] = await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.loadL2(governor.signer) + ;({ l2GraphTokenGateway, staking, grt } = fixtureContracts) + + await grt.connect(governor.signer).mint(me.address, tokens1m) + await grt.connect(me.signer).approve(staking.address, tokens1m) + await grt.connect(governor.signer).mint(other.address, tokens1m) + await grt.connect(other.signer).approve(staking.address, tokens1m) + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + mockL1GNS.address, + mockL1Staking.address, + ) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('receive()', function () { + it('should not allow receiving ETH', async function () { + const tx = me.signer.sendTransaction({ + to: staking.address, + value: parseEther('1'), + }) + await expect(tx).revertedWith('RECEIVE_ETH_NOT_ALLOWED') + }) + }) + describe('receiving indexer stake from L1 (onTokenTransfer)', function () { + it('cannot be called by someone other than the L2GraphTokenGateway', async function () { + const functionData = defaultAbiCoder.encode(['tuple(address)'], [[me.address]]) + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(0), functionData], // code = 1 means RECEIVE_INDEXER_CODE + ) + const tx = staking + .connect(me.signer) + .onTokenTransfer(mockL1GNS.address, tokens100k, callhookData) + await expect(tx).revertedWith('ONLY_GATEWAY') + }) + it('rejects calls if the L1 sender is not the L1Staking', async function () { + const functionData = defaultAbiCoder.encode(['tuple(address)'], [[me.address]]) + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(0), functionData], // code = 1 means RECEIVE_INDEXER_CODE + ) + const tx = gatewayFinalizeTransfer(me.address, staking.address, tokens100k, callhookData) + + await expect(tx).revertedWith('ONLY_L1_STAKING_THROUGH_BRIDGE') + }) + it('adds stake to a new indexer', async function () { + const functionData = defaultAbiCoder.encode(['tuple(address)'], [[me.address]]) + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(0), functionData], // code = 1 means RECEIVE_INDEXER_CODE + ) + const tx = gatewayFinalizeTransfer( + mockL1Staking.address, + staking.address, + tokens100k, + callhookData, + ) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1Staking.address, staking.address, tokens100k) + await expect(tx).emit(staking, 'StakeDeposited').withArgs(me.address, tokens100k) + expect(await staking.getIndexerStakedTokens(me.address)).to.equal(tokens100k) + }) + it('adds stake to an existing indexer that was already migrated', async function () { + const functionData = defaultAbiCoder.encode(['tuple(address)'], [[me.address]]) + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(0), functionData], // code = 1 means RECEIVE_INDEXER_CODE + ) + await gatewayFinalizeTransfer( + mockL1Staking.address, + staking.address, + tokens100k, + callhookData, + ) + const tx = gatewayFinalizeTransfer( + mockL1Staking.address, + staking.address, + tokens100k, + callhookData, + ) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1Staking.address, staking.address, tokens100k) + await expect(tx).emit(staking, 'StakeDeposited').withArgs(me.address, tokens100k) + expect(await staking.getIndexerStakedTokens(me.address)).to.equal(tokens100k.add(tokens100k)) + }) + it('adds stake to an existing indexer that was staked in L2', async function () { + const functionData = defaultAbiCoder.encode(['tuple(address)'], [[me.address]]) + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(0), functionData], // code = 1 means RECEIVE_INDEXER_CODE + ) + await staking.connect(me.signer).stake(tokens100k) + const tx = gatewayFinalizeTransfer( + mockL1Staking.address, + staking.address, + tokens100k, + callhookData, + ) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1Staking.address, staking.address, tokens100k) + await expect(tx).emit(staking, 'StakeDeposited').withArgs(me.address, tokens100k) + expect(await staking.getIndexerStakedTokens(me.address)).to.equal(tokens100k.add(tokens100k)) + }) + }) + + describe('receiving delegation from L1 (onTokenTransfer)', function () { + it('adds delegation for a new delegator', async function () { + await staking.connect(me.signer).stake(tokens100k) + + const functionData = defaultAbiCoder.encode( + ['tuple(address,address)'], + [[me.address, other.address]], + ) + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(1), functionData], // code = 1 means RECEIVE_DELEGATION_CODE + ) + const tx = gatewayFinalizeTransfer( + mockL1Staking.address, + staking.address, + tokens10k, + callhookData, + ) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1Staking.address, staking.address, tokens10k) + const expectedShares = tokens10k + await expect(tx) + .emit(staking, 'StakeDelegated') + .withArgs(me.address, other.address, tokens10k, expectedShares) + const delegation = await staking.getDelegation(me.address, other.address) + expect(delegation.shares).to.equal(expectedShares) + }) + it('adds delegation for an existing delegator', async function () { + await staking.connect(me.signer).stake(tokens100k) + await staking.connect(other.signer).delegate(me.address, tokens10k) + + const functionData = defaultAbiCoder.encode( + ['tuple(address,address)'], + [[me.address, other.address]], + ) + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(1), functionData], // code = 1 means RECEIVE_DELEGATION_CODE + ) + const tx = gatewayFinalizeTransfer( + mockL1Staking.address, + staking.address, + tokens10k, + callhookData, + ) + + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1Staking.address, staking.address, tokens10k) + const expectedNewShares = tokens10k + const expectedTotalShares = tokens10k.mul(2) + await expect(tx) + .emit(staking, 'StakeDelegated') + .withArgs(me.address, other.address, tokens10k, expectedNewShares) + const delegation = await staking.getDelegation(me.address, other.address) + expect(delegation.shares).to.equal(expectedTotalShares) + }) + }) + describe('onTokenTransfer with invalid messages', function () { + it('reverts if the code is invalid', 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', 'bytes'], [toBN(2), '0x12345678']) + const tx = gatewayFinalizeTransfer( + mockL1Staking.address, + staking.address, + toGRT('1'), + callhookData, + ) + await expect(tx).revertedWith('INVALID_CODE') + }) + it('reverts if the message encoding is invalid', 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. + const callhookData = defaultAbiCoder.encode(['address', 'uint128'], [AddressZero, toBN(2)]) + const tx = gatewayFinalizeTransfer( + mockL1Staking.address, + staking.address, + toGRT('1'), + callhookData, + ) + await expect(tx).reverted // abi.decode will fail with no reason + }) + }) +}) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index a6afe9dbb..4900f3d48 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -9,12 +9,15 @@ 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' import { GraphToken } from '../../build/types/GraphToken' import { ServiceRegistry } from '../../build/types/ServiceRegistry' -import { Staking } from '../../build/types/Staking' +import { StakingExtension } from '../../build/types/StakingExtension' +import { IL1Staking } from '../../build/types/IL1Staking' +import { IL2Staking } from '../../build/types/IL2Staking' import { RewardsManager } from '../../build/types/RewardsManager' import { GraphGovernance } from '../../build/types/GraphGovernance' import { SubgraphNFT } from '../../build/types/SubgraphNFT' @@ -22,10 +25,19 @@ 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' +import path from 'path' +import { Artifacts } from 'hardhat/internal/artifacts' // Disable logging for tests logger.pause() +const ARTIFACTS_PATH = path.resolve('build/contracts') +const artifacts = new Artifacts(ARTIFACTS_PATH) +const iL1StakingAbi = artifacts.readArtifactSync('IL1Staking').abi +const iL2StakingAbi = artifacts.readArtifactSync('IL2Staking').abi + // Default configuration used in tests export const defaults = { @@ -56,7 +68,7 @@ export const defaults = { initialSupply: toGRT('10000000000'), // 10 billion }, rewards: { - issuanceRate: toGRT('1.000000023206889619'), // 5% annual rate + issuancePerBlock: toGRT('114.155251141552511415'), // 300M GRT/year dripInterval: toBN('50400'), // 1 week in blocks (post-Merge) }, } @@ -120,6 +132,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.minimumCurationDeposit, + ], + deployer, + ) as unknown as L2Curation +} + export async function deployDisputeManager( deployer: Signer, controller: string, @@ -155,13 +189,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 +203,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 +221,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( @@ -198,14 +258,48 @@ export async function deployServiceRegistry( ) as unknown as Promise } -export async function deployStaking( +export async function deployL1Staking( deployer: Signer, controller: string, proxyAdmin: GraphProxyAdmin, -): Promise { - return network.deployContractWithProxy( +): Promise { + const extensionImpl = (await deployContract( + 'StakingExtension', + deployer, + )) as unknown as StakingExtension + return (await network.deployContractWithProxy( + proxyAdmin, + 'L1Staking', + [ + controller, + defaults.staking.minimumIndexerStake, + defaults.staking.thawingPeriod, + 0, + 0, + defaults.staking.channelDisputeEpochs, + defaults.staking.maxAllocationEpochs, + defaults.staking.delegationUnbondingPeriod, + 0, + defaults.staking.alphaNumerator, + defaults.staking.alphaDenominator, + extensionImpl.address, + ], + deployer, + )) as unknown as IL1Staking +} + +export async function deployL2Staking( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + const extensionImpl = (await deployContract( + 'StakingExtension', + deployer, + )) as unknown as StakingExtension + return (await network.deployContractWithProxy( proxyAdmin, - 'Staking', + 'L2Staking', [ controller, defaults.staking.minimumIndexerStake, @@ -218,9 +312,10 @@ export async function deployStaking( 0, defaults.staking.alphaNumerator, defaults.staking.alphaDenominator, + extensionImpl.address, ], deployer, - ) as unknown as Staking + )) as unknown as IL2Staking } export async function deployRewardsManager( diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 8375a86a4..fc73c2129 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -12,8 +12,11 @@ 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 { Staking } from '../../build/types/Staking' +import { L2Curation } from '../../build/types/L2Curation' +import { L1GNS } from '../../build/types/L1GNS' +import { L2GNS } from '../../build/types/L2GNS' +import { IL1Staking } from '../../build/types/IL1Staking' +import { IL2Staking } from '../../build/types/IL2Staking' import { RewardsManager } from '../../build/types/RewardsManager' import { ServiceRegistry } from '../../build/types/ServiceRegistry' import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' @@ -28,8 +31,8 @@ export interface L1FixtureContracts { epochManager: EpochManager grt: GraphToken curation: Curation - gns: GNS - staking: Staking + gns: L1GNS + staking: IL1Staking rewardsManager: RewardsManager serviceRegistry: ServiceRegistry proxyAdmin: GraphProxyAdmin @@ -42,9 +45,9 @@ export interface L2FixtureContracts { disputeManager: DisputeManager epochManager: EpochManager grt: L2GraphToken - curation: Curation - gns: GNS - staking: Staking + curation: L2Curation + gns: L2GNS + staking: IL2Staking rewardsManager: RewardsManager serviceRegistry: ServiceRegistry proxyAdmin: GraphProxyAdmin @@ -91,9 +94,21 @@ 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) - const staking = await deployment.deployStaking(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 + let staking: IL1Staking | IL2Staking + if (isL2) { + gns = await deployment.deployL2GNS(deployer, controller.address, proxyAdmin) + staking = await deployment.deployL2Staking(deployer, controller.address, proxyAdmin) + } else { + gns = await deployment.deployL1GNS(deployer, controller.address, proxyAdmin) + staking = await deployment.deployL1Staking(deployer, controller.address, proxyAdmin) + } const disputeManager = await deployment.deployDisputeManager( deployer, controller.address, @@ -137,6 +152,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 { @@ -155,6 +171,7 @@ export class NetworkFixture { } else { await l1GraphTokenGateway.connect(deployer).syncAllContracts() await bridgeEscrow.connect(deployer).syncAllContracts() + await grt.connect(deployer).addMinter(l1GraphTokenGateway.address) } await staking.connect(deployer).setSlasher(slasherAddress, true) @@ -232,6 +249,8 @@ export class NetworkFixture { mockRouterAddress: string, mockL2GRTAddress: string, mockL2GatewayAddress: string, + mockL2GNSAddress: string, + mockL2StakingAddress: string, ): Promise { // First configure the Arbitrum bridge mocks await arbitrumMocks.bridgeMock.connect(deployer).setInbox(arbitrumMocks.inboxMock.address, true) @@ -257,6 +276,16 @@ 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.staking + .connect(deployer) + .setCounterpartStakingAddress(mockL2StakingAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .addToCallhookAllowlist(l1FixtureContracts.staking.address) await l1FixtureContracts.l1GraphTokenGateway.connect(deployer).setPaused(false) } @@ -266,6 +295,8 @@ export class NetworkFixture { mockRouterAddress: string, mockL1GRTAddress: string, mockL1GatewayAddress: string, + mockL1GNSAddress: string, + mockL1StakingAddress: string, ): Promise { // Configure the L2 GRT // Configure the gateway @@ -281,6 +312,10 @@ export class NetworkFixture { await l2FixtureContracts.l2GraphTokenGateway .connect(deployer) .setL1CounterpartAddress(mockL1GatewayAddress) + await l2FixtureContracts.gns.connect(deployer).setCounterpartGNSAddress(mockL1GNSAddress) + await l2FixtureContracts.staking + .connect(deployer) + .setCounterpartStakingAddress(mockL1StakingAddress) 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/payments/allocationExchange.test.ts b/test/payments/allocationExchange.test.ts index 03a1a3229..fba672e03 100644 --- a/test/payments/allocationExchange.test.ts +++ b/test/payments/allocationExchange.test.ts @@ -3,7 +3,7 @@ import { BigNumber, constants, Wallet } from 'ethers' import { AllocationExchange } from '../../build/types/AllocationExchange' import { GraphToken } from '../../build/types/GraphToken' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { NetworkFixture } from '../lib/fixtures' import * as deployment from '../lib/deployment' @@ -33,7 +33,7 @@ describe('AllocationExchange', () => { let fixture: NetworkFixture let grt: GraphToken - let staking: Staking + let staking: IStaking let allocationExchange: AllocationExchange async function createVoucher( diff --git a/test/payments/withdrawHelper.test.ts b/test/payments/withdrawHelper.test.ts index d7b2c8655..1cda7427e 100644 --- a/test/payments/withdrawHelper.test.ts +++ b/test/payments/withdrawHelper.test.ts @@ -3,7 +3,7 @@ import { constants } from 'ethers' import { GRTWithdrawHelper } from '../../build/types/GRTWithdrawHelper' import { GraphToken } from '../../build/types/GraphToken' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { NetworkFixture } from '../lib/fixtures' import * as deployment from '../lib/deployment' @@ -26,7 +26,7 @@ describe('WithdrawHelper', () => { let fixture: NetworkFixture let grt: GraphToken - let staking: Staking + let staking: IStaking let withdrawHelper: GRTWithdrawHelper function createWithdrawData(callData: string) { diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index beed73a73..6bf881e8f 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -9,8 +9,7 @@ import { Curation } from '../../build/types/Curation' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' import { RewardsManager } from '../../build/types/RewardsManager' -import { RewardsManagerMock } from '../../build/types/RewardsManagerMock' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { advanceBlocks, @@ -30,7 +29,7 @@ const MAX_PPM = 1000000 const { HashZero, WeiPerEther } = constants -const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] +const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] describe('Rewards', () => { let delegator: Account @@ -46,9 +45,8 @@ describe('Rewards', () => { let grt: GraphToken let curation: Curation let epochManager: EpochManager - let staking: Staking + let staking: IStaking let rewardsManager: RewardsManager - let rewardsManagerMock: RewardsManagerMock // Derive some channel keys for each indexer used to sign attestations const channelKey1 = deriveChannelKey() @@ -62,20 +60,19 @@ describe('Rewards', () => { const metadata = HashZero - const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 5% rewards - const ISSUANCE_RATE_PER_BLOCK = toBN('1012272234429039270') // % increase every block + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 800 GRT rewards + const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT every block // Core formula that gets accumulated rewards per signal for a period of time - const getRewardsPerSignal = (p: BN, r: BN, t: BN, s: BN): string => { + const getRewardsPerSignal = (k: BN, t: BN, s: BN): string => { if (s.eq(0)) { return '0' } - return p.times(r.pow(t)).minus(p).div(s).toPrecision(18).toString() + return k.times(t).div(s).toPrecision(18).toString() } // Tracks the accumulated rewards as totalSignalled or supply changes across snapshots class RewardsTracker { - totalSupply = BigNumber.from(0) totalSignalled = BigNumber.from(0) lastUpdatedBlock = BigNumber.from(0) accumulated = BigNumber.from(0) @@ -88,7 +85,6 @@ describe('Rewards', () => { async snapshot() { this.accumulated = this.accumulated.add(await this.accrued()) - this.totalSupply = await grt.totalSupply() this.totalSignalled = await grt.balanceOf(curation.address) this.lastUpdatedBlock = await latestBlock() return this @@ -106,8 +102,7 @@ describe('Rewards', () => { async accruedByElapsed(nBlocks: BigNumber | number) { const n = getRewardsPerSignal( - new BN(this.totalSupply.toString()), - new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18), + new BN(ISSUANCE_PER_BLOCK.toString()), new BN(nBlocks.toString()), new BN(this.totalSignalled.toString()), ) @@ -143,13 +138,8 @@ describe('Rewards', () => { governor.signer, )) - rewardsManagerMock = (await deployContract( - 'RewardsManagerMock', - governor.signer, - )) as unknown as RewardsManagerMock - - // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + // 200 GRT per block + await rewardsManager.connect(governor.signer).setIssuancePerBlock(ISSUANCE_PER_BLOCK) // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { @@ -168,28 +158,22 @@ describe('Rewards', () => { }) describe('configuration', function () { - describe('issuance rate update', function () { - it('reject set issuance rate if unauthorized', async function () { - const tx = rewardsManager.connect(indexer1.signer).setIssuanceRate(toGRT('1.025')) + describe('issuance per block update', function () { + it('reject set issuance per block if unauthorized', async function () { + const tx = rewardsManager.connect(indexer1.signer).setIssuancePerBlock(toGRT('1.025')) await expect(tx).revertedWith('Only Controller governor') }) - it('reject set issuance rate to less than minimum allowed', async function () { - const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 - const tx = rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - await expect(tx).revertedWith('Issuance rate under minimum allowed') - }) - - it('should set issuance rate to minimum allowed', async function () { - const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) + it('should set issuance rate to minimum allowed (0)', async function () { + const newIssuancePerBlock = toGRT('0') + await rewardsManager.connect(governor.signer).setIssuancePerBlock(newIssuancePerBlock) + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) }) it('should set issuance rate', async function () { - const newIssuanceRate = toGRT('1.025') - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) + const newIssuancePerBlock = toGRT('100.025') + await rewardsManager.connect(governor.signer).setIssuancePerBlock(newIssuancePerBlock) + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await latestBlock()) }) }) @@ -245,7 +229,7 @@ describe('Rewards', () => { context('issuing rewards', async function () { beforeEach(async function () { // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await rewardsManager.connect(governor.signer).setIssuancePerBlock(ISSUANCE_PER_BLOCK) }) describe('getNewRewardsPerSignal', function () { @@ -450,10 +434,8 @@ describe('Rewards', () => { await advanceBlocks(ISSUANCE_RATE_PERIODS) // Prepare expected results - // NOTE: calculated the expected result manually as the above code has 1 off block difference - // replace with a RewardsManagerMock - const expectedSubgraphRewards = toGRT('891695470') - const expectedRewardsAT = toGRT('51571') + const expectedSubgraphRewards = toGRT('1400') // 7 blocks since signaling to when we do getAccRewardsForSubgraph + const expectedRewardsAT = toGRT('0.08') // allocated during 5 blocks: 1000 GRT, divided by 12500 allocated tokens // Update await rewardsManager.onSubgraphAllocationUpdate(subgraphDeploymentID1) @@ -466,7 +448,7 @@ describe('Rewards', () => { const contractRewardsAT = subgraph.accRewardsPerAllocatedToken expect(toRound(expectedSubgraphRewards)).eq(toRound(contractSubgraphRewards)) - expect(toRound(expectedRewardsAT)).eq(toRound(contractRewardsAT)) + expect(toRound(expectedRewardsAT.mul(1000))).eq(toRound(contractRewardsAT.mul(1000))) }) }) @@ -619,13 +601,11 @@ describe('Rewards', () => { const beforeStakingBalance = await grt.balanceOf(staking.address) // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted. + // The final snapshot is when we close the allocation, that happens 9 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 7) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('1400') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking @@ -731,13 +711,11 @@ describe('Rewards', () => { const beforeStakingBalance = await grt.balanceOf(staking.address) // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted. + // The final snapshot is when we close the allocation, that happens 9 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 7) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('1400') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking @@ -805,13 +783,11 @@ describe('Rewards', () => { // Check that rewards are put into delegators pool accordingly // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 14500 = 8466.995 - // The final snapshot is when we close the allocation, that happens 4 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 4 - 10004000000) / 14500 = 34496.55 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. - const expectedIndexingRewards = toGRT('377428566.77') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 1 block after the signal is minted. + // The final snapshot is when we close the allocation, that happens 4 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 3) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('600') // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards) @@ -848,18 +824,6 @@ describe('Rewards', () => { }) }) - describe('pow', function () { - it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { - const baseRatio = toGRT('0.000000004641377923') // 1% annual rate - const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks - for (let i = 0; i < 50; i = i + 4) { - const r = baseRatio.mul(i * 4).add(toGRT('1')) - const h = await rewardsManagerMock.pow(r, timePeriods, toGRT('1')) - console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) - } - }) - }) - describe('edge scenarios', function () { it('close allocation on a subgraph that no longer have signal', async function () { // Update total signalled diff --git a/test/serviceRegisty.test.ts b/test/serviceRegisty.test.ts index 726afa2f2..14027b5c3 100644 --- a/test/serviceRegisty.test.ts +++ b/test/serviceRegisty.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { ServiceRegistry } from '../build/types/ServiceRegistry' -import { Staking } from '../build/types/Staking' +import { IStaking } from '../build/types/IStaking' import { getAccounts, Account } from './lib/testHelpers' import { NetworkFixture } from './lib/fixtures' @@ -14,7 +14,7 @@ describe('ServiceRegistry', () => { let fixture: NetworkFixture let serviceRegistry: ServiceRegistry - let staking: Staking + let staking: IStaking const shouldRegister = async (url: string, geohash: string) => { // Register the indexer service diff --git a/test/staking/allocation.test.ts b/test/staking/allocation.test.ts index 4d6a9150c..c3a9bbb0e 100644 --- a/test/staking/allocation.test.ts +++ b/test/staking/allocation.test.ts @@ -4,7 +4,7 @@ import { constants, BigNumber, PopulatedTransaction } from 'ethers' import { Curation } from '../../build/types/Curation' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { NetworkFixture } from '../lib/fixtures' import { @@ -51,7 +51,7 @@ describe('Staking:Allocation', () => { let curation: Curation let epochManager: EpochManager let grt: GraphToken - let staking: Staking + let staking: IStaking // Test values @@ -84,6 +84,9 @@ describe('Staking:Allocation', () => { // Before state const beforeStake = await staking.stakes(indexer.address) + // Advance to next epoch to prevent issues doing this at an epoch boundary + await advanceToNextEpoch(epochManager) + // Allocate const currentEpoch = await epochManager.currentEpoch() const tx = allocate(tokensToAllocate) @@ -354,7 +357,7 @@ describe('Staking:Allocation', () => { it('reject allocate if no tokens staked', async function () { const tx = allocate(toBN('1')) - await expect(tx).revertedWith('!capacity') + await expect(tx).revertedWith('!minimumIndexerStake') }) it('reject allocate zero tokens if no minimum stake', async function () { @@ -582,6 +585,9 @@ 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) }) diff --git a/test/staking/configuration.test.ts b/test/staking/configuration.test.ts index 52aeaa843..9f86edd8d 100644 --- a/test/staking/configuration.test.ts +++ b/test/staking/configuration.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { constants } from 'ethers' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { defaults } from '../lib/deployment' import { NetworkFixture } from '../lib/fixtures' @@ -19,7 +19,7 @@ describe('Staking:Config', () => { let fixture: NetworkFixture - let staking: Staking + let staking: IStaking before(async function () { ;[me, other, governor, slasher] = await getAccounts() diff --git a/test/staking/delegation.test.ts b/test/staking/delegation.test.ts index e097dfd9f..3ad68cadb 100644 --- a/test/staking/delegation.test.ts +++ b/test/staking/delegation.test.ts @@ -3,7 +3,7 @@ import { constants, BigNumber } from 'ethers' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { NetworkFixture } from '../lib/fixtures' import { @@ -34,7 +34,7 @@ describe('Staking::Delegation', () => { let epochManager: EpochManager let grt: GraphToken - let staking: Staking + let staking: IStaking // Test values const poi = randomHexBytes() diff --git a/test/staking/migration.test.ts b/test/staking/migration.test.ts new file mode 100644 index 000000000..d1e0a4a08 --- /dev/null +++ b/test/staking/migration.test.ts @@ -0,0 +1,786 @@ +import { expect } from 'chai' +import { constants, BigNumber, Event } from 'ethers' +import { defaultAbiCoder, ParamType, parseEther } from 'ethers/lib/utils' + +import { GraphToken } from '../../build/types/GraphToken' +import { IL1Staking } from '../../build/types/IL1Staking' +import { IStaking } from '../../build/types/IStaking' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +import { L1GraphTokenLockMigratorMock } from '../../build/types/L1GraphTokenLockMigratorMock' + +import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' + +import { + advanceBlockTo, + deriveChannelKey, + getAccounts, + randomHexBytes, + latestBlock, + toBN, + toGRT, + provider, + Account, + setAccountBalance, + impersonateAccount, +} from '../lib/testHelpers' +import { deployContract } from '../lib/deployment' + +const { AddressZero, MaxUint256 } = constants + +describe('L1Staking:Migration', () => { + let me: Account + let governor: Account + let indexer: Account + let slasher: Account + let l2Indexer: Account + let delegator: Account + let l2Delegator: Account + let mockRouter: Account + let mockL2GRT: Account + let mockL2Gateway: Account + let mockL2GNS: Account + let mockL2Staking: Account + + let fixture: NetworkFixture + let fixtureContracts: L1FixtureContracts + + let grt: GraphToken + let staking: IL1Staking + let l1GraphTokenGateway: L1GraphTokenGateway + let arbitrumMocks: ArbitrumL1Mocks + let l1GraphTokenLockMigrator: L1GraphTokenLockMigratorMock + + // Test values + const indexerTokens = toGRT('10000000') + const delegatorTokens = toGRT('1000000') + const tokensToStake = toGRT('200000') + const subgraphDeploymentID = randomHexBytes() + const channelKey = deriveChannelKey() + const allocationID = channelKey.address + const metadata = randomHexBytes(32) + const minimumIndexerStake = toGRT('100000') + const delegationTaxPPM = 10000 // 1% + // Dummy L2 gas values + const maxGas = toBN('1000000') + const gasPriceBid = toBN('1000000000') + const maxSubmissionCost = toBN('1000000000') + + // Allocate with test values + const allocate = async (tokens: BigNumber) => { + return staking + .connect(indexer.signer) + .allocateFrom( + indexer.address, + subgraphDeploymentID, + tokens, + allocationID, + metadata, + await channelKey.generateProof(indexer.address), + ) + } + + before(async function () { + ;[ + me, + governor, + indexer, + slasher, + delegator, + l2Indexer, + mockRouter, + mockL2GRT, + mockL2Gateway, + mockL2GNS, + mockL2Staking, + l2Delegator, + ] = await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.load(governor.signer, slasher.signer) + ;({ grt, staking, l1GraphTokenGateway } = fixtureContracts) + // Dummy code on the mock router so that it appears as a contract + await provider().send('hardhat_setCode', [mockRouter.address, '0x1234']) + arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + mockL2GNS.address, + mockL2Staking.address, + ) + + l1GraphTokenLockMigrator = (await deployContract( + 'L1GraphTokenLockMigratorMock', + governor.signer, + )) as unknown as L1GraphTokenLockMigratorMock + + await setAccountBalance(l1GraphTokenLockMigrator.address, parseEther('1')) + + await staking + .connect(governor.signer) + .setL1GraphTokenLockMigrator(l1GraphTokenLockMigrator.address) + + // Give some funds to the indexer and approve staking contract to use funds on indexer behalf + await grt.connect(governor.signer).mint(indexer.address, indexerTokens) + await grt.connect(indexer.signer).approve(staking.address, indexerTokens) + + await grt.connect(governor.signer).mint(delegator.address, delegatorTokens) + await grt.connect(delegator.signer).approve(staking.address, delegatorTokens) + + await staking.connect(governor.signer).setMinimumIndexerStake(minimumIndexerStake) + await staking.connect(governor.signer).setDelegationTaxPercentage(delegationTaxPPM) // 1% + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + context('> when not staked', function () { + describe('migrateStakeToL2', function () { + it('should not allow migrating for someone who has not staked', async function () { + const tx = staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + tokensToStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('tokensStaked == 0') + }) + }) + }) + + context('> when staked', function () { + const shouldMigrateIndexerStake = async ( + amountToSend: BigNumber, + options: { + expectedSeqNum?: number + l2Beneficiary?: string + } = {}, + ) => { + const l2Beneficiary = options.l2Beneficiary ?? l2Indexer.address + const expectedSeqNum = options.expectedSeqNum ?? 1 + const tx = staking + .connect(indexer.signer) + .migrateStakeToL2(l2Beneficiary, amountToSend, maxGas, gasPriceBid, maxSubmissionCost) + const expectedFunctionData = defaultAbiCoder.encode(['tuple(address)'], [[l2Indexer.address]]) + + const expectedCallhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(0), expectedFunctionData], // code = 1 means RECEIVE_INDEXER_CODE + ) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + staking.address, + mockL2Staking.address, + amountToSend, + expectedCallhookData, + ) + + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(staking.address, mockL2Gateway.address, toBN(expectedSeqNum), expectedL2Data) + } + + beforeEach(async function () { + await staking.connect(indexer.signer).stake(tokensToStake) + }) + + describe('receive()', function () { + it('should not allow receiving funds from a random address', async function () { + const tx = indexer.signer.sendTransaction({ + to: staking.address, + value: parseEther('1'), + }) + await expect(tx).revertedWith('Only migrator can send ETH') + }) + it('should allow receiving funds from the migrator', async function () { + const impersonatedMigrator = await impersonateAccount(l1GraphTokenLockMigrator.address) + const tx = impersonatedMigrator.sendTransaction({ + to: staking.address, + value: parseEther('1'), + }) + await expect(tx).to.not.be.reverted + }) + }) + describe('migrateStakeToL2', function () { + it('should not allow migrating but leaving less than the minimum indexer stake', async function () { + const tx = staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + tokensToStake.sub(minimumIndexerStake).add(1), + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('!minimumIndexerStake remaining') + }) + it('should not allow migrating less than the minimum indexer stake the first time', async function () { + const tx = staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake.sub(1), + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('!minimumIndexerStake sent') + }) + it('should not allow migrating if there are tokens locked for withdrawal', async function () { + await staking.connect(indexer.signer).unstake(tokensToStake) + const tx = staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + tokensToStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('tokensLocked != 0') + }) + it('should not allow migrating to a beneficiary that is address zero', async function () { + const tx = staking + .connect(indexer.signer) + .migrateStakeToL2(AddressZero, tokensToStake, maxGas, gasPriceBid, maxSubmissionCost) + await expect(tx).revertedWith('l2Beneficiary == 0') + }) + it('should not allow migrating the whole stake if there are open allocations', async function () { + await allocate(toGRT('10')) + const tx = staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + tokensToStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('allocated') + }) + it('should not allow migrating partial stake if the remaining indexer capacity is insufficient for open allocations', async function () { + // We set delegation ratio == 1 so an indexer can only use as much delegation as their own stake + await staking.connect(governor.signer).setDelegationRatio(1) + const tokensToDelegate = toGRT('202100') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + + // Now the indexer has 200k tokens staked and 200k tokens delegated + await allocate(toGRT('400000')) + + // But if we try to migrate even 100k, we will not have enough indexer capacity to cover the open allocation + const tx = staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + toGRT('100000'), + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('! allocation capacity') + }) + it('sends the tokens and a message through the L1GraphTokenGateway', async function () { + const amountToSend = minimumIndexerStake + await shouldMigrateIndexerStake(amountToSend) + // Check that the indexer stake was reduced by the sent amount + expect((await staking.stakes(indexer.address)).tokensStaked).to.equal( + tokensToStake.sub(amountToSend), + ) + }) + it('should allow migrating the whole stake if there are no open allocations', async function () { + await shouldMigrateIndexerStake(tokensToStake) + // Check that the indexer stake was reduced by the sent amount + expect((await staking.stakes(indexer.address)).tokensStaked).to.equal(0) + }) + it('should allow migrating partial stake if the remaining capacity can cover the allocations', async function () { + // We set delegation ratio == 1 so an indexer can only use as much delegation as their own stake + await staking.connect(governor.signer).setDelegationRatio(1) + const tokensToDelegate = toGRT('200000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + + // Now the indexer has 200k tokens staked and 200k tokens delegated, + // but they allocate 200k + await allocate(toGRT('200000')) + + // If we migrate 100k, we will still have enough indexer capacity to cover the open allocation + const amountToSend = toGRT('100000') + await shouldMigrateIndexerStake(amountToSend) + // Check that the indexer stake was reduced by the sent amount + expect((await staking.stakes(indexer.address)).tokensStaked).to.equal( + tokensToStake.sub(amountToSend), + ) + }) + it('allows migrating several times to the same beneficiary', async function () { + // Stake a bit more so we're still over the minimum stake after migrating twice + await staking.connect(indexer.signer).stake(tokensToStake) + await shouldMigrateIndexerStake(minimumIndexerStake) + await shouldMigrateIndexerStake(toGRT('1000'), { expectedSeqNum: 2 }) + expect((await staking.stakes(indexer.address)).tokensStaked).to.equal( + tokensToStake.mul(2).sub(minimumIndexerStake).sub(toGRT('1000')), + ) + }) + it('should not allow migrating to a different beneficiary the second time', async function () { + await shouldMigrateIndexerStake(minimumIndexerStake) + const tx = staking.connect(indexer.signer).migrateStakeToL2( + indexer.address, // Note this is different from l2Indexer used before + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('l2Beneficiary != previous') + }) + }) + + describe('migrateLockedStakeToL2', function () { + it('sends a message through L1GraphTokenGateway like migrateStakeToL2, but gets the beneficiary and ETH from a migrator contract', async function () { + const amountToSend = minimumIndexerStake + + await l1GraphTokenLockMigrator.setMigratedAddress(indexer.address, l2Indexer.address) + const oldMigratorEthBalance = await provider().getBalance(l1GraphTokenLockMigrator.address) + const tx = staking + .connect(indexer.signer) + .migrateLockedStakeToL2(minimumIndexerStake, maxGas, gasPriceBid, maxSubmissionCost) + const expectedFunctionData = defaultAbiCoder.encode( + ['tuple(address)'], + [[l2Indexer.address]], + ) + + const expectedCallhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(0), expectedFunctionData], // code = 0 means RECEIVE_INDEXER_CODE + ) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + staking.address, + mockL2Staking.address, + amountToSend, + expectedCallhookData, + ) + + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(staking.address, mockL2Gateway.address, toBN(1), expectedL2Data) + expect(await provider().getBalance(l1GraphTokenLockMigrator.address)).to.equal( + oldMigratorEthBalance.sub(maxSubmissionCost).sub(gasPriceBid.mul(maxGas)), + ) + }) + it('should not allow migrating if the migrator contract returns a zero address beneficiary', async function () { + const amountToSend = minimumIndexerStake + + const tx = staking + .connect(indexer.signer) + .migrateLockedStakeToL2(minimumIndexerStake, maxGas, gasPriceBid, maxSubmissionCost) + await expect(tx).revertedWith('LOCK NOT MIGRATED') + }) + }) + describe('unlockDelegationToMigratedIndexer', function () { + beforeEach(async function () { + await staking.connect(governor.signer).setDelegationUnbondingPeriod(28) // epochs + }) + it('allows a delegator to a migrated indexer to withdraw locked delegation before the unbonding period', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + const actualDelegation = tokensToDelegate.sub( + tokensToDelegate.mul(delegationTaxPPM).div(1000000), + ) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + tokensToStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await staking.connect(delegator.signer).undelegate(indexer.address, actualDelegation) + const tx = await staking + .connect(delegator.signer) + .unlockDelegationToMigratedIndexer(indexer.address) + await expect(tx) + .emit(staking, 'StakeDelegatedUnlockedDueToMigration') + .withArgs(indexer.address, delegator.address) + const tx2 = await staking + .connect(delegator.signer) + .withdrawDelegated(indexer.address, AddressZero) + await expect(tx2) + .emit(staking, 'StakeDelegatedWithdrawn') + .withArgs(indexer.address, delegator.address, actualDelegation) + }) + it('rejects calls if the indexer has not migrated their stake', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + const tx = staking + .connect(delegator.signer) + .unlockDelegationToMigratedIndexer(indexer.address) + await expect(tx).revertedWith('indexer not migrated') + }) + it('rejects calls if the indexer has only migrated part of their stake but not all', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + const tx = staking + .connect(delegator.signer) + .unlockDelegationToMigratedIndexer(indexer.address) + await expect(tx).revertedWith('indexer not migrated') + }) + it('rejects calls if the delegator has not undelegated first', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + tokensToStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + const tx = staking + .connect(delegator.signer) + .unlockDelegationToMigratedIndexer(indexer.address) + await expect(tx).revertedWith('! locked') + }) + it('rejects calls if the caller is not a delegator', async function () { + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + tokensToStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + const tx = staking + .connect(delegator.signer) + .unlockDelegationToMigratedIndexer(indexer.address) + // The function checks for tokensLockedUntil so this is the error we should get: + await expect(tx).revertedWith('! locked') + }) + }) + describe('migrateDelegationToL2', function () { + it('rejects calls if the delegated indexer has not migrated stake to L2', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + + const tx = staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + l2Delegator.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('indexer not migrated') + }) + it('rejects calls if the beneficiary is zero', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + const tx = staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + AddressZero, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('l2Beneficiary == 0') + }) + it('rejects calls if the delegator has tokens locked for undelegation', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await staking.connect(delegator.signer).undelegate(indexer.address, toGRT('1')) + + const tx = staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + l2Delegator.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('tokensLocked != 0') + }) + it('rejects calls if the delegator has no tokens delegated to the indexer', async function () { + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + const tx = staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + l2Delegator.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('delegation == 0') + }) + it('sends all the tokens delegated to the indexer to the beneficiary on L2, using the gateway', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + const actualDelegation = tokensToDelegate.sub( + tokensToDelegate.mul(delegationTaxPPM).div(1000000), + ) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + const expectedFunctionData = defaultAbiCoder.encode( + ['tuple(address,address)'], + [[l2Indexer.address, l2Delegator.address]], + ) + + const expectedCallhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(1), expectedFunctionData], // code = 1 means RECEIVE_DELEGATION_CODE + ) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + staking.address, + mockL2Staking.address, + actualDelegation, + expectedCallhookData, + ) + + const tx = staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + l2Delegator.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + // seqNum is 2 because the first bridge call was in migrateStakeToL2 + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(staking.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx) + .emit(staking, 'DelegationMigratedToL2') + .withArgs( + delegator.address, + l2Delegator.address, + indexer.address, + l2Indexer.address, + actualDelegation, + ) + }) + it('sets the delegation shares to zero so cannot be called twice', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + await staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + l2Delegator.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + const tx = staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + l2Delegator.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx).revertedWith('delegation == 0') + }) + it('can be called again if the delegator added more delegation (edge case)', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + const actualDelegation = tokensToDelegate.sub( + tokensToDelegate.mul(delegationTaxPPM).div(1000000), + ) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + await staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + l2Delegator.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + + const tx = staking + .connect(delegator.signer) + .migrateDelegationToL2( + indexer.address, + l2Delegator.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + await expect(tx) + .emit(staking, 'DelegationMigratedToL2') + .withArgs( + delegator.address, + l2Delegator.address, + indexer.address, + l2Indexer.address, + actualDelegation, + ) + }) + }) + describe('migrateLockedDelegationToL2', function () { + it('sends delegated tokens to L2 like migrateDelegationToL2, but gets the beneficiary and ETH from the L1GraphTokenLockMigrator', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + const actualDelegation = tokensToDelegate.sub( + tokensToDelegate.mul(delegationTaxPPM).div(1000000), + ) + + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + const expectedFunctionData = defaultAbiCoder.encode( + ['tuple(address,address)'], + [[l2Indexer.address, l2Delegator.address]], + ) + + const expectedCallhookData = defaultAbiCoder.encode( + ['uint8', 'bytes'], + [toBN(1), expectedFunctionData], // code = 1 means RECEIVE_DELEGATION_CODE + ) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + staking.address, + mockL2Staking.address, + actualDelegation, + expectedCallhookData, + ) + + await l1GraphTokenLockMigrator.setMigratedAddress(delegator.address, l2Delegator.address) + + const oldMigratorEthBalance = await provider().getBalance(l1GraphTokenLockMigrator.address) + const tx = staking + .connect(delegator.signer) + .migrateLockedDelegationToL2(indexer.address, maxGas, gasPriceBid, maxSubmissionCost) + // seqNum is 2 because the first bridge call was in migrateStakeToL2 + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(staking.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx) + .emit(staking, 'DelegationMigratedToL2') + .withArgs( + delegator.address, + l2Delegator.address, + indexer.address, + l2Indexer.address, + actualDelegation, + ) + expect(await provider().getBalance(l1GraphTokenLockMigrator.address)).to.equal( + oldMigratorEthBalance.sub(maxSubmissionCost).sub(gasPriceBid.mul(maxGas)), + ) + }) + it('rejects calls if the migrator contract returns a zero address beneficiary', async function () { + const tokensToDelegate = toGRT('10000') + await staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate) + const actualDelegation = tokensToDelegate.sub( + tokensToDelegate.mul(delegationTaxPPM).div(1000000), + ) + await staking + .connect(indexer.signer) + .migrateStakeToL2( + l2Indexer.address, + minimumIndexerStake, + maxGas, + gasPriceBid, + maxSubmissionCost, + ) + + const tx = staking + .connect(delegator.signer) + .migrateLockedDelegationToL2(indexer.address, maxGas, gasPriceBid, maxSubmissionCost) + await expect(tx).revertedWith('LOCK NOT MIGRATED') + }) + }) + }) +}) diff --git a/test/staking/staking.test.ts b/test/staking/staking.test.ts index b769c8f57..bebe9247a 100644 --- a/test/staking/staking.test.ts +++ b/test/staking/staking.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import { constants, BigNumber, Event } from 'ethers' import { GraphToken } from '../../build/types/GraphToken' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import { NetworkFixture } from '../lib/fixtures' @@ -39,7 +39,7 @@ describe('Staking:Stakes', () => { let fixture: NetworkFixture let grt: GraphToken - let staking: Staking + let staking: IStaking // Test values const indexerTokens = toGRT('1000') diff --git a/test/upgrade/admin.test.ts b/test/upgrade/admin.test.ts index a20640943..71e30b777 100644 --- a/test/upgrade/admin.test.ts +++ b/test/upgrade/admin.test.ts @@ -5,7 +5,7 @@ import '@nomiclabs/hardhat-ethers' import { GraphProxy } from '../../build/types/GraphProxy' import { Curation } from '../../build/types/Curation' import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' -import { Staking } from '../../build/types/Staking' +import { IStaking } from '../../build/types/IStaking' import * as deployment from '../lib/deployment' import { NetworkFixture } from '../lib/fixtures' @@ -24,7 +24,7 @@ describe('Upgrades', () => { let proxyAdmin: GraphProxyAdmin let curation: Curation - let staking: Staking + let staking: IStaking let stakingProxy: GraphProxy before(async function () {