Skip to content

Commit 329e92f

Browse files
authored
Allow batch transactions using multicall in the GNS (#485)
Add multicall for batched calls to the GNS By using a multicall a user can batch multiple operations into a single transaction. An example of this is if the owner publishing a subgraph decides to mint some signal on that subgraph. * Use convenience token utils for transferring tokens * Reduce bytecode by using an inline function for access * Introduce a multicall abstract contract for batch calls * Add security tests to multicall
1 parent 310326d commit 329e92f

File tree

5 files changed

+154
-31
lines changed

5 files changed

+154
-31
lines changed

contracts/base/IMulticall.sol

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.3;
4+
pragma experimental ABIEncoderV2;
5+
6+
/**
7+
* @title Multicall interface
8+
* @notice Enables calling multiple methods in a single call to the contract
9+
*/
10+
interface IMulticall {
11+
/**
12+
* @notice Call multiple functions in the current contract and return the data from all of them if they all succeed
13+
* @param data The encoded function data for each of the calls to make to this contract
14+
* @return results The results from each of the calls passed in via data
15+
*/
16+
function multicall(bytes[] calldata data) external returns (bytes[] memory results);
17+
}

contracts/base/Multicall.sol

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.3;
4+
pragma experimental ABIEncoderV2;
5+
6+
import "./IMulticall.sol";
7+
8+
// Inspired by https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/base/Multicall.sol
9+
// Note: Removed payable from the multicall
10+
11+
/**
12+
* @title Multicall
13+
* @notice Enables calling multiple methods in a single call to the contract
14+
*/
15+
abstract contract Multicall is IMulticall {
16+
/// @inheritdoc IMulticall
17+
function multicall(bytes[] calldata data) external override returns (bytes[] memory results) {
18+
results = new bytes[](data.length);
19+
for (uint256 i = 0; i < data.length; i++) {
20+
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
21+
22+
if (!success) {
23+
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
24+
if (result.length < 68) revert();
25+
assembly {
26+
result := add(result, 0x04)
27+
}
28+
revert(abi.decode(result, (string)));
29+
}
30+
31+
results[i] = result;
32+
}
33+
}
34+
}

contracts/discovery/GNS.sol

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ pragma experimental ABIEncoderV2;
55

66
import "@openzeppelin/contracts/math/SafeMath.sol";
77

8+
import "../base/Multicall.sol";
89
import "../bancor/BancorFormula.sol";
910
import "../upgrades/GraphUpgradeable.sol";
11+
import "../utils/TokenUtils.sol";
1012

1113
import "./IGNS.sol";
1214
import "./GNSStorage.sol";
@@ -17,10 +19,14 @@ import "./GNSStorage.sol";
1719
* used in the scope of the Graph Network. It translates subgraph names into subgraph versions.
1820
* Each version is associated with a Subgraph Deployment. The contract has no knowledge of
1921
* human-readable names. All human readable names emitted in events.
22+
* The contract implements a multicall behaviour to support batching multiple calls in a single
23+
* transaction.
2024
*/
21-
contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
25+
contract GNS is GNSV1Storage, GraphUpgradeable, IGNS, Multicall {
2226
using SafeMath for uint256;
2327

28+
// -- Constants --
29+
2430
uint256 private constant MAX_UINT256 = 2**256 - 1;
2531

2632
// 100% in parts per million
@@ -136,16 +142,28 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
136142
uint256 withdrawnGRT
137143
);
138144

145+
// -- Modifiers --
146+
139147
/**
140-
@dev Modifier that allows a function to be called by owner of a graph account
141-
@param _graphAccount Address of the graph account
142-
*/
143-
modifier onlyGraphAccountOwner(address _graphAccount) {
148+
* @dev Check if the owner is the graph account
149+
* @param _graphAccount Address of the graph account
150+
*/
151+
function _isGraphAccountOwner(address _graphAccount) private view {
144152
address graphAccountOwner = erc1056Registry.identityOwner(_graphAccount);
145153
require(graphAccountOwner == msg.sender, "GNS: Only graph account owner can call");
154+
}
155+
156+
/**
157+
* @dev Modifier that allows a function to be called by owner of a graph account
158+
* @param _graphAccount Address of the graph account
159+
*/
160+
modifier onlyGraphAccountOwner(address _graphAccount) {
161+
_isGraphAccountOwner(_graphAccount);
146162
_;
147163
}
148164

165+
// -- Functions --
166+
149167
/**
150168
* @dev Initialize this contract.
151169
*/
@@ -408,10 +426,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
408426
);
409427

410428
// Pull tokens from sender
411-
require(
412-
graphToken().transferFrom(msg.sender, address(this), _tokensIn),
413-
"GNS: Cannot transfer tokens to mint n signal"
414-
);
429+
TokenUtils.pullTokens(graphToken(), msg.sender, _tokensIn);
415430

416431
// Get name signal to mint for tokens deposited
417432
(uint256 vSignal, ) = curation().mint(namePool.subgraphDeploymentID, _tokensIn, 0);
@@ -461,11 +476,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
461476
namePool.nSignal = namePool.nSignal.sub(_nSignal);
462477
namePool.curatorNSignal[msg.sender] = namePool.curatorNSignal[msg.sender].sub(_nSignal);
463478

464-
// Return the tokens to the nameCurator
465-
require(
466-
graphToken().transfer(msg.sender, tokens),
467-
"GNS: Error sending nameCurators tokens"
468-
);
479+
// Return the tokens to the curator
480+
TokenUtils.pushTokens(graphToken(), msg.sender, tokens);
469481

470482
emit NSignalBurned(_graphAccount, _subgraphNumber, msg.sender, _nSignal, vSignal, tokens);
471483
}
@@ -482,6 +494,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
482494

