@@ -10,6 +10,7 @@ import {
10
10
getL2SignerFromL1 ,
11
11
setAccountBalance ,
12
12
latestBlock ,
13
+ deriveChannelKey ,
13
14
} from '../lib/testHelpers'
14
15
import { L2FixtureContracts , NetworkFixture } from '../lib/fixtures'
15
16
import { toBN } from '../lib/testHelpers'
@@ -30,6 +31,7 @@ import {
30
31
} from '../lib/gnsUtils'
31
32
import { L2Curation } from '../../build/types/L2Curation'
32
33
import { GraphToken } from '../../build/types/GraphToken'
34
+ import { IL2Staking } from '../../build/types/IL2Staking'
33
35
34
36
const { HashZero } = ethers . constants
35
37
@@ -43,6 +45,7 @@ interface L1SubgraphParams {
43
45
describe ( 'L2GNS' , ( ) => {
44
46
let me : Account
45
47
let other : Account
48
+ let attacker : Account
46
49
let governor : Account
47
50
let mockRouter : Account
48
51
let mockL1GRT : Account
@@ -56,6 +59,7 @@ describe('L2GNS', () => {
56
59
let gns : L2GNS
57
60
let curation : L2Curation
58
61
let grt : GraphToken
62
+ let staking : IL2Staking
59
63
60
64
let newSubgraph0 : PublishSubgraph
61
65
let newSubgraph1 : PublishSubgraph
@@ -114,12 +118,21 @@ describe('L2GNS', () => {
114
118
115
119
before ( async function ( ) {
116
120
newSubgraph0 = buildSubgraph ( )
117
- ; [ me , other , governor , mockRouter , mockL1GRT , mockL1Gateway , mockL1GNS , mockL1Staking ] =
118
- await getAccounts ( )
121
+ ; [
122
+ me ,
123
+ other ,
124
+ governor ,
125
+ mockRouter ,
126
+ mockL1GRT ,
127
+ mockL1Gateway ,
128
+ mockL1GNS ,
129
+ mockL1Staking ,
130
+ attacker ,
131
+ ] = await getAccounts ( )
119
132
120
133
fixture = new NetworkFixture ( )
121
134
fixtureContracts = await fixture . loadL2 ( governor . signer )
122
- ; ( { l2GraphTokenGateway, gns, curation, grt } = fixtureContracts )
135
+ ; ( { l2GraphTokenGateway, gns, curation, grt, staking } = fixtureContracts )
123
136
124
137
await grt . connect ( governor . signer ) . mint ( me . address , toGRT ( '10000' ) )
125
138
await fixture . configureL2Bridge (
@@ -398,6 +411,67 @@ describe('L2GNS', () => {
398
411
. emit ( gns , 'SignalMinted' )
399
412
. withArgs ( l2SubgraphId , me . address , expectedNSignal , expectedSignal , curatedTokens )
400
413
} )
414
+ it ( 'protects the owner against a rounding attack' , async function ( ) {
415
+ const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } =
416
+ await defaultL1SubgraphParams ( )
417
+ const collectTokens = curatedTokens . mul ( 20 )
418
+
419
+ await staking . connect ( governor . signer ) . setCurationPercentage ( 100000 )
420
+
421
+ // Set up an indexer account with some stake
422
+ await grt . connect ( governor . signer ) . mint ( attacker . address , toGRT ( '1000000' ) )
423
+ // Curate 1 wei GRT by minting 1 GRT and burning most of it
424
+ await grt . connect ( attacker . signer ) . approve ( curation . address , toBN ( 1 ) )
425
+ await curation . connect ( attacker . signer ) . mint ( newSubgraph0 . subgraphDeploymentID , toBN ( 1 ) , 0 )
426
+
427
+ // Check this actually gave us 1 wei signal
428
+ expect ( await curation . getCurationPoolTokens ( newSubgraph0 . subgraphDeploymentID ) ) . eq ( 1 )
429
+ await grt . connect ( attacker . signer ) . approve ( staking . address , toGRT ( '1000000' ) )
430
+ await staking . connect ( attacker . signer ) . stake ( toGRT ( '100000' ) )
431
+ const channelKey = deriveChannelKey ( )
432
+ // Allocate to the same deployment ID
433
+ await staking
434
+ . connect ( attacker . signer )
435
+ . allocateFrom (
436
+ attacker . address ,
437
+ newSubgraph0 . subgraphDeploymentID ,
438
+ toGRT ( '100000' ) ,
439
+ channelKey . address ,
440
+ randomHexBytes ( 32 ) ,
441
+ await channelKey . generateProof ( attacker . address ) ,
442
+ )
443
+ // Spoof some query fees, 10% of which will go to the Curation pool
444
+ await staking . connect ( attacker . signer ) . collect ( collectTokens , channelKey . address )
445
+ // The curation pool now has 1 wei shares and a lot of tokens, so the rounding attack is prepared
446
+ // But L2GNS will protect the owner by sending the tokens
447
+ const callhookData = defaultAbiCoder . encode (
448
+ [ 'uint8' , 'uint256' , 'address' ] ,
449
+ [ toBN ( 0 ) , l1SubgraphId , me . address ] ,
450
+ )
451
+ await gatewayFinalizeTransfer ( mockL1GNS . address , gns . address , curatedTokens , callhookData )
452
+
453
+ const l2SubgraphId = await gns . getAliasedL2SubgraphID ( l1SubgraphId )
454
+ const tx = gns
455
+ . connect ( me . signer )
456
+ . finishSubgraphTransferFromL1 (
457
+ l2SubgraphId ,
458
+ newSubgraph0 . subgraphDeploymentID ,
459
+ subgraphMetadata ,
460
+ versionMetadata ,
461
+ )
462
+ await expect ( tx )
463
+ . emit ( gns , 'SubgraphPublished' )
464
+ . withArgs ( l2SubgraphId , newSubgraph0 . subgraphDeploymentID , DEFAULT_RESERVE_RATIO )
465
+ await expect ( tx ) . emit ( gns , 'SubgraphMetadataUpdated' ) . withArgs ( l2SubgraphId , subgraphMetadata )
466
+ await expect ( tx ) . emit ( gns , 'CuratorBalanceReturnedToBeneficiary' )
467
+ await expect ( tx )
468
+ . emit ( gns , 'SubgraphUpgraded' )
469
+ . withArgs ( l2SubgraphId , 0 , 0 , newSubgraph0 . subgraphDeploymentID )
470
+ await expect ( tx )
471
+ . emit ( gns , 'SubgraphVersionUpdated' )
472
+ . withArgs ( l2SubgraphId , newSubgraph0 . subgraphDeploymentID , versionMetadata )
473
+ await expect ( tx ) . emit ( gns , 'SubgraphL2TransferFinalized' ) . withArgs ( l2SubgraphId )
474
+ } )
401
475
it ( 'cannot be called by someone other than the subgraph owner' , async function ( ) {
402
476
const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } =
403
477
await defaultL1SubgraphParams ( )
@@ -741,6 +815,63 @@ describe('L2GNS', () => {
741
815
expect ( gnsBalanceAfter ) . eq ( gnsBalanceBefore )
742
816
} )
743
817
818
+ it ( 'protects the curator against a rounding attack' , async function ( ) {
819
+ // Transfer a subgraph from L1 with only 1 wei GRT of curated signal
820
+ const { l1SubgraphId, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams ( )
821
+ const curatedTokens = toBN ( '1' )
822
+ await transferMockSubgraphFromL1 (
823
+ l1SubgraphId ,
824
+ curatedTokens ,
825
+ subgraphMetadata ,
826
+ versionMetadata ,
827
+ )
828
+ // Prepare the rounding attack by setting up an indexer and collecting a lot of query fees
829
+ const curatorTokens = toGRT ( '10000' )
830
+ const collectTokens = curatorTokens . mul ( 20 )
831
+ await staking . connect ( governor . signer ) . setCurationPercentage ( 100000 )
832
+ // Set up an indexer account with some stake
833
+ await grt . connect ( governor . signer ) . mint ( attacker . address , toGRT ( '1000000' ) )
834
+
835
+ await grt . connect ( attacker . signer ) . approve ( staking . address , toGRT ( '1000000' ) )
836
+ await staking . connect ( attacker . signer ) . stake ( toGRT ( '100000' ) )
837
+ const channelKey = deriveChannelKey ( )
838
+ // Allocate to the same deployment ID
839
+ await staking
840
+ . connect ( attacker . signer )
841
+ . allocateFrom (
842
+ attacker . address ,
843
+ newSubgraph0 . subgraphDeploymentID ,
844
+ toGRT ( '100000' ) ,
845
+ channelKey . address ,
846
+ randomHexBytes ( 32 ) ,
847
+ await channelKey . generateProof ( attacker . address ) ,
848
+ )
849
+ // Spoof some query fees, 10% of which will go to the Curation pool
850
+ await staking . connect ( attacker . signer ) . collect ( collectTokens , channelKey . address )
851
+
852
+ const callhookData = defaultAbiCoder . encode (
853
+ [ 'uint8' , 'uint256' , 'address' ] ,
854
+ [ toBN ( 1 ) , l1SubgraphId , me . address ] ,
855
+ )
856
+ const curatorTokensBefore = await grt . balanceOf ( me . address )
857
+ const gnsBalanceBefore = await grt . balanceOf ( gns . address )
858
+ const tx = gatewayFinalizeTransfer (
859
+ mockL1GNS . address ,
860
+ gns . address ,
861
+ curatorTokens ,
862
+ callhookData ,
863
+ )
864
+ await expect ( tx )
865
+ . emit ( gns , 'CuratorBalanceReturnedToBeneficiary' )
866
+ . withArgs ( l1SubgraphId , me . address , curatorTokens )
867
+ const curatorTokensAfter = await grt . balanceOf ( me . address )
868
+ expect ( curatorTokensAfter ) . eq ( curatorTokensBefore . add ( curatorTokens ) )
869
+ const gnsBalanceAfter = await grt . balanceOf ( gns . address )
870
+ // gatewayFinalizeTransfer will mint the tokens that are sent to the curator,
871
+ // so the GNS balance should be the same
872
+ expect ( gnsBalanceAfter ) . eq ( gnsBalanceBefore )
873
+ } )
874
+
744
875
it ( 'if a subgraph was deprecated after transfer, it returns the tokens to the beneficiary' , async function ( ) {
745
876
const mockL1GNSL2Alias = await getL2SignerFromL1 ( mockL1GNS . address )
746
877
// Eth for gas:
0 commit comments