Skip to content

Commit cb34880

Browse files
ArjunBhuptaniabarmatHeikoFischLayneHaberRahul Sethuram
authored
Add Graph-specific Connext contracts (#179)
* Add connext contracts to repo * Add hardcoded indexer addresses to multisigData * Route indexer multisig commitments to custom CTDT * Add custom indexerCTDT - hardcodes interpreters * Indexer single asset interpreter * Indexer free balance interpreter * Move constants to multisig mastercopy + constructor * Send to hardcoded addresses everywhere * Support indexer-specific withdraw * Port withdrawWapper changes to normal CTDT * Use Solidity version ^0.6.4 * Remove DolphinCoin test token The `DolphinCoin` token causes compiler errors because it needs the new (v3.0) OpenZeppelin ERC20 contract to inherit from. Since it is only used for testing, it can safely be removed in this repo. * Replace `isIndexer` with `hasStake` The function `isIndexer` doesn't exist in the `Staking` contract. I assume `hasStake` was meant instead. * Make `masterCopy` address in multisig payable Otherwise the compiler complains when we want to access the "static" multisig variables via their getters, because the multisig contract has a `receive` function. * Get rid of compiler warnings about unused function parameters * CTDTs don't need to inherit from MultisigTransfer anymore because of change to interpreter-based withdrawals. * Reorder static multisig variables * IndexerCTDT needs to inherit from MultisigData * Add node address to static multisig variables * Fix CTDT replacement in multisig * Move `_owners` array from MVM to MultisigData ... because, for the customizations, we'll need to access the owners outside of the MinimumViableMultisig. * Introduce IndexerMultisigTransfer contract ... which is similar to MultisigTransfer, but handles transfers out of a multisig where one party is an indexer (and the other party is the node). Basically, the transfer is redirected to the staking contract, unless the recipient is the node's address. Actual details of the transfer to the staking contract still need to be filled in. * Remove existing versions of Indexer* interpreters We're gonna start with these from scratch... * Fresh start with Indexer* interpreters We start with copies from the regular single/multi asset interpreters. * Reimplement Indexer* interpreters * Fill in details of interaction with staking contract * Indexer channels handle only Graph token * For indexer channels, only allow delegatecalls out of the multisig. * Add lock mechanism for node-indexer multisigs The staking contract can lock/unlock multisigs; a locked node-indexer multisig cannot execute transactions. * Helpers to deploy channel contracts * Correctly format commitments * Switch to using mocked staking contract in test * Add SettleEvent to parrot information in mock * Funding and withdrawal tests passing * Deploy contracts for disputes in channel context * Add MockDispute test fixture * Update connext to 6.6.5 * Remove logs, connect signer to provider * Test app uses single asset interpreter * Use proxy factory for deployment * Hack workaround to unblock dispute tests * Initial app dispute test passing * Add types package * Simplify channel contracts deployment * Remove setup hack, correctly access params from proxy * Indexer-node multisig integration tests passing * Add locking fns to mock staking * Update test utils to allow for non-indexer channels Co-authored-by: Ariel Barmat <[email protected]> Co-authored-by: Heiko Fisch <[email protected]> Co-authored-by: LayneHaber <[email protected]> Co-authored-by: Rahul Sethuram <[email protected]>
1 parent d310d61 commit cb34880

29 files changed

+3254
-631
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ cache/
33
coverage/
44
node_modules/
55
truffle.js
6+
cache/

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"contract": false,
1313
"assert": false
1414
},
15+
"ignorePatterns": ["./cache", "./build"],
1516
"rules": {
1617
// Strict mode
1718
"strict": ["error", "global"],
@@ -25,7 +26,7 @@
2526
"eol-last": ["error", "always"],
2627
"eqeqeq": ["error", "smart"],
2728
"generator-star-spacing": ["error", "before"],
28-
"indent": ["error", 2],
29+
"indent": ["error", 2, { "SwitchCase": 1 }],
2930
"linebreak-style": ["error", "unix"],
3031
"max-len": ["warn", 100, 2],
3132
"no-debugger": "off",

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ node_modules/
55
build/
66
dist/
77

8+
# Ignore buidler stuff
9+
cache/
10+
811
# Coverage tests
912
coverage/
1013

.soliumignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ contracts/openzeppelin
55
contracts/Migrations.sol
66
contracts/MultiSigWallet.sol
77
contracts/Staking.sol
8+
contracts/connext/IndexerMultiAssetInterpreter.sol
9+
contracts/connext/IndexerSingleAssetInterpreter.sol
10+
contracts/connext/IndexerWithdrawInterpreter.sol
11+
contracts/connext/Proxy.sol
12+
contracts/connext/MinimumViableMultisig.sol

buidler.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BuidlerConfig, task, usePlugin } from '@nomiclabs/buidler/config'
22