483495
// If no nSignal, then no need to burn vSignal
484496
if (namePool.nSignal != 0) {
497+
// Note: No slippage, burn at any cost
485498
namePool.withdrawableGRT = curation().burn(
486499
namePool.subgraphDeploymentID,
487500
namePool.vSignal,
@@ -522,10 +535,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
522535
namePool.nSignal = namePool.nSignal.sub(curatorNSignal);
523536
namePool.withdrawableGRT = namePool.withdrawableGRT.sub(tokensOut);
524537

525-
require(
526-
graphToken().transfer(msg.sender, tokensOut),
527-
"GNS: Error withdrawing tokens for nameCurator"
528-
);
538+
// Return tokens to the curator
539+
TokenUtils.pushTokens(graphToken(), msg.sender, tokensOut);
529540

530541
emit GRTWithdrawn(_graphAccount, _subgraphNumber, msg.sender, curatorNSignal, tokensOut);
531542
}
@@ -567,10 +578,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
567578
uint256 ownerTaxAdjustedUp = totalAdjustedUp.sub(_tokens);
568579

569580
// Get the owner of the subgraph to reimburse the curation tax
570-
require(
571-
graphToken().transferFrom(_owner, address(this), ownerTaxAdjustedUp),
572-
"GNS: Error reimbursing curation tax"
573-
);
581+
TokenUtils.pullTokens(graphToken(), _owner, ownerTaxAdjustedUp);
582+
574583
return totalAdjustedUp;
575584
}
576585

@@ -587,8 +596,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
587596
uint256 _tokensIn
588597
)
589598
public
590-
override
591599
view
600+
override
592601
returns (
593602
uint256,
594603
uint256,
@@ -615,7 +624,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
615624
address _graphAccount,
616625
uint256 _subgraphNumber,
617626
uint256 _nSignalIn
618-
) public override view returns (uint256, uint256) {
627+
) public view override returns (uint256, uint256) {
619628
NameCurationPool storage namePool = nameSignals[_graphAccount][_subgraphNumber];
620629
uint256 vSignal = nSignalToVSignal(_graphAccount, _subgraphNumber, _nSignalIn);
621630
uint256 tokensOut = curation().signalToTokens(namePool.subgraphDeploymentID, vSignal);
@@ -633,7 +642,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
633642
address _graphAccount,
634643
uint256 _subgraphNumber,
635644
uint256 _vSignalIn
636-
) public override view returns (uint256) {
645+
) public view override returns (uint256) {
637646
NameCurationPool storage namePool = nameSignals[_graphAccount][_subgraphNumber];
638647

639648
// Handle initialization by using 1:1 version to name signal
@@ -661,7 +670,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
661670
address _graphAccount,
662671
uint256 _subgraphNumber,
663672
uint256 _nSignalIn
664-
) public override view returns (uint256) {
673+
) public view override returns (uint256) {
665674
NameCurationPool storage namePool = nameSignals[_graphAccount][_subgraphNumber];
666675
return
667676
BancorFormula(bondingCurve).calculateSaleReturn(
@@ -683,7 +692,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
683692
address _graphAccount,
684693
uint256 _subgraphNumber,
685694
address _curator
686-
) public override view returns (uint256) {
695+
) public view override returns (uint256) {
687696
return nameSignals[_graphAccount][_subgraphNumber].curatorNSignal[_curator];
688697
}
689698

@@ -695,8 +704,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS {
695704
*/
696705
function isPublished(address _graphAccount, uint256 _subgraphNumber)
697706
public
698-
override
699707
view
708+
override
700709
returns (bool)
701710
{
702711
return subgraphs[_graphAccount][_subgraphNumber] != 0;

contracts/discovery/GNSStorage.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ pragma experimental ABIEncoderV2;
66
import "../governance/Managed.sol";
77

88
import "./erc1056/IEthereumDIDRegistry.sol";
9-
109
import "./IGNS.sol";
1110

1211
contract GNSV1Storage is Managed {

test/gns.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { expect } from 'chai'
22
import { ethers, ContractTransaction, BigNumber, Event } from 'ethers'
33

44
import { GNS } from '../build/types/GNS'
5-
import { getAccounts, randomHexBytes, Account, toGRT } from './lib/testHelpers'
6-
import { NetworkFixture } from './lib/fixtures'
75
import { GraphToken } from '../build/types/GraphToken'
86
import { Curation } from '../build/types/Curation'
97

8+
import { getAccounts, randomHexBytes, Account, toGRT } from './lib/testHelpers'
9+
import { NetworkFixture } from './lib/fixtures'
1010
import { toBN, formatGRT } from './lib/testHelpers'
1111

1212
interface Subgraph {
@@ -983,4 +983,68 @@ describe('GNS', () => {
983983
await gns.connect(me.signer).mintNSignal(me.address, 1, toGRT('10'), 0)
984984
})
985985
})
986+
987+
describe('batch calls', function () {
988+
it('should publish new subgraph and mint signal in single transaction', async function () {
989+
// Create a subgraph
990+
const tx1 = await gns.populateTransaction.publishNewSubgraph(
991+
me.address,
992+
subgraph0.subgraphDeploymentID,
993+
subgraph0.versionMetadata,
994+
subgraph0.subgraphMetadata,
995+
)
996+
// Curate on the subgraph
997+
const subgraphNumber = await gns.graphAccountSubgraphNumbers(me.address)
998+
const tx2 = await gns.populateTransaction.mintNSignal(
999+
me.address,
1000+
subgraphNumber,
1001+
toGRT('90000'),
1002+
0,
1003+
)
1004+
1005+
// Batch send transaction
1006+
await gns.connect(me.signer).multicall([tx1.data, tx2.data])
1007+
})
1008+
1009+
it('should revert if batching a call to non-authorized function', async function () {
1010+
// Call a forbidden function
1011+
const tx1 = await gns.populateTransaction.setOwnerTaxPercentage(100)
1012+
1013+
// Create a subgraph
1014+
const tx2 = await gns.populateTransaction.publishNewSubgraph(
1015+
me.address,
1016+
subgraph0.subgraphDeploymentID,
1017+
subgraph0.versionMetadata,
1018+
subgraph0.subgraphMetadata,
1019+
)
1020+
1021+
// Batch send transaction
1022+
const tx = gns.connect(me.signer).multicall([tx1.data, tx2.data])
1023+
await expect(tx).revertedWith('Caller must be Controller governor')
1024+
})
1025+
1026+
it('should revert if trying to call a private function', async function () {
1027+
// Craft call a private function
1028+
const hash = ethers.utils.id('_setOwnerTaxPercentage(uint32)')
1029+
const functionHash = hash.slice(0, 10)
1030+
const calldata = ethers.utils.arrayify(
1031+
ethers.utils.defaultAbiCoder.encode(['uint32'], ['100']),
1032+
)
1033+
const bogusPayload = ethers.utils.concat([functionHash, calldata])
1034+
1035+
// Create a subgraph
1036+
const tx2 = await gns.populateTransaction.publishNewSubgraph(
1037+
me.address,
1038+
subgraph0.subgraphDeploymentID,
1039+
subgraph0.versionMetadata,
1040+
subgraph0.subgraphMetadata,
1041+
)
1042+
1043+
// Batch send transaction
1044+
const tx = gns.connect(me.signer).multicall([bogusPayload, tx2.data])
1045+
await expect(tx).revertedWith(
1046+
"function selector was not recognized and there's no fallback function",
1047+
)
1048+
})
1049+
})
9861050
})

0 commit comments

Comments
 (0)