33
usePlugin('@nomiclabs/buidler-waffle')
44
usePlugin('solidity-coverage')
5+
usePlugin('buidler-typechain')
56

67
// This is a sample Buidler task. To learn how to create your own go to
78
// https://buidler.dev/guides/create-task.html
@@ -44,6 +45,10 @@ const config: BuidlerConfig = {
4445
url: 'http://localhost:8545',
4546
},
4647
},
48+
typechain: {
49+
outDir: './build/typechain/contracts',
50+
target: 'ethers-v4',
51+
},
4752
}
4853

4954
export default config

contracts/Curation.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import "./GraphToken.sol";
66
import "./bancor/BancorFormula.sol";
77
import "@openzeppelin/contracts/math/SafeMath.sol";
88

9-
109
/**
1110
* @title Curation contract
1211
* @dev Allows curators to signal on subgraph deployments that might be relevant to indexers by

contracts/Staking.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import "./GraphToken.sol";
88
import "./libs/Rebates.sol";
99
import "./libs/Stakes.sol";
1010

11-
1211
/**
1312
* @title Staking contract
1413
*/

contracts/connext/IndexerCtdt.sol

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
pragma solidity ^0.6.4;
2+
pragma experimental "ABIEncoderV2";
3+
4+
import "@connext/contracts/src.sol/shared/libs/LibChannelCrypto.sol";
5+
import "@connext/contracts/src.sol/shared/libs/LibCommitment.sol";
6+
import "@connext/contracts/src.sol/adjudicator/ChallengeRegistry.sol";
7+
import "@connext/contracts/src.sol/funding/libs/LibOutcome.sol";
8+
9+
import "./MultisigData.sol";
10+
import "./MinimumViableMultisig.sol";
11+
12+
13+
contract IndexerCtdt is MultisigData {
14+
uint256 constant MAX_UINT256 = 2**256 - 1;
15+
16+
struct FreeBalanceAppState {
17+
address[] tokenAddresses;
18+
// The inner array contains the list of CoinTransfers for a single asset type
19+
// The outer array contains the list of asset balances for respecitve assets
20+
// according to the indexing used in the `tokens` array above
21+
LibOutcome.CoinTransfer[][] balances;
22+
bytes32[] activeApps;
23+
}
24+
25+
struct MultiAssetMultiPartyCoinTransferInterpreterParams {
26+
uint256[] limit;
27+
address[] tokenAddresses;
28+
}
29+
30+
function executeWithdraw(
31+
address, /* interpreterAddress */
32+
bytes32, /* nonce */
33+
bytes memory encodedOutput,
34+
bytes memory encodedParams
35+
) public {
36+
address withdrawInterpreter = MinimumViableMultisig(masterCopy)
37+
.INDEXER_WITHDRAW_INTERPRETER_ADDRESS();
38+
(bool success, ) = withdrawInterpreter.delegatecall(
39+
abi.encodeWithSignature(
40+
"interpretOutcomeAndExecuteEffect(bytes,bytes)",
41+
encodedOutput,
42+
encodedParams
43+
)
44+
);
45+
46+
require(success, "Execution of executeWithdraw failed");
47+
}
48+
49+
function executeEffectOfFreeBalance(
50+
ChallengeRegistry challengeRegistry,
51+
bytes32 freeBalanceAppIdentityHash,
52+
address /* multiAssetMultiPartyCoinTransferInterpreterAddress */
53+
) public {
54+
FreeBalanceAppState memory freeBalanceAppState = abi.decode(
55+
challengeRegistry.getOutcome(freeBalanceAppIdentityHash),
56+
(FreeBalanceAppState)
57+
);
58+
59+
uint256[] memory limits = new uint256[](freeBalanceAppState.tokenAddresses.length);
60+
61+
address multiAssetInterpreter = MinimumViableMultisig(masterCopy)
62+
.INDEXER_MULTI_ASSET_INTERPRETER_ADDRESS();
63+
64+
for (uint256 i = 0; i < freeBalanceAppState.tokenAddresses.length; i++) {
65+
// The transaction's interpreter parameters are determined at the time
66+
// of creation of the free balance; hence we cannot know how much will be
67+
// deposited into it all-time. Relying on the app state is unsafe so
68+
// we just give it full permissions by setting the limit to the max here.
69+
limits[i] = MAX_UINT256;
70+
}
71+
72+
(bool success, ) = multiAssetInterpreter.delegatecall(
73+
abi.encodeWithSignature(
74+
"interpretOutcomeAndExecuteEffect(bytes,bytes)",
75+
abi.encode(freeBalanceAppState.balances),
76+
abi.encode(
77+
MultiAssetMultiPartyCoinTransferInterpreterParams(
78+
limits,
79+
freeBalanceAppState.tokenAddresses
80+
)
81+
)
82+
)
83+
);
84+
85+
require(success, "Execution of executeEffectOfFreeBalance failed");
86+
}
87+
88+
/// @notice Execute a fund transfer for a state channel app in a finalized state
89+
/// @param appIdentityHash AppIdentityHash to be resolved
90+
function executeEffectOfInterpretedAppOutcome(
91+
ChallengeRegistry challengeRegistry,
92+
bytes32 freeBalanceAppIdentityHash,
93+
bytes32 appIdentityHash,
94+
address, /* interpreterAddress */
95+
bytes memory interpreterParams
96+
) public {
97+
bytes32[] memory activeApps = abi
98+
.decode(challengeRegistry.getOutcome(freeBalanceAppIdentityHash), (FreeBalanceAppState))
99+
.activeApps;
100+
101+
bool appIsFunded = false;
102+
103+
address singleAssetInterpreter = MinimumViableMultisig(masterCopy)
104+
.INDEXER_SINGLE_ASSET_INTERPRETER_ADDRESS();
105+
106+
for (uint256 i = 0; i < activeApps.length; i++) {
107+
if (activeApps[i] == appIdentityHash) {
108+
appIsFunded = true;
109+
}
110+
}
111+
112+
require(appIsFunded, "Referenced AppInstance is not funded");
113+
114+
bytes memory outcome = challengeRegistry.getOutcome(appIdentityHash);
115+
116+
(bool success, ) = singleAssetInterpreter.delegatecall(
117+
abi.encodeWithSignature(
118+
"interpretOutcomeAndExecuteEffect(bytes,bytes)",
119+
outcome,
120+
interpreterParams
121+
)
122+
);
123+
124+
require(success, "Execution of executeEffectOfInterpretedAppOutcome failed");
125+
}
126+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
pragma solidity ^0.6.4;
2+
pragma experimental "ABIEncoderV2";
3+
4+
import "@connext/contracts/src.sol/funding/libs/LibOutcome.sol";
5+
import "@connext/contracts/src.sol/funding/Interpreter.sol";
6+
7+
import "./IndexerMultisigTransfer.sol";
8+
9+
10+
/**
11+
* This file is excluded from ethlint/solium because of this issue:
12+
* https://github.com/duaraghav8/Ethlint/issues/261
13+
*/
14+
contract IndexerMultiAssetInterpreter is IndexerMultisigTransfer, Interpreter {
15+
uint256 constant MAX_UINT256 = 2**256 - 1;
16+
17+
struct MultiAssetMultiPartyCoinTransferInterpreterParams {
18+
uint256[] limit;
19+
address[] tokenAddresses;
20+
}
21+
22+
// NOTE: This is useful for writing tests, but is bad practice
23+
// to have in the contract when deploying it. We do not want people
24+
// to send funds to this contract in any scenario.
25+
receive() external payable {}
26+
27+
function interpretOutcomeAndExecuteEffect(
28+
bytes calldata encodedOutcome,
29+
bytes calldata encodedParams
30+
) external override {
31+
MultiAssetMultiPartyCoinTransferInterpreterParams memory params = abi.decode(
32+
encodedParams,
33+
(MultiAssetMultiPartyCoinTransferInterpreterParams)
34+
);
35+
36+
LibOutcome.CoinTransfer[][] memory coinTransferListOfLists = abi.decode(
37+
encodedOutcome,
38+
(LibOutcome.CoinTransfer[][])
39+
);
40+
41+
for (uint256 i = 0; i < coinTransferListOfLists.length; i++) {
42+
address tokenAddress = params.tokenAddresses[i];
43+
uint256 limitRemaining = params.limit[i];
44+
LibOutcome.CoinTransfer[] memory coinTransferList = coinTransferListOfLists[i];
45+
46+
for (uint256 j = 0; j < coinTransferList.length; j++) {
47+
LibOutcome.CoinTransfer memory coinTransfer = coinTransferList[j];
48+
49+
address payable to = address(uint160(coinTransfer.to));
50+
51+
if (coinTransfer.amount > 0) {
52+
limitRemaining -= coinTransfer.amount;
53+
multisigTransfer(to, tokenAddress, coinTransfer.amount);
54+
}
55+
}
56+
57+
// NOTE: If the limit is MAX_UINT256 it can bypass this check.
58+
// We do this for the FreeBalanceApp since its values change.
59+
if (params.limit[i] != MAX_UINT256) {
60+
require(
61+
limitRemaining == 0,
62+
"Sum of total amounts received from outcome did not equate to limits."
63+
);
64+
}
65+
}
66+
}
67+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
pragma solidity ^0.6.4;
2+
pragma experimental ABIEncoderV2;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
6+
import "../Staking.sol";
7+
8+
import "./MultisigData.sol";
9+
import "./MinimumViableMultisig.sol";
10+
11+
12+
/// @title IndexerMultisigTransfer - Indexer variant
13+
/// of the regular MultisigTransfer.
14+
contract IndexerMultisigTransfer is MultisigData {
15+
/// @notice Use this function for transfers of assets out of
16+
/// the multisig.
17+
/// @param recipient the recipient of the transfer -- unless this is the node's
18+
/// address, the funds will be transferred to the staking contract on the indexer's behalf
19+
/// @param assetId the asset to be transferred; must be the Graph token
20+
/// @param amount the amount to be transferred
21+
22+
function multisigTransfer(
23+
address payable recipient,
24+
address assetId,
25+
uint256 amount
26+
) public {
27+
address staking = MinimumViableMultisig(masterCopy).INDEXER_STAKING_ADDRESS();
28+
address token = address(Staking(staking).token());
29+
require(staking != address(0), "multisigTransfer");
30+
31+
if (assetId != token) {
32+
return;
33+
}
34+
35+
// // Note, explicitly do NOT use safemath here. See discussion in: TODO
36+
totalAmountWithdrawn[assetId] += amount;
37+
38+
if (recipient == MinimumViableMultisig(masterCopy).NODE_ADDRESS()) {
39+
IERC20(token).transfer(MinimumViableMultisig(masterCopy).NODE_ADDRESS(), amount);
40+
} else {
41+
// transfer to staking contract
42+
require(
43+
IERC20(token).approve(staking, amount),
44+
"IndexerMultisigTransfer: approving tokens to staking contract failed"
45+
);
46+
Staking(staking).settle(amount);
47+
}
48+
}
49+
50+
function getNodeAndIndexer() public view returns (address, address) {
51+
if (_owners[0] == MinimumViableMultisig(masterCopy).NODE_ADDRESS()) {
52+
return (_owners[0], _owners[1]);
53+
} else {
54+
return (_owners[1], _owners[0]);
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)