├── .env.sample ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .graphclientrc.yml ├── .openzeppelin ├── goerli.json ├── mainnet.json ├── unknown-11155111.json ├── unknown-42161.json ├── unknown-421613.json └── unknown-421614.json ├── .prettierignore ├── .prettierrc.json ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── DEPLOYMENT.md ├── LICENSE.md ├── README.md ├── abi ├── TokenLockWalletABIFull.json └── TokenLockWalletABIRemix.json ├── audits └── 2020-11-graph-token-distribution.pdf ├── contracts ├── GraphTokenDistributor.sol ├── GraphTokenLock.sol ├── GraphTokenLockManager.sol ├── GraphTokenLockSimple.sol ├── GraphTokenLockWallet.sol ├── ICallhookReceiver.sol ├── IGraphTokenLock.sol ├── IGraphTokenLockManager.sol ├── L1GraphTokenLockTransferTool.sol ├── L2GraphTokenLockManager.sol ├── L2GraphTokenLockTransferTool.sol ├── L2GraphTokenLockWallet.sol ├── MathUtils.sol ├── MinimalProxyFactory.sol ├── Ownable.sol ├── arbitrum │ └── ITokenGateway.sol └── tests │ ├── BridgeMock.sol │ ├── GraphTokenMock.sol │ ├── InboxMock.sol │ ├── L1TokenGatewayMock.sol │ ├── L2TokenGatewayMock.sol │ ├── Stakes.sol │ ├── StakingMock.sol │ ├── WalletMock.sol │ └── arbitrum │ ├── AddressAliasHelper.sol │ ├── IBridge.sol │ ├── IInbox.sol │ └── IMessageProvider.sol ├── deploy ├── 1_test.ts ├── 2_l1_manager_wallet.ts ├── 3_l2_wallet.ts ├── 4_l1_transfer_tool.ts ├── 5_l2_manager.ts ├── 6_l2_transfer_tool.ts └── lib │ └── utils.ts ├── deployments ├── arbitrum-goerli │ ├── .chainId │ ├── L2GraphTokenLockManager-Testnet.json │ ├── L2GraphTokenLockTransferTool.json │ ├── L2GraphTokenLockWallet.json │ └── solcInputs │ │ └── b5cdad58099d39cd1aed000b2fd864d8.json ├── arbitrum-one │ ├── .chainId │ ├── L2GraphTokenLockManager-Foundation-v1.json │ ├── L2GraphTokenLockManager-MIPs.json │ ├── L2GraphTokenLockManager.json │ ├── L2GraphTokenLockTransferTool.json │ ├── L2GraphTokenLockWallet.json │ └── solcInputs │ │ └── b5cdad58099d39cd1aed000b2fd864d8.json ├── arbitrum-sepolia │ ├── .chainId │ ├── L2GraphTokenLockManager.json │ ├── L2GraphTokenLockTransferTool.json │ ├── L2GraphTokenLockWallet.json │ └── solcInputs │ │ └── 095bd30babc75057be19228ca1fd7aa4.json ├── goerli │ ├── .chainId │ ├── GraphTokenLockManager-Testnet.json │ ├── GraphTokenLockManager.json │ ├── GraphTokenLockWallet-Testnet.json │ ├── GraphTokenLockWallet.json │ ├── L1GraphTokenLockTransferTool.json │ └── solcInputs │ │ ├── 3c1e469b4f9ba208577ab7c230900006.json │ │ └── b5cdad58099d39cd1aed000b2fd864d8.json ├── mainnet │ ├── .chainId │ ├── GraphTokenLockManager-Foundation.json │ ├── GraphTokenLockManager-MIPs.json │ ├── GraphTokenLockManager-Migrations.json │ ├── GraphTokenLockManager.json │ ├── GraphTokenLockWallet-Foundation.json │ ├── GraphTokenLockWallet-MIPs.json │ ├── GraphTokenLockWallet-Migrations.json │ ├── GraphTokenLockWallet.json │ ├── L1GraphTokenLockTransferTool.json │ └── solcInputs │ │ ├── 5ad03e035f8e3c63878532d87a315ef8.json │ │ ├── 6f5e8f450f52dd96ebb796aa6620fee9.json │ │ ├── a72ab6278ade6c5c10115f7be2c555c9.json │ │ ├── b5cdad58099d39cd1aed000b2fd864d8.json │ │ └── f0757d7c1c560a6ae9697525709a3f5b.json ├── rinkeby │ ├── .chainId │ ├── GraphTokenLockManager.json │ ├── GraphTokenLockWallet.json │ ├── GraphTokenMock.json │ └── solcInputs │ │ └── a72ab6278ade6c5c10115f7be2c555c9.json └── sepolia │ ├── .chainId │ ├── GraphTokenLockManager.json │ ├── GraphTokenLockWallet.json │ ├── L1GraphTokenLockTransferTool.json │ └── solcInputs │ └── 095bd30babc75057be19228ca1fd7aa4.json ├── hardhat.config.ts ├── ops ├── beneficiary.ts ├── create.ts ├── delete.ts ├── deploy-data.csv ├── info.ts ├── manager.ts ├── queries │ ├── account.graphql │ ├── curators.graphql │ ├── network.graphql │ └── tokenLockWallets.graphql ├── results.csv ├── tx-builder-template.json └── tx-builder.ts ├── package.json ├── scripts ├── build ├── coverage ├── flatten ├── prepublish ├── security └── test ├── test ├── config.ts ├── distributor.test.ts ├── l1TokenLockTransferTool.test.ts ├── l2TokenLockManager.test.ts ├── l2TokenLockTransferTool.test.ts ├── network.ts ├── tokenLock.test.ts └── tokenLockWallet.test.ts ├── tsconfig.json └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | MNEMONIC= 2 | ETHERSCAN_API_KEY= 3 | INFURA_KEY= 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | cache/ 3 | dist/ 4 | node_modules/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module" 6 | }, 7 | "extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], 8 | "rules": { 9 | "prefer-const": "warn", 10 | "no-extra-semi": "off", 11 | "@typescript-eslint/no-extra-semi": "warn", 12 | "@typescript-eslint/no-inferrable-types": "warn", 13 | "@typescript-eslint/no-empty-function": "warn" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: {} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - shell: bash 26 | env: 27 | NPM_TOKEN: ${{secrets.npm_token}} 28 | run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > ~/.npmrc 29 | - run: yarn cache clean 30 | - run: yarn install --frozen-lockfile 31 | - run: yarn run build 32 | - run: yarn test 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore node stuff 2 | node_modules/ 3 | yarn-error.log 4 | 5 | # Ignore build stuff 6 | cache/ 7 | build/ 8 | dist/ 9 | deployments/hardhat/ 10 | 11 | # Hardhat cache 12 | cached/ 13 | 14 | # Ignore solc bin output 15 | bin/ 16 | 17 | # Others 18 | .env 19 | .DS_Store 20 | .vscode 21 | 22 | # Reports 23 | /reports 24 | 25 | 26 | .graphclient 27 | 28 | # Ignore tx-builder files 29 | tx-builder-*.json 30 | !tx-builder-template.json 31 | -------------------------------------------------------------------------------- /.graphclientrc.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | - name: graph-network 3 | handler: 4 | graphql: 5 | endpoint: https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet 6 | retry: 5 7 | 8 | - name: token-distribution 9 | handler: 10 | graphql: 11 | endpoint: https://api.thegraph.com/subgraphs/name/graphprotocol/token-distribution 12 | retry: 5 13 | transforms: 14 | - autoPagination: 15 | validateSchema: true 16 | 17 | documents: 18 | - ops/queries/account.graphql 19 | - ops/queries/curators.graphql 20 | - ops/queries/network.graphql 21 | - ops/queries/tokenLockWallets.graphql 22 | -------------------------------------------------------------------------------- /.openzeppelin/goerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": "3.2", 3 | "admin": { 4 | "address": "0x4e8EC4059854d0634d7Ba04eA41B453cD56Afa5d", 5 | "txHash": "0x47c8a0973f2457fda3f1243238d3b590cb78e6e78f2cd1040d97e94ced6834c4" 6 | }, 7 | "proxies": [ 8 | { 9 | "address": "0xa725CF32c367778CFF2ba7089Ab4e941BDD88612", 10 | "txHash": "0x7297670fbbf9f1c014aac93fa0219522c079bdd0ad4bb16c75a204ba97b1bc81", 11 | "kind": "transparent" 12 | } 13 | ], 14 | "impls": { 15 | "483bf2556291169f0be3d109cdfe83a3493b0312de77748f3926fc7426444bb5": { 16 | "address": "0xF546bF936241571C380272bbf169D66D4184390c", 17 | "txHash": "0xa276937f5f5913524ba900fa100509bfb3428e98e74f9f76345337c408c2614b", 18 | "layout": { 19 | "solcVersion": "0.7.3", 20 | "storage": [ 21 | { 22 | "label": "_owner", 23 | "offset": 0, 24 | "slot": "0", 25 | "type": "t_address", 26 | "contract": "Ownable", 27 | "src": "contracts/Ownable.sol:19" 28 | }, 29 | { 30 | "label": "__gap", 31 | "offset": 0, 32 | "slot": "1", 33 | "type": "t_array(t_uint256)50_storage", 34 | "contract": "Ownable", 35 | "src": "contracts/Ownable.sol:22" 36 | }, 37 | { 38 | "label": "_initialized", 39 | "offset": 0, 40 | "slot": "51", 41 | "type": "t_bool", 42 | "contract": "Initializable", 43 | "src": "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol:25" 44 | }, 45 | { 46 | "label": "_initializing", 47 | "offset": 1, 48 | "slot": "51", 49 | "type": "t_bool", 50 | "contract": "Initializable", 51 | "src": "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol:30" 52 | }, 53 | { 54 | "label": "l2LockManager", 55 | "offset": 0, 56 | "slot": "52", 57 | "type": "t_mapping(t_address,t_address)", 58 | "contract": "L1GraphTokenLockTransferTool", 59 | "src": "contracts/L1GraphTokenLockTransferTool.sol:50" 60 | }, 61 | { 62 | "label": "l2WalletOwner", 63 | "offset": 0, 64 | "slot": "53", 65 | "type": "t_mapping(t_address,t_address)", 66 | "contract": "L1GraphTokenLockTransferTool", 67 | "src": "contracts/L1GraphTokenLockTransferTool.sol:53" 68 | }, 69 | { 70 | "label": "l2WalletAddress", 71 | "offset": 0, 72 | "slot": "54", 73 | "type": "t_mapping(t_address,t_address)", 74 | "contract": "L1GraphTokenLockTransferTool", 75 | "src": "contracts/L1GraphTokenLockTransferTool.sol:56" 76 | }, 77 | { 78 | "label": "tokenLockETHBalances", 79 | "offset": 0, 80 | "slot": "55", 81 | "type": "t_mapping(t_address,t_uint256)", 82 | "contract": "L1GraphTokenLockTransferTool", 83 | "src": "contracts/L1GraphTokenLockTransferTool.sol:59" 84 | }, 85 | { 86 | "label": "l2Beneficiary", 87 | "offset": 0, 88 | "slot": "56", 89 | "type": "t_mapping(t_address,t_address)", 90 | "contract": "L1GraphTokenLockTransferTool", 91 | "src": "contracts/L1GraphTokenLockTransferTool.sol:62" 92 | }, 93 | { 94 | "label": "l2WalletAddressSetManually", 95 | "offset": 0, 96 | "slot": "57", 97 | "type": "t_mapping(t_address,t_bool)", 98 | "contract": "L1GraphTokenLockTransferTool", 99 | "src": "contracts/L1GraphTokenLockTransferTool.sol:66" 100 | } 101 | ], 102 | "types": { 103 | "t_address": { 104 | "label": "address", 105 | "numberOfBytes": "20" 106 | }, 107 | "t_array(t_uint256)50_storage": { 108 | "label": "uint256[50]", 109 | "numberOfBytes": "1600" 110 | }, 111 | "t_bool": { 112 | "label": "bool", 113 | "numberOfBytes": "1" 114 | }, 115 | "t_mapping(t_address,t_address)": { 116 | "label": "mapping(address => address)", 117 | "numberOfBytes": "32" 118 | }, 119 | "t_mapping(t_address,t_bool)": { 120 | "label": "mapping(address => bool)", 121 | "numberOfBytes": "32" 122 | }, 123 | "t_mapping(t_address,t_uint256)": { 124 | "label": "mapping(address => uint256)", 125 | "numberOfBytes": "32" 126 | }, 127 | "t_uint256": { 128 | "label": "uint256", 129 | "numberOfBytes": "32" 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /.openzeppelin/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": "3.2", 3 | "admin": { 4 | "address": "0x79E321CB828A0D1435050a1ce8d7985C4C3dfFFA", 5 | "txHash": "0x7ed23b9530cd24071011637e782f43e942b2e0206042a67f5efe3affddbbc9c8" 6 | }, 7 | "proxies": [ 8 | { 9 | "address": "0xCa82c7Ce3388b0B5d307574099aC57d7a00d509F", 10 | "txHash": "0x8535f77828c04d09f10107dea149d9ad72b477a386fd482d109d456e487667e0", 11 | "kind": "transparent" 12 | } 13 | ], 14 | "impls": { 15 | "81b972d1a45d53657aeda6174118a103da6ec0b7e74535dd83748f6618297656": { 16 | "address": "0x6a2A9bAd7b9Fa6ecEE8f249a0850f47eE184a275", 17 | "txHash": "0x5c0dea018da76739bc39d324588a3fdd87b35c86a28469eb1442f701af0a7a82", 18 | "layout": { 19 | "solcVersion": "0.7.3", 20 | "storage": [ 21 | { 22 | "label": "_owner", 23 | "offset": 0, 24 | "slot": "0", 25 | "type": "t_address", 26 | "contract": "Ownable", 27 | "src": "contracts/Ownable.sol:19" 28 | }, 29 | { 30 | "label": "__gap", 31 | "offset": 0, 32 | "slot": "1", 33 | "type": "t_array(t_uint256)50_storage", 34 | "contract": "Ownable", 35 | "src": "contracts/Ownable.sol:22" 36 | }, 37 | { 38 | "label": "_initialized", 39 | "offset": 0, 40 | "slot": "51", 41 | "type": "t_bool", 42 | "contract": "Initializable", 43 | "src": "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol:25" 44 | }, 45 | { 46 | "label": "_initializing", 47 | "offset": 1, 48 | "slot": "51", 49 | "type": "t_bool", 50 | "contract": "Initializable", 51 | "src": "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol:30" 52 | }, 53 | { 54 | "label": "l2LockManager", 55 | "offset": 0, 56 | "slot": "52", 57 | "type": "t_mapping(t_address,t_address)", 58 | "contract": "L1GraphTokenLockTransferTool", 59 | "src": "contracts/L1GraphTokenLockTransferTool.sol:50" 60 | }, 61 | { 62 | "label": "l2WalletOwner", 63 | "offset": 0, 64 | "slot": "53", 65 | "type": "t_mapping(t_address,t_address)", 66 | "contract": "L1GraphTokenLockTransferTool", 67 | "src": "contracts/L1GraphTokenLockTransferTool.sol:53" 68 | }, 69 | { 70 | "label": "l2WalletAddress", 71 | "offset": 0, 72 | "slot": "54", 73 | "type": "t_mapping(t_address,t_address)", 74 | "contract": "L1GraphTokenLockTransferTool", 75 | "src": "contracts/L1GraphTokenLockTransferTool.sol:56" 76 | }, 77 | { 78 | "label": "tokenLockETHBalances", 79 | "offset": 0, 80 | "slot": "55", 81 | "type": "t_mapping(t_address,t_uint256)", 82 | "contract": "L1GraphTokenLockTransferTool", 83 | "src": "contracts/L1GraphTokenLockTransferTool.sol:59" 84 | }, 85 | { 86 | "label": "l2Beneficiary", 87 | "offset": 0, 88 | "slot": "56", 89 | "type": "t_mapping(t_address,t_address)", 90 | "contract": "L1GraphTokenLockTransferTool", 91 | "src": "contracts/L1GraphTokenLockTransferTool.sol:62" 92 | }, 93 | { 94 | "label": "l2WalletAddressSetManually", 95 | "offset": 0, 96 | "slot": "57", 97 | "type": "t_mapping(t_address,t_bool)", 98 | "contract": "L1GraphTokenLockTransferTool", 99 | "src": "contracts/L1GraphTokenLockTransferTool.sol:66" 100 | } 101 | ], 102 | "types": { 103 | "t_address": { 104 | "label": "address", 105 | "numberOfBytes": "20" 106 | }, 107 | "t_array(t_uint256)50_storage": { 108 | "label": "uint256[50]", 109 | "numberOfBytes": "1600" 110 | }, 111 | "t_bool": { 112 | "label": "bool", 113 | "numberOfBytes": "1" 114 | }, 115 | "t_mapping(t_address,t_address)": { 116 | "label": "mapping(address => address)", 117 | "numberOfBytes": "32" 118 | }, 119 | "t_mapping(t_address,t_bool)": { 120 | "label": "mapping(address => bool)", 121 | "numberOfBytes": "32" 122 | }, 123 | "t_mapping(t_address,t_uint256)": { 124 | "label": "mapping(address => uint256)", 125 | "numberOfBytes": "32" 126 | }, 127 | "t_uint256": { 128 | "label": "uint256", 129 | "numberOfBytes": "32" 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /.openzeppelin/unknown-11155111.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": "3.2", 3 | "admin": { 4 | "address": "0xBbfAaA4A754931757B925AC0DDCcf97EB08e2895", 5 | "txHash": "0x39364dc94694446616735d46554b5b5a937a7ec904deffee7aec9e9213c6fc12" 6 | }, 7 | "proxies": [ 8 | { 9 | "address": "0x543F8BFFb65c46091B4eEF4b1c394dFa43C4b065", 10 | "txHash": "0x73d09cc6f92b3c97de26d3049db72a41249e0772d45c24c3818bce3344de8070", 11 | "kind": "transparent" 12 | } 13 | ], 14 | "impls": { 15 | "cd6b1adc749eb68d0f47e7a8ff7b7c183afb072ac1de568f0c8b991a4e6388d0": { 16 | "address": "0xDB47924Ad61D5C64D7921E9D0cb049Fb404A2DFB", 17 | "txHash": "0xcab1656794d9a5e19e396214c5a4a687dd7e1b48197237d6778001e5510bd70c", 18 | "layout": { 19 | "solcVersion": "0.7.3", 20 | "storage": [ 21 | { 22 | "label": "_owner", 23 | "offset": 0, 24 | "slot": "0", 25 | "type": "t_address", 26 | "contract": "Ownable", 27 | "src": "contracts/Ownable.sol:19" 28 | }, 29 | { 30 | "label": "__gap", 31 | "offset": 0, 32 | "slot": "1", 33 | "type": "t_array(t_uint256)50_storage", 34 | "contract": "Ownable", 35 | "src": "contracts/Ownable.sol:22" 36 | }, 37 | { 38 | "label": "_initialized", 39 | "offset": 0, 40 | "slot": "51", 41 | "type": "t_bool", 42 | "contract": "Initializable", 43 | "src": "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol:25" 44 | }, 45 | { 46 | "label": "_initializing", 47 | "offset": 1, 48 | "slot": "51", 49 | "type": "t_bool", 50 | "contract": "Initializable", 51 | "src": "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol:30" 52 | }, 53 | { 54 | "label": "l2LockManager", 55 | "offset": 0, 56 | "slot": "52", 57 | "type": "t_mapping(t_address,t_address)", 58 | "contract": "L1GraphTokenLockTransferTool", 59 | "src": "contracts/L1GraphTokenLockTransferTool.sol:50" 60 | }, 61 | { 62 | "label": "l2WalletOwner", 63 | "offset": 0, 64 | "slot": "53", 65 | "type": "t_mapping(t_address,t_address)", 66 | "contract": "L1GraphTokenLockTransferTool", 67 | "src": "contracts/L1GraphTokenLockTransferTool.sol:53" 68 | }, 69 | { 70 | "label": "l2WalletAddress", 71 | "offset": 0, 72 | "slot": "54", 73 | "type": "t_mapping(t_address,t_address)", 74 | "contract": "L1GraphTokenLockTransferTool", 75 | "src": "contracts/L1GraphTokenLockTransferTool.sol:56" 76 | }, 77 | { 78 | "label": "tokenLockETHBalances", 79 | "offset": 0, 80 | "slot": "55", 81 | "type": "t_mapping(t_address,t_uint256)", 82 | "contract": "L1GraphTokenLockTransferTool", 83 | "src": "contracts/L1GraphTokenLockTransferTool.sol:59" 84 | }, 85 | { 86 | "label": "l2Beneficiary", 87 | "offset": 0, 88 | "slot": "56", 89 | "type": "t_mapping(t_address,t_address)", 90 | "contract": "L1GraphTokenLockTransferTool", 91 | "src": "contracts/L1GraphTokenLockTransferTool.sol:62" 92 | }, 93 | { 94 | "label": "l2WalletAddressSetManually", 95 | "offset": 0, 96 | "slot": "57", 97 | "type": "t_mapping(t_address,t_bool)", 98 | "contract": "L1GraphTokenLockTransferTool", 99 | "src": "contracts/L1GraphTokenLockTransferTool.sol:66" 100 | } 101 | ], 102 | "types": { 103 | "t_address": { 104 | "label": "address", 105 | "numberOfBytes": "20" 106 | }, 107 | "t_array(t_uint256)50_storage": { 108 | "label": "uint256[50]", 109 | "numberOfBytes": "1600" 110 | }, 111 | "t_bool": { 112 | "label": "bool", 113 | "numberOfBytes": "1" 114 | }, 115 | "t_mapping(t_address,t_address)": { 116 | "label": "mapping(address => address)", 117 | "numberOfBytes": "32" 118 | }, 119 | "t_mapping(t_address,t_bool)": { 120 | "label": "mapping(address => bool)", 121 | "numberOfBytes": "32" 122 | }, 123 | "t_mapping(t_address,t_uint256)": { 124 | "label": "mapping(address => uint256)", 125 | "numberOfBytes": "32" 126 | }, 127 | "t_uint256": { 128 | "label": "uint256", 129 | "numberOfBytes": "32" 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /.openzeppelin/unknown-42161.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": "3.2", 3 | "admin": { 4 | "address": "0x69E5E6aae945d342d6FA17D112C137D18E52C4Af", 5 | "txHash": "0x00dcd23252a58b8304af1019f0d34dfefee1199c4b809acbc382d13615aee939" 6 | }, 7 | "proxies": [ 8 | { 9 | "address": "0x23C9c8575E6bA0349a497b6D0E8F0b9239e68028", 10 | "txHash": "0xecb5b61a0d6fbca8f01174fea87d34172d4321650ba0566b0a9c87c7eca8df73", 11 | "kind": "transparent" 12 | } 13 | ], 14 | "impls": { 15 | "3dff628cbc1a793190dc5ae0bc979ad427a9a98001209a3664d22dafc301b9b1": { 16 | "address": "0x440e07acE09d848a581077c6DC8D7fb60FD8af62", 17 | "txHash": "0x19f4d4f2701765d612f8ec4d7703a3d84761c4d81f3822addea9ba7ed023e0b3", 18 | "layout": { 19 | "solcVersion": "0.7.3", 20 | "storage": [], 21 | "types": {} 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.openzeppelin/unknown-421613.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": "3.2", 3 | "admin": { 4 | "address": "0x5de859Bfd66BC330950c7dbf77F7F8fE519a1834", 5 | "txHash": "0xbc443c4f31a36a6ff2cb5a8f39d9debd530626f9f51a9acccf28db6cfb6e94e0" 6 | }, 7 | "proxies": [ 8 | { 9 | "address": "0xc1A9C2E76171e64Cd5669B3E89D9A25a6b0FAfB7", 10 | "txHash": "0x4c0fdb3290d0e247de1d0863bc2a7b13ea9414a86e5bfe94f1e2eba7c5c47f70", 11 | "kind": "transparent" 12 | } 13 | ], 14 | "impls": { 15 | "f6ef37a54af859f9b6df6f1932063a6236e85e4e6ef0d8873a358db6ba412cb5": { 16 | "address": "0xDc95A3418B4869c09572141Db70344434C8Bd9a8", 17 | "txHash": "0x4b3258ad891966098856994fb4b6fde4a156d275d3ad813daa4930b9fbcf7861", 18 | "layout": { 19 | "solcVersion": "0.7.3", 20 | "storage": [], 21 | "types": {} 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.openzeppelin/unknown-421614.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": "3.2", 3 | "admin": { 4 | "address": "0xaf06340Afd383c81C0F025806f93e613Bf6229b2", 5 | "txHash": "0x9cae0d327af86d32826d828ce26eb50c3d7c1138a6591ceaebb2de5accef1fe5" 6 | }, 7 | "proxies": [ 8 | { 9 | "address": "0xe21cd62E1E0CD68476C47F518980226C0a05fB19", 10 | "txHash": "0x4785cb6bfeae00d727ed1199ad2724772507d6631135c73797069382a58af7d3", 11 | "kind": "transparent" 12 | } 13 | ], 14 | "impls": { 15 | "9473208ed72647e49559194b7e906bf5eeb9ed0daf880eefb020b9493100d856": { 16 | "address": "0x435F557d1fa367CAF33B567589F22a911Be28957", 17 | "txHash": "0x321a11019d5a3d8fbdf1ba435b4a7a9c1961f3a6178c8deb2aaaeaeae7515775", 18 | "layout": { 19 | "solcVersion": "0.7.3", 20 | "storage": [], 21 | "types": {} 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphprotocol/token-distribution/6c9e580c961d7e97c37e9f94162eda24a305f3f1/.prettierignore -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "useTabs": false, 4 | "bracketSpacing": true, 5 | "overrides": [ 6 | { 7 | "files": "*.js", 8 | "options": { 9 | "semi": false, 10 | "trailingComma": "all", 11 | "tabWidth": 2, 12 | "singleQuote": true, 13 | "explicitTypes": "always" 14 | } 15 | }, 16 | { 17 | "files": "*.ts", 18 | "options": { 19 | "semi": false, 20 | "trailingComma": "all", 21 | "tabWidth": 2, 22 | "singleQuote": true, 23 | "explicitTypes": "always" 24 | } 25 | }, 26 | { 27 | "files": "*.sol", 28 | "options": { 29 | "tabWidth": 4, 30 | "singleQuote": false, 31 | "explicitTypes": "always" 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | const skipFiles = [''] 2 | 3 | module.exports = { 4 | providerOptions: { 5 | mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect', 6 | network_id: 1337, 7 | }, 8 | skipFiles, 9 | istanbulFolder: './reports/coverage', 10 | } 11 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "func-visibility": ["warn", { "ignoreConstructors": true }], 7 | "compiler-version": ["off"], 8 | "constructor-syntax": "warn", 9 | "quotes": ["error", "double"], 10 | "reason-string": ["off"], 11 | "not-rely-on-time": "off", 12 | "no-empty-blocks": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Deploy a TokenManager (L1) 4 | 5 | The following instructions are for testnet (goerli), use `--network mainnet` to deploy to Mainnet. 6 | 7 | ### 1. Deploy a Token Manager contract 8 | 9 | During this process the master copy of the GraphTokenLockWallet will be deployed and used in the Manager. 10 | 11 | ``` 12 | npx hardhat deploy --tags manager --network goerli 13 | ``` 14 | 15 | ### 2. Fund the manager with the amount we need to deploy contracts 16 | 17 | The task will convert the amount passed in GRT to wei before calling the contracts. 18 | 19 | ``` 20 | npx hardhat manager-deposit --amount --network goerli 21 | ``` 22 | 23 | ### 3. Deploy a number of Token Lock contracts using the Manager 24 | 25 | The process to set up the CSV file is described in the [README](./README.md). 26 | 27 | ``` 28 | npx hardhat create-token-locks --deploy-file --result-file --owner-address --network goerli 29 | ``` 30 | 31 | ### 4. Setup the Token Manager to allow default protocol functions 32 | 33 | ``` 34 | npx hardhat manager-setup-auth --target-address --network goerli 35 | ``` 36 | 37 | ## Deploying the manager, wallet and transfer tools to L2 38 | 39 | This assumes a manager and token lock have already been deployed in L1 (and potentially many managers and token locks). 40 | 41 | The following instructions are for testnet (goerli and Arbitrum goerli), use `--network mainnet` to deploy to Mainnet and `--network arbitrum-one` for Arbitrum One. 42 | 43 | ### 1. Deploy the L2GraphTokenLockWallet master copy 44 | 45 | Keep in mind you might want to use a different mnemonic in `.env` for the L2 deployer. Note that each transfer tool in L1 will only support a single wallet implementation in L2, so if you deploy several L2 managers, make sure all of them use the same wallet master copy in L2. 46 | 47 | ``` 48 | npx hardhat deploy --tags l2-wallet --network arbitrum-goerli 49 | ``` 50 | 51 | ### 2. Deploy the L1GraphTokenLockTransferTool 52 | 53 | You will be prompted for a few relevant addresses, including the Staking contract and the address of the L2GraphTokenLockWallet implementation that you just deployed. 54 | 55 | Note the transfer tool is upgradeable (uses an OZ transparent proxy). 56 | 57 | ``` 58 | npx hardhat deploy --tags l1-transfer-tool --network goerli 59 | ``` 60 | 61 | ### 3. Deploy the L2GraphTokenLockManager for each L1 manager 62 | 63 | Note this will not ask you for the L1 manager address, it is set separately in the transfer tool contract. 64 | 65 | You can optionally fund the L2 manager if you'd like to also create L2-native vesting contracts with it. 66 | 67 | ``` 68 | npx hardhat deploy --tags l2-manager --network arbitrum-goerli 69 | ``` 70 | 71 | ### 4. Deploy the L2GraphTokenLockTransferTool 72 | 73 | Note the transfer tool is upgradeable (uses an OZ transparent proxy). 74 | 75 | ``` 76 | npx hardhat deploy --tags l2-transfer-tool --network arbitrum-goerli 77 | ``` 78 | 79 | ### 5. Set the L2 owners and manager addresses 80 | 81 | Each token lock has an owner, that may map to a different address in L2. So we need to set the owner address in the L1GraphTokenLockTransferTool. 82 | 83 | This is done using a hardhat console on L1, i.e. `npx hardhat console --network goerli`: 84 | 85 | ```javascript 86 | transferToolAddress = '' 87 | l1Owner = '' 88 | l2Owner = '' 89 | deployer = (await hre.ethers.getSigners())[0] 90 | transferTool = await hre.ethers.getContractAt('L1GraphTokenLockTransferTool', transferToolAddress) 91 | await transferTool.connect(deployer).setL2WalletOwner(l1Owner, l2Owner) 92 | // Repeat for each owner... 93 | ``` 94 | 95 | After doing this for each owner, you must also set the L2GraphTokenLockManager address that corresponds to each L1 manager: 96 | 97 | ```javascript 98 | transferToolAddress = '' 99 | l1Manager = '' 100 | l2Manager = '' 101 | deployer = (await hre.ethers.getSigners())[0] 102 | transferTool = await hre.ethers.getContractAt('L1GraphTokenLockTransferTool', transferToolAddress) 103 | await transferTool.connect(deployer).setL2LockManager(l1Manager, l2Manager) 104 | // Repeat for each manager... 105 | ``` 106 | 107 | ### 6. Configure the new authorized functions in L1 108 | 109 | The addition of the L1 transfer tool means adding a new authorized contract and functions in the L1 manager's allowlist. For each manager, we need to add a new token destination (the L1 transfer tool) and the corresponding functions. This assumes the deployer is also the manager owner, if that's not the case, use the correct signer: 110 | 111 | ```javascript 112 | transferToolAddress = '' 113 | stakingAddress = ' 114 | l1Manager = '' 115 | deployer = (await hre.ethers.getSigners())[0] 116 | tokenLockManager = await hre.ethers.getContractAt('GraphTokenLockManager', l1Manager) 117 | await tokenLockManager.setAuthFunctionCall('depositToL2Locked(uint256,address,uint256,uint256,uint256)', transferToolAddress) 118 | await tokenLockManager.setAuthFunctionCall('withdrawETH(address,uint256)', transferToolAddress) 119 | await tokenLockManager.setAuthFunctionCall('setL2WalletAddressManually(address)', transferToolAddress) 120 | await tokenLockManager.addTokenDestination(transferToolAddress) 121 | await tokenLockManager.setAuthFunctionCall('transferLockedDelegationToL2(address,uint256,uint256,uint256)', stakingAddress) 122 | await tokenLockManager.setAuthFunctionCall('transferLockedStakeToL2(uint256,uint256,uint256,uint256)', stakingAddress) 123 | // Repeat for each manager... 124 | ``` 125 | 126 | Keep in mind that existing lock wallets that had already called `approveProtocol()` to interact with the protocol will need to call `revokeProtocol()` and then `approveProtocol()` to be able to use the transfer tool. 127 | 128 | ### 7. Configure the authorized functions on L2 129 | 130 | The L2 managers will also need to authorize the functions to interact with the protocol. This is similar to step 4 when setting up the manager in L1, but here we must specify the manager name used when deploying the L2 manager 131 | 132 | ``` 133 | npx hardhat manager-setup-auth --target-address --manager-name --network arbitrum-goerli 134 | ``` 135 | 136 | We then need to also add authorization to call the L2 transfer tool on a hardhat console: 137 | 138 | ```javascript 139 | transferToolAddress = '' 140 | l2Manager = '' 141 | deployer = (await hre.ethers.getSigners())[0] 142 | tokenLockManager = await hre.ethers.getContractAt('L2GraphTokenLockManager', l2Manager) 143 | await tokenLockManager.setAuthFunctionCall('withdrawToL1Locked(uint256)', transferToolAddress) 144 | await tokenLockManager.addTokenDestination(transferToolAddress) 145 | // Repeat for each manager... 146 | ``` 147 | 148 | ### 8. Make sure the protocol is configured 149 | 150 | The contracts for The Graph must be configured such that the L1 transfer tool is added to the bridge callhook allowlist so that it can send data through the bridge. 151 | Additionally, the L1Staking contract must be configured to use the L1 transfer tool when transferring stake and delegation for vesting contracts; this is done using the `setL1GraphTokenLockTransferTool` (called by the Governor, i.e. the Council). 152 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 The Graph Foundation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ This repository has been moved to https://github.com/graphprotocol/contracts and is now deprecated ⚠️ 2 | 3 | # Graph Protocol Token Lock 4 | 5 | This repository contains a number of contracts that will support the locking of tokens of participants under different schedules. 6 | An important premise is that participants with locked tokens can perform a number of restricted actions in the protocol with their tokens. 7 | 8 | ## Contracts 9 | 10 | ### GraphTokenLock 11 | 12 | The contract lock manages a number of tokens deposited into the contract to ensure that they can only be released under certain time conditions. 13 | 14 | This contract implements a release scheduled based on periods where tokens are released in steps after each period ends. It can be configured with one period in which case it works like a plain TimeLock. 15 | It also supports revocation by the contract owner to be used for vesting schedules. 16 | 17 | The contract supports receiving extra funds over the managed tokens that can be withdrawn by the beneficiary at any time. 18 | 19 | A releaseStartTime parameter is included to override the default release schedule and perform the first release on the configured time. After that initial release it will continue with the default schedule. 20 | 21 | ### GraphTokenLockWallet 22 | 23 | This contract is built on top of the base **GraphTokenLock** functionality. It allows the use of locked funds only when authorized function calls are issued to the contract. 24 | It works by "forwarding" authorized function calls to predefined target contracts in the Graph Network. 25 | 26 | The idea is that supporters with locked tokens can participate in the protocol but disallow any release before the vesting/lock schedule. 27 | The function calls allowed are queried to the **GraphTokenLockManager**, this way the same configuration can be shared for all the created lock wallet contracts. 28 | 29 | Locked tokens must only leave this contract under the locking rules and by the beneficiary calling release(). Tokens used in the protocol need to get back to this contract when unstaked or undelegated. 30 | 31 | Some users can profit by participating in the protocol through their locked tokens, if they withdraw them from the protocol back to the lock contract, they should be able to withdraw those surplus funds out of the contract. 32 | 33 | The following functions signatures will be authorized for use: 34 | 35 | ``` 36 | ### Target 37 | 38 | - Staking contract address 39 | 40 | 41 | ### Function Signatures 42 | 43 | - setOperator(address,bool) 44 | 45 | - stake(uint256) 46 | - unstake(uint256) 47 | - withdraw() 48 | 49 | - setDelegationParameters(uint32,uint32,uint32) 50 | - delegate(address,uint256) 51 | - undelegate(address,uint256) 52 | - withdrawDelegated(address,address) 53 | ``` 54 | 55 | ### GraphTokenLockManager 56 | 57 | Contract that works as a factory of **GraphTokenLockWallet** contracts. It manages the function calls authorized to be called on any GraphTokenWallet and also holds addresses of our protocol contracts configured as targets. 58 | 59 | The Manager supports creating TokenLock contracts based on a mastercopy bytecode using a Minimal Proxy to save gas. It also do so with CREATE2 to have reproducible addresses, this way any future to be deployed contract address can be passed to beneficiaries before actual deployment. 60 | 61 | For convenience, the Manager will also fund the created contract with the amount of each contract's managed tokens. 62 | 63 | ## L2 deployment and transfer tools 64 | 65 | As part of the process for The Graph to move to Arbitrum, some contracts were added to allow GraphTokenLockWallet beneficiaries to transfer their funds to a vesting wallet in L2 and transfer their stake and delegations to L2 as well, where they will be owned by the L2 vesting wallet. See [GIP-0046](https://forum.thegraph.com/t/gip-0046-l2-transfer-tools/4023) for more details about the architecture. 66 | 67 | ### L2GraphTokenLockWallet 68 | 69 | The L2 version of the GraphTokenLockWallet is essentially the same, but adds a special function to initialize a wallet with information received from L1. Keep in mind that **vesting contracts in L2 that are created from an L1 contract cannot release funds in L2 until the end of the full vesting timeline**. Funds can be sent back to L1 using the L2GraphTokenLockTransferTool to be released there. 70 | 71 | ### L2GraphTokenLockManager 72 | 73 | The L2 manager inherits from GraphTokenLockManager but includes an `onTokenTransfer` function for the GRT bridge to call when receiving locked GRT from L1. The first time that GRT is received for a new L1 wallet, the corresponding L2GraphTokenLockWallet contract is created and initialized. 74 | 75 | ### L1GraphTokenLockTransferTool 76 | 77 | For L1 GraphTokenLockWallet beneficiaries to transfer to L2, they must use this transfer tool contract. The contract allows beneficiaries to deposit ETH to pay for the L2 gas and fees related to sending messages to L2. It then allows two different flows, depending on the state of the vesting contract: 78 | 79 | - For fully vested GraphTokenLockWallet contracts, the wallet can call the transfer tool specifying the address that the beneficiary wants to use in L2. This will be a normal wallet (can be an EOA or a multisig in L2), and this will be queried by the Staking contract when transferring stake or delegation. Please note that **this address can only be set once and cannot be changed**. 80 | - For contracts that are still vesting, the wallet can call the transfer tool specifying an amount of GRT to transfer to L2. This GRT value will be sent from the vesting wallet's GRT balance, and must be nonzero. The caller can also specify a beneficiary that will own the L2 vesting wallet, which is only used if this hasn't already been set in a previous call. 81 | 82 | ### L2GraphTokenLockTransferTool 83 | 84 | For beneficiaries of L1 vesting contracts to release funds that have been sent to L2, the GRT funds must be sent to L1 first. The L2GraphTokenLockTransferTool only has a `withdrawToL1Locked` function for this purpose, it will use the GRT bridge to send GRT to the L1 vesting contract that corresponds to the caller. 85 | 86 | ## Operations 87 | 88 | For detailed instructions about deploying the manager or the transfer tools, check out [DEPLOYMENT.md](./DEPLOYMENT.md). 89 | 90 | ### Deploying new vesting locks 91 | 92 | **1) Check configuration** 93 | 94 | Ensure the .env file contains the MNEMONIC you are going to use for the deployment. Please refer to the `.env.sample` file for reference. 95 | 96 | **2) Create the deployment file** 97 | 98 | The file must be have CSV format in placed in the `/ops` folder with the following header: 99 | ``` 100 | beneficiary,managedAmount,startTime,endTime,periods,revocable,releaseStartTime,vestingCliffTime 101 | ... line 1 102 | ... line 2 103 | ... N 104 | ``` 105 | 106 | - **beneficiary** Address of the beneficiary of locked tokens. 107 | - **managedAmount** Amount of tokens to be managed by the lock contract. 108 | - **startTime** Start time of the release schedule. 109 | - **endTime** End time of the release schedule. 110 | - **periods** Number of periods between start time and end time. 111 | - **revocable** Whether the contract is revocable. Should be 1 for `Enabled` or 2 for 1 `Disable`. Setting this to 0 for `NotSet` will cause the transaction to fail with error `Must set a revocability option`. 112 | - **releaseStartTime** Override time for when the releases start. 113 | - **vestingCliffTime** Time the cliff vests, 0 if no cliff 114 | 115 | You can define one line per contract. Keep the header in the file. 116 | 117 | In addition to that, create an empty file in the `/ops` folder to store the results of the deployed contracts. 118 | 119 | **2) Deposit funds in the Manager** 120 | 121 | You need to deposit enough funds in the Manager to be able to use for the deployments. When you run the `create-token-locks` command it will always check that the Manager has enough tokens to cover for the sum of vesting amount. 122 | 123 | ``` 124 | npx hardhat manager-deposit --amount --network 125 | ``` 126 | 127 | - **amount** is a string and it can have 18 decimals. For example 1000.12 128 | 129 | - **network** depends on the `hardhat.config` but most of the times will be sepolia or mainnet. 130 | 131 | **3) Deploy the contracts** 132 | 133 | ``` 134 | npx hardhat create-token-locks --network sepolia \ 135 | --deploy-file \ 136 | --result-file \ 137 | --owner-address \ 138 | --dry-run (Flag) \ 139 | --tx-builder (Flag) \ 140 | --tx-builder-template (Optional) 141 | ``` 142 | 143 | - **network** depends on the hardhat.config but most of the times will be rinkeby or mainnet. 144 | 145 | - **deploy-file** file name under `/ops` that contains the contracts to deploy. 146 | 147 | - **result-file** file with the results of deployments. 148 | 149 | - **owner-address** address to use as owner of the vesting contracts. The owner can revoke the contract if revocable. 150 | 151 | - **dry-run** Get the deterministic contract addresses but do not deploy. 152 | 153 | - **tx-builder** Output transaction batch in JSON format, compatible with Gnosis Safe transaction builder. Does not deploy contracts. 154 | 155 | - **tx-builder-template** File to use as a template for the transaction builder. 156 | 157 | ## Copyright 158 | 159 | Copyright © 2020 The Graph Foundation 160 | 161 | Licensed under the [MIT license](LICENSE.md). 162 | -------------------------------------------------------------------------------- /audits/2020-11-graph-token-distribution.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphprotocol/token-distribution/6c9e580c961d7e97c37e9f94162eda24a305f3f1/audits/2020-11-graph-token-distribution.pdf -------------------------------------------------------------------------------- /contracts/GraphTokenDistributor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/math/SafeMath.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 9 | 10 | /** 11 | * @title GraphTokenDistributor 12 | * @dev Contract that allows distribution of tokens to multiple beneficiaries. 13 | * The contract accept deposits in the configured token by anyone. 14 | * The owner can setup the desired distribution by setting the amount of tokens 15 | * assigned to each beneficiary account. 16 | * Beneficiaries claim for their allocated tokens. 17 | * Only the owner can withdraw tokens from this contract without limitations. 18 | * For the distribution to work this contract must be unlocked by the owner. 19 | */ 20 | contract GraphTokenDistributor is Ownable { 21 | using SafeMath for uint256; 22 | using SafeERC20 for IERC20; 23 | 24 | // -- State -- 25 | 26 | bool public locked; 27 | mapping(address => uint256) public beneficiaries; 28 | 29 | IERC20 public token; 30 | 31 | // -- Events -- 32 | 33 | event BeneficiaryUpdated(address indexed beneficiary, uint256 amount); 34 | event TokensDeposited(address indexed sender, uint256 amount); 35 | event TokensWithdrawn(address indexed sender, uint256 amount); 36 | event TokensClaimed(address indexed beneficiary, address to, uint256 amount); 37 | event LockUpdated(bool locked); 38 | 39 | modifier whenNotLocked() { 40 | require(locked == false, "Distributor: Claim is locked"); 41 | _; 42 | } 43 | 44 | /** 45 | * Constructor. 46 | * @param _token Token to use for deposits and withdrawals 47 | */ 48 | constructor(IERC20 _token) { 49 | token = _token; 50 | locked = true; 51 | } 52 | 53 | /** 54 | * Deposit tokens into the contract. 55 | * Even if the ERC20 token can be transferred directly to the contract 56 | * this function provide a safe interface to do the transfer and avoid mistakes 57 | * @param _amount Amount to deposit 58 | */ 59 | function deposit(uint256 _amount) external { 60 | token.safeTransferFrom(msg.sender, address(this), _amount); 61 | emit TokensDeposited(msg.sender, _amount); 62 | } 63 | 64 | // -- Admin functions -- 65 | 66 | /** 67 | * Add token balance available for account. 68 | * @param _account Address to assign tokens to 69 | * @param _amount Amount of tokens to assign to beneficiary 70 | */ 71 | function addBeneficiaryTokens(address _account, uint256 _amount) external onlyOwner { 72 | _setBeneficiaryTokens(_account, beneficiaries[_account].add(_amount)); 73 | } 74 | 75 | /** 76 | * Add token balance available for multiple accounts. 77 | * @param _accounts Addresses to assign tokens to 78 | * @param _amounts Amounts of tokens to assign to beneficiary 79 | */ 80 | function addBeneficiaryTokensMulti(address[] calldata _accounts, uint256[] calldata _amounts) external onlyOwner { 81 | require(_accounts.length == _amounts.length, "Distributor: !length"); 82 | for (uint256 i = 0; i < _accounts.length; i++) { 83 | _setBeneficiaryTokens(_accounts[i], beneficiaries[_accounts[i]].add(_amounts[i])); 84 | } 85 | } 86 | 87 | /** 88 | * Remove token balance available for account. 89 | * @param _account Address to assign tokens to 90 | * @param _amount Amount of tokens to assign to beneficiary 91 | */ 92 | function subBeneficiaryTokens(address _account, uint256 _amount) external onlyOwner { 93 | _setBeneficiaryTokens(_account, beneficiaries[_account].sub(_amount)); 94 | } 95 | 96 | /** 97 | * Remove token balance available for multiple accounts. 98 | * @param _accounts Addresses to assign tokens to 99 | * @param _amounts Amounts of tokens to assign to beneficiary 100 | */ 101 | function subBeneficiaryTokensMulti(address[] calldata _accounts, uint256[] calldata _amounts) external onlyOwner { 102 | require(_accounts.length == _amounts.length, "Distributor: !length"); 103 | for (uint256 i = 0; i < _accounts.length; i++) { 104 | _setBeneficiaryTokens(_accounts[i], beneficiaries[_accounts[i]].sub(_amounts[i])); 105 | } 106 | } 107 | 108 | /** 109 | * Set amount of tokens available for beneficiary account. 110 | * @param _account Address to assign tokens to 111 | * @param _amount Amount of tokens to assign to beneficiary 112 | */ 113 | function _setBeneficiaryTokens(address _account, uint256 _amount) private { 114 | require(_account != address(0), "Distributor: !account"); 115 | 116 | beneficiaries[_account] = _amount; 117 | emit BeneficiaryUpdated(_account, _amount); 118 | } 119 | 120 | /** 121 | * Set locked withdrawals. 122 | * @param _locked True to lock withdrawals 123 | */ 124 | function setLocked(bool _locked) external onlyOwner { 125 | locked = _locked; 126 | emit LockUpdated(_locked); 127 | } 128 | 129 | /** 130 | * Withdraw tokens from the contract. This function is included as 131 | * a escape hatch in case of mistakes or to recover remaining funds. 132 | * @param _amount Amount of tokens to withdraw 133 | */ 134 | function withdraw(uint256 _amount) external onlyOwner { 135 | token.safeTransfer(msg.sender, _amount); 136 | emit TokensWithdrawn(msg.sender, _amount); 137 | } 138 | 139 | // -- Beneficiary functions -- 140 | 141 | /** 142 | * Claim tokens and send to caller. 143 | */ 144 | function claim() external whenNotLocked { 145 | claimTo(msg.sender); 146 | } 147 | 148 | /** 149 | * Claim tokens and send to address. 150 | * @param _to Address where to send tokens 151 | */ 152 | function claimTo(address _to) public whenNotLocked { 153 | uint256 claimableTokens = beneficiaries[msg.sender]; 154 | require(claimableTokens > 0, "Distributor: Unavailable funds"); 155 | 156 | _setBeneficiaryTokens(msg.sender, 0); 157 | 158 | token.safeTransfer(_to, claimableTokens); 159 | emit TokensClaimed(msg.sender, _to, claimableTokens); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /contracts/GraphTokenLockSimple.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | import "./GraphTokenLock.sol"; 6 | 7 | /** 8 | * @title GraphTokenLockSimple 9 | * @notice This contract is the concrete simple implementation built on top of the base 10 | * GraphTokenLock functionality for use when we only need the token lock schedule 11 | * features but no interaction with the network. 12 | * 13 | * This contract is designed to be deployed without the use of a TokenManager. 14 | */ 15 | contract GraphTokenLockSimple is GraphTokenLock { 16 | // Constructor 17 | constructor() { 18 | OwnableInitializable._initialize(msg.sender); 19 | } 20 | 21 | // Initializer 22 | function initialize( 23 | address _owner, 24 | address _beneficiary, 25 | address _token, 26 | uint256 _managedAmount, 27 | uint256 _startTime, 28 | uint256 _endTime, 29 | uint256 _periods, 30 | uint256 _releaseStartTime, 31 | uint256 _vestingCliffTime, 32 | Revocability _revocable 33 | ) external onlyOwner { 34 | _initialize( 35 | _owner, 36 | _beneficiary, 37 | _token, 38 | _managedAmount, 39 | _startTime, 40 | _endTime, 41 | _periods, 42 | _releaseStartTime, 43 | _vestingCliffTime, 44 | _revocable 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/GraphTokenLockWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/contracts/utils/Address.sol"; 7 | import "@openzeppelin/contracts/math/SafeMath.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | 10 | import "./GraphTokenLock.sol"; 11 | import "./IGraphTokenLockManager.sol"; 12 | 13 | /** 14 | * @title GraphTokenLockWallet 15 | * @notice This contract is built on top of the base GraphTokenLock functionality. 16 | * It allows wallet beneficiaries to use the deposited funds to perform specific function calls 17 | * on specific contracts. 18 | * 19 | * The idea is that supporters with locked tokens can participate in the protocol 20 | * but disallow any release before the vesting/lock schedule. 21 | * The beneficiary can issue authorized function calls to this contract that will 22 | * get forwarded to a target contract. A target contract is any of our protocol contracts. 23 | * The function calls allowed are queried to the GraphTokenLockManager, this way 24 | * the same configuration can be shared for all the created lock wallet contracts. 25 | * 26 | * NOTE: Contracts used as target must have its function signatures checked to avoid collisions 27 | * with any of this contract functions. 28 | * Beneficiaries need to approve the use of the tokens to the protocol contracts. For convenience 29 | * the maximum amount of tokens is authorized. 30 | * Function calls do not forward ETH value so DO NOT SEND ETH TO THIS CONTRACT. 31 | */ 32 | contract GraphTokenLockWallet is GraphTokenLock { 33 | using SafeMath for uint256; 34 | 35 | // -- State -- 36 | 37 | IGraphTokenLockManager public manager; 38 | uint256 public usedAmount; 39 | 40 | // -- Events -- 41 | 42 | event ManagerUpdated(address indexed _oldManager, address indexed _newManager); 43 | event TokenDestinationsApproved(); 44 | event TokenDestinationsRevoked(); 45 | 46 | // Initializer 47 | function initialize( 48 | address _manager, 49 | address _owner, 50 | address _beneficiary, 51 | address _token, 52 | uint256 _managedAmount, 53 | uint256 _startTime, 54 | uint256 _endTime, 55 | uint256 _periods, 56 | uint256 _releaseStartTime, 57 | uint256 _vestingCliffTime, 58 | Revocability _revocable 59 | ) external { 60 | _initialize( 61 | _owner, 62 | _beneficiary, 63 | _token, 64 | _managedAmount, 65 | _startTime, 66 | _endTime, 67 | _periods, 68 | _releaseStartTime, 69 | _vestingCliffTime, 70 | _revocable 71 | ); 72 | _setManager(_manager); 73 | } 74 | 75 | // -- Admin -- 76 | 77 | /** 78 | * @notice Sets a new manager for this contract 79 | * @param _newManager Address of the new manager 80 | */ 81 | function setManager(address _newManager) external onlyOwner { 82 | _setManager(_newManager); 83 | } 84 | 85 | /** 86 | * @dev Sets a new manager for this contract 87 | * @param _newManager Address of the new manager 88 | */ 89 | function _setManager(address _newManager) internal { 90 | require(_newManager != address(0), "Manager cannot be empty"); 91 | require(Address.isContract(_newManager), "Manager must be a contract"); 92 | 93 | address oldManager = address(manager); 94 | manager = IGraphTokenLockManager(_newManager); 95 | 96 | emit ManagerUpdated(oldManager, _newManager); 97 | } 98 | 99 | // -- Beneficiary -- 100 | 101 | /** 102 | * @notice Approves protocol access of the tokens managed by this contract 103 | * @dev Approves all token destinations registered in the manager to pull tokens 104 | */ 105 | function approveProtocol() external onlyBeneficiary { 106 | address[] memory dstList = manager.getTokenDestinations(); 107 | for (uint256 i = 0; i < dstList.length; i++) { 108 | // Note this is only safe because we are using the max uint256 value 109 | token.approve(dstList[i], type(uint256).max); 110 | } 111 | emit TokenDestinationsApproved(); 112 | } 113 | 114 | /** 115 | * @notice Revokes protocol access of the tokens managed by this contract 116 | * @dev Revokes approval to all token destinations in the manager to pull tokens 117 | */ 118 | function revokeProtocol() external onlyBeneficiary { 119 | address[] memory dstList = manager.getTokenDestinations(); 120 | for (uint256 i = 0; i < dstList.length; i++) { 121 | // Note this is only safe cause we're using 0 as the amount 122 | token.approve(dstList[i], 0); 123 | } 124 | emit TokenDestinationsRevoked(); 125 | } 126 | 127 | /** 128 | * @notice Gets tokens currently available for release 129 | * @dev Considers the schedule, takes into account already released tokens and used amount 130 | * @return Amount of tokens ready to be released 131 | */ 132 | function releasableAmount() public view override returns (uint256) { 133 | if (revocable == Revocability.Disabled) { 134 | return super.releasableAmount(); 135 | } 136 | 137 | // -- Revocability enabled logic 138 | // This needs to deal with additional considerations for when tokens are used in the protocol 139 | 140 | // If a release start time is set no tokens are available for release before this date 141 | // If not set it follows the default schedule and tokens are available on 142 | // the first period passed 143 | if (releaseStartTime > 0 && currentTime() < releaseStartTime) { 144 | return 0; 145 | } 146 | 147 | // Vesting cliff is activated and it has not passed means nothing is vested yet 148 | // so funds cannot be released 149 | if (revocable == Revocability.Enabled && vestingCliffTime > 0 && currentTime() < vestingCliffTime) { 150 | return 0; 151 | } 152 | 153 | // A beneficiary can never have more releasable tokens than the contract balance 154 | // We consider the `usedAmount` in the protocol as part of the calculations 155 | // the beneficiary should not release funds that are used. 156 | uint256 releasable = availableAmount().sub(releasedAmount).sub(usedAmount); 157 | return MathUtils.min(currentBalance(), releasable); 158 | } 159 | 160 | /** 161 | * @notice Forward authorized contract calls to protocol contracts 162 | * @dev Fallback function can be called by the beneficiary only if function call is allowed 163 | */ 164 | // solhint-disable-next-line no-complex-fallback 165 | fallback() external payable { 166 | // Only beneficiary can forward calls 167 | require(msg.sender == beneficiary, "Unauthorized caller"); 168 | require(msg.value == 0, "ETH transfers not supported"); 169 | 170 | // Function call validation 171 | address _target = manager.getAuthFunctionCallTarget(msg.sig); 172 | require(_target != address(0), "Unauthorized function"); 173 | 174 | uint256 oldBalance = currentBalance(); 175 | 176 | // Call function with data 177 | Address.functionCall(_target, msg.data); 178 | 179 | // Tracked used tokens in the protocol 180 | // We do this check after balances were updated by the forwarded call 181 | // Check is only enforced for revocable contracts to save some gas 182 | if (revocable == Revocability.Enabled) { 183 | // Track contract balance change 184 | uint256 newBalance = currentBalance(); 185 | if (newBalance < oldBalance) { 186 | // Outflow 187 | uint256 diff = oldBalance.sub(newBalance); 188 | usedAmount = usedAmount.add(diff); 189 | } else { 190 | // Inflow: We can receive profits from the protocol, that could make usedAmount to 191 | // underflow. We set it to zero in that case. 192 | uint256 diff = newBalance.sub(oldBalance); 193 | usedAmount = (diff >= usedAmount) ? 0 : usedAmount.sub(diff); 194 | } 195 | require(usedAmount <= vestedAmount(), "Cannot use more tokens than vested amount"); 196 | } 197 | } 198 | 199 | /** 200 | * @notice Receive function that always reverts. 201 | * @dev Only included to supress warnings, see https://github.com/ethereum/solidity/issues/10159 202 | */ 203 | receive() external payable { 204 | revert("Bad call"); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /contracts/ICallhookReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | // Copied from graphprotocol/contracts, changed solidity version to 0.7.3 4 | 5 | /** 6 | * @title Interface for contracts that can receive callhooks through the Arbitrum GRT bridge 7 | * @dev Any contract that can receive a callhook on L2, sent through the bridge from L1, must 8 | * be allowlisted by the governor, but also implement this interface that contains 9 | * the function that will actually be called by the L2GraphTokenGateway. 10 | */ 11 | pragma solidity ^0.7.3; 12 | 13 | interface ICallhookReceiver { 14 | /** 15 | * @notice Receive tokens with a callhook from the bridge 16 | * @param _from Token sender in L1 17 | * @param _amount Amount of tokens that were transferred 18 | * @param _data ABI-encoded callhook data 19 | */ 20 | function onTokenTransfer(address _from, uint256 _amount, bytes calldata _data) external; 21 | } 22 | -------------------------------------------------------------------------------- /contracts/IGraphTokenLock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | interface IGraphTokenLock { 9 | enum Revocability { 10 | NotSet, 11 | Enabled, 12 | Disabled 13 | } 14 | 15 | // -- Balances -- 16 | 17 | function currentBalance() external view returns (uint256); 18 | 19 | // -- Time & Periods -- 20 | 21 | function currentTime() external view returns (uint256); 22 | 23 | function duration() external view returns (uint256); 24 | 25 | function sinceStartTime() external view returns (uint256); 26 | 27 | function amountPerPeriod() external view returns (uint256); 28 | 29 | function periodDuration() external view returns (uint256); 30 | 31 | function currentPeriod() external view returns (uint256); 32 | 33 | function passedPeriods() external view returns (uint256); 34 | 35 | // -- Locking & Release Schedule -- 36 | 37 | function availableAmount() external view returns (uint256); 38 | 39 | function vestedAmount() external view returns (uint256); 40 | 41 | function releasableAmount() external view returns (uint256); 42 | 43 | function totalOutstandingAmount() external view returns (uint256); 44 | 45 | function surplusAmount() external view returns (uint256); 46 | 47 | // -- Value Transfer -- 48 | 49 | function release() external; 50 | 51 | function withdrawSurplus(uint256 _amount) external; 52 | 53 | function revoke() external; 54 | } 55 | -------------------------------------------------------------------------------- /contracts/IGraphTokenLockManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | import "./IGraphTokenLock.sol"; 9 | 10 | interface IGraphTokenLockManager { 11 | // -- Factory -- 12 | 13 | function setMasterCopy(address _masterCopy) external; 14 | 15 | function createTokenLockWallet( 16 | address _owner, 17 | address _beneficiary, 18 | uint256 _managedAmount, 19 | uint256 _startTime, 20 | uint256 _endTime, 21 | uint256 _periods, 22 | uint256 _releaseStartTime, 23 | uint256 _vestingCliffTime, 24 | IGraphTokenLock.Revocability _revocable 25 | ) external; 26 | 27 | // -- Funds Management -- 28 | 29 | function token() external returns (IERC20); 30 | 31 | function deposit(uint256 _amount) external; 32 | 33 | function withdraw(uint256 _amount) external; 34 | 35 | // -- Allowed Funds Destinations -- 36 | 37 | function addTokenDestination(address _dst) external; 38 | 39 | function removeTokenDestination(address _dst) external; 40 | 41 | function isTokenDestination(address _dst) external view returns (bool); 42 | 43 | function getTokenDestinations() external view returns (address[] memory); 44 | 45 | // -- Function Call Authorization -- 46 | 47 | function setAuthFunctionCall(string calldata _signature, address _target) external; 48 | 49 | function unsetAuthFunctionCall(string calldata _signature) external; 50 | 51 | function setAuthFunctionCallMany(string[] calldata _signatures, address[] calldata _targets) external; 52 | 53 | function getAuthFunctionCallTarget(bytes4 _sigHash) external view returns (address); 54 | 55 | function isAuthFunctionCall(bytes4 _sigHash) external view returns (bool); 56 | } 57 | -------------------------------------------------------------------------------- /contracts/L2GraphTokenLockManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 8 | 9 | import { ICallhookReceiver } from "./ICallhookReceiver.sol"; 10 | import { GraphTokenLockManager } from "./GraphTokenLockManager.sol"; 11 | import { L2GraphTokenLockWallet } from "./L2GraphTokenLockWallet.sol"; 12 | 13 | /** 14 | * @title L2GraphTokenLockManager 15 | * @notice This contract manages a list of authorized function calls and targets that can be called 16 | * by any TokenLockWallet contract and it is a factory of TokenLockWallet contracts. 17 | * 18 | * This contract receives funds to make the process of creating TokenLockWallet contracts 19 | * easier by distributing them the initial tokens to be managed. 20 | * 21 | * In particular, this L2 variant is designed to receive token lock wallets from L1, 22 | * through the GRT bridge. These transferred wallets will not allow releasing funds in L2 until 23 | * the end of the vesting timeline, but they can allow withdrawing funds back to L1 using 24 | * the L2GraphTokenLockTransferTool contract. 25 | * 26 | * The owner can setup a list of token destinations that will be used by TokenLock contracts to 27 | * approve the pulling of funds, this way in can be guaranteed that only protocol contracts 28 | * will manipulate users funds. 29 | */ 30 | contract L2GraphTokenLockManager is GraphTokenLockManager, ICallhookReceiver { 31 | using SafeERC20 for IERC20; 32 | 33 | /// @dev Struct to hold the data of a transferred wallet; this is 34 | /// the data that must be encoded in L1 to send a wallet to L2. 35 | struct TransferredWalletData { 36 | address l1Address; 37 | address owner; 38 | address beneficiary; 39 | uint256 managedAmount; 40 | uint256 startTime; 41 | uint256 endTime; 42 | } 43 | 44 | /// Address of the L2GraphTokenGateway 45 | address public immutable l2Gateway; 46 | /// Address of the L1 transfer tool contract (in L1, no aliasing) 47 | address public immutable l1TransferTool; 48 | /// Mapping of each L1 wallet to its L2 wallet counterpart (populated when each wallet is received) 49 | /// L1 address => L2 address 50 | mapping(address => address) public l1WalletToL2Wallet; 51 | /// Mapping of each L2 wallet to its L1 wallet counterpart (populated when each wallet is received) 52 | /// L2 address => L1 address 53 | mapping(address => address) public l2WalletToL1Wallet; 54 | 55 | /// @dev Event emitted when a wallet is received and created from L1 56 | event TokenLockCreatedFromL1( 57 | address indexed contractAddress, 58 | bytes32 initHash, 59 | address indexed beneficiary, 60 | uint256 managedAmount, 61 | uint256 startTime, 62 | uint256 endTime, 63 | address indexed l1Address 64 | ); 65 | 66 | /// @dev Emitted when locked tokens are received from L1 (whether the wallet 67 | /// had already been received or not) 68 | event LockedTokensReceivedFromL1(address indexed l1Address, address indexed l2Address, uint256 amount); 69 | 70 | /** 71 | * @dev Checks that the sender is the L2GraphTokenGateway. 72 | */ 73 | modifier onlyL2Gateway() { 74 | require(msg.sender == l2Gateway, "ONLY_GATEWAY"); 75 | _; 76 | } 77 | 78 | /** 79 | * @notice Constructor for the L2GraphTokenLockManager contract. 80 | * @param _graphToken Address of the L2 GRT token contract 81 | * @param _masterCopy Address of the master copy of the L2GraphTokenLockWallet implementation 82 | * @param _l2Gateway Address of the L2GraphTokenGateway contract 83 | * @param _l1TransferTool Address of the L1 transfer tool contract (in L1, without aliasing) 84 | */ 85 | constructor( 86 | IERC20 _graphToken, 87 | address _masterCopy, 88 | address _l2Gateway, 89 | address _l1TransferTool 90 | ) GraphTokenLockManager(_graphToken, _masterCopy) { 91 | l2Gateway = _l2Gateway; 92 | l1TransferTool = _l1TransferTool; 93 | } 94 | 95 | /** 96 | * @notice This function is called by the L2GraphTokenGateway when tokens are sent from L1. 97 | * @dev This function will create a new wallet if it doesn't exist yet, or send the tokens to 98 | * the existing wallet if it does. 99 | * @param _from Address of the sender in L1, which must be the L1GraphTokenLockTransferTool 100 | * @param _amount Amount of tokens received 101 | * @param _data Encoded data of the transferred wallet, which must be an ABI-encoded TransferredWalletData struct 102 | */ 103 | function onTokenTransfer(address _from, uint256 _amount, bytes calldata _data) external override onlyL2Gateway { 104 | require(_from == l1TransferTool, "ONLY_TRANSFER_TOOL"); 105 | TransferredWalletData memory walletData = abi.decode(_data, (TransferredWalletData)); 106 | 107 | if (l1WalletToL2Wallet[walletData.l1Address] != address(0)) { 108 | // If the wallet was already received, just send the tokens to the L2 address 109 | _token.safeTransfer(l1WalletToL2Wallet[walletData.l1Address], _amount); 110 | } else { 111 | // Create contract using a minimal proxy and call initializer 112 | (bytes32 initHash, address contractAddress) = _deployFromL1(keccak256(_data), walletData); 113 | l1WalletToL2Wallet[walletData.l1Address] = contractAddress; 114 | l2WalletToL1Wallet[contractAddress] = walletData.l1Address; 115 | 116 | // Send managed amount to the created contract 117 | _token.safeTransfer(contractAddress, _amount); 118 | 119 | emit TokenLockCreatedFromL1( 120 | contractAddress, 121 | initHash, 122 | walletData.beneficiary, 123 | walletData.managedAmount, 124 | walletData.startTime, 125 | walletData.endTime, 126 | walletData.l1Address 127 | ); 128 | } 129 | emit LockedTokensReceivedFromL1(walletData.l1Address, l1WalletToL2Wallet[walletData.l1Address], _amount); 130 | } 131 | 132 | /** 133 | * @dev Deploy a token lock wallet with data received from L1 134 | * @param _salt Salt for the CREATE2 call, which must be the hash of the wallet data 135 | * @param _walletData Data of the wallet to be created 136 | * @return Hash of the initialization calldata 137 | * @return Address of the created contract 138 | */ 139 | function _deployFromL1(bytes32 _salt, TransferredWalletData memory _walletData) internal returns (bytes32, address) { 140 | bytes memory initializer = _encodeInitializer(_walletData); 141 | address contractAddress = _deployProxy2(_salt, masterCopy, initializer); 142 | return (keccak256(initializer), contractAddress); 143 | } 144 | 145 | /** 146 | * @dev Encode the initializer for the token lock wallet received from L1 147 | * @param _walletData Data of the wallet to be created 148 | * @return Encoded initializer calldata, including the function signature 149 | */ 150 | function _encodeInitializer(TransferredWalletData memory _walletData) internal view returns (bytes memory) { 151 | return 152 | abi.encodeWithSelector( 153 | L2GraphTokenLockWallet.initializeFromL1.selector, 154 | address(this), 155 | address(_token), 156 | _walletData 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /contracts/L2GraphTokenLockTransferTool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | import { L2GraphTokenLockManager } from "./L2GraphTokenLockManager.sol"; 9 | import { L2GraphTokenLockWallet } from "./L2GraphTokenLockWallet.sol"; 10 | import { ITokenGateway } from "./arbitrum/ITokenGateway.sol"; 11 | 12 | /** 13 | * @title L2GraphTokenLockTransferTool contract 14 | * @notice This contract is used to transfer GRT from L2 token lock wallets 15 | * back to their L1 counterparts. 16 | */ 17 | contract L2GraphTokenLockTransferTool { 18 | /// Address of the L2 GRT token 19 | IERC20 public immutable graphToken; 20 | /// Address of the L2GraphTokenGateway 21 | ITokenGateway public immutable l2Gateway; 22 | /// Address of the L1 GRT token (in L1, no aliasing) 23 | address public immutable l1GraphToken; 24 | 25 | /// @dev Emitted when GRT is sent to L1 from a token lock 26 | event LockedFundsSentToL1( 27 | address indexed l1Wallet, 28 | address indexed l2Wallet, 29 | address indexed l2LockManager, 30 | uint256 amount 31 | ); 32 | 33 | /** 34 | * @notice Constructor for the L2GraphTokenLockTransferTool contract 35 | * @dev Note the L2GraphTokenLockTransferTool can be deployed behind a proxy, 36 | * and the constructor for the implementation will only set some immutable 37 | * variables. 38 | * @param _graphToken Address of the L2 GRT token 39 | * @param _l2Gateway Address of the L2GraphTokenGateway 40 | * @param _l1GraphToken Address of the L1 GRT token (in L1, no aliasing) 41 | */ 42 | constructor(IERC20 _graphToken, ITokenGateway _l2Gateway, address _l1GraphToken) { 43 | graphToken = _graphToken; 44 | l2Gateway = _l2Gateway; 45 | l1GraphToken = _l1GraphToken; 46 | } 47 | 48 | /** 49 | * @notice Withdraw GRT from an L2 token lock wallet to its L1 counterpart. 50 | * This function must be called from an L2GraphTokenLockWallet contract. 51 | * The GRT will be sent to L1 and must be claimed using the Arbitrum Outbox on L1 52 | * after the standard Arbitrum withdrawal period (7 days). 53 | * @param _amount Amount of GRT to withdraw 54 | */ 55 | function withdrawToL1Locked(uint256 _amount) external { 56 | L2GraphTokenLockWallet wallet = L2GraphTokenLockWallet(msg.sender); 57 | L2GraphTokenLockManager manager = L2GraphTokenLockManager(address(wallet.manager())); 58 | require(address(manager) != address(0), "INVALID_SENDER"); 59 | address l1Wallet = manager.l2WalletToL1Wallet(msg.sender); 60 | require(l1Wallet != address(0), "NOT_L1_WALLET"); 61 | require(_amount <= graphToken.balanceOf(msg.sender), "INSUFFICIENT_BALANCE"); 62 | require(_amount != 0, "ZERO_AMOUNT"); 63 | 64 | graphToken.transferFrom(msg.sender, address(this), _amount); 65 | graphToken.approve(address(l2Gateway), _amount); 66 | 67 | // Send the tokens through the L2GraphTokenGateway to the L1 wallet counterpart 68 | l2Gateway.outboundTransfer(l1GraphToken, l1Wallet, _amount, 0, 0, ""); 69 | emit LockedFundsSentToL1(l1Wallet, msg.sender, address(manager), _amount); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /contracts/L2GraphTokenLockWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | import { GraphTokenLockWallet } from "./GraphTokenLockWallet.sol"; 9 | import { Ownable as OwnableInitializable } from "./Ownable.sol"; 10 | import { L2GraphTokenLockManager } from "./L2GraphTokenLockManager.sol"; 11 | 12 | /** 13 | * @title L2GraphTokenLockWallet 14 | * @notice This contract is built on top of the base GraphTokenLock functionality. 15 | * It allows wallet beneficiaries to use the deposited funds to perform specific function calls 16 | * on specific contracts. 17 | * 18 | * The idea is that supporters with locked tokens can participate in the protocol 19 | * but disallow any release before the vesting/lock schedule. 20 | * The beneficiary can issue authorized function calls to this contract that will 21 | * get forwarded to a target contract. A target contract is any of our protocol contracts. 22 | * The function calls allowed are queried to the GraphTokenLockManager, this way 23 | * the same configuration can be shared for all the created lock wallet contracts. 24 | * 25 | * This L2 variant includes a special initializer so that it can be created from 26 | * a wallet's data received from L1. These transferred wallets will not allow releasing 27 | * funds in L2 until the end of the vesting timeline, but they can allow withdrawing 28 | * funds back to L1 using the L2GraphTokenLockTransferTool contract. 29 | * 30 | * Note that surplusAmount and releasedAmount in L2 will be skewed for wallets received from L1, 31 | * so releasing surplus tokens might also only be possible by bridging tokens back to L1. 32 | * 33 | * NOTE: Contracts used as target must have its function signatures checked to avoid collisions 34 | * with any of this contract functions. 35 | * Beneficiaries need to approve the use of the tokens to the protocol contracts. For convenience 36 | * the maximum amount of tokens is authorized. 37 | * Function calls do not forward ETH value so DO NOT SEND ETH TO THIS CONTRACT. 38 | */ 39 | contract L2GraphTokenLockWallet is GraphTokenLockWallet { 40 | // Initializer when created from a message from L1 41 | function initializeFromL1( 42 | address _manager, 43 | address _token, 44 | L2GraphTokenLockManager.TransferredWalletData calldata _walletData 45 | ) external { 46 | require(!isInitialized, "Already initialized"); 47 | isInitialized = true; 48 | 49 | OwnableInitializable._initialize(_walletData.owner); 50 | beneficiary = _walletData.beneficiary; 51 | token = IERC20(_token); 52 | 53 | managedAmount = _walletData.managedAmount; 54 | 55 | startTime = _walletData.startTime; 56 | endTime = _walletData.endTime; 57 | periods = 1; 58 | isAccepted = true; 59 | 60 | // Optionals 61 | releaseStartTime = _walletData.endTime; 62 | revocable = Revocability.Disabled; 63 | 64 | _setManager(_manager); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/MathUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | library MathUtils { 6 | function min(uint256 a, uint256 b) internal pure returns (uint256) { 7 | return a < b ? a : b; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/MinimalProxyFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 6 | import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; 7 | 8 | /** 9 | * @title MinimalProxyFactory: a factory contract for creating minimal proxies 10 | * @notice Adapted from https://github.com/OpenZeppelin/openzeppelin-sdk/blob/v2.5.0/packages/lib/contracts/upgradeability/ProxyFactory.sol 11 | * Based on https://eips.ethereum.org/EIPS/eip-1167 12 | */ 13 | contract MinimalProxyFactory { 14 | /// @dev Emitted when a new proxy is created 15 | event ProxyCreated(address indexed proxy); 16 | 17 | 18 | /** 19 | * @notice Gets the deterministic CREATE2 address for MinimalProxy with a particular implementation 20 | * @dev Uses address(this) as deployer to compute the address. Only for backwards compatibility. 21 | * @param _salt Bytes32 salt to use for CREATE2 22 | * @param _implementation Address of the proxy target implementation 23 | * @return Address of the counterfactual MinimalProxy 24 | */ 25 | function getDeploymentAddress( 26 | bytes32 _salt, 27 | address _implementation 28 | ) public view returns (address) { 29 | return getDeploymentAddress(_salt, _implementation, address(this)); 30 | } 31 | 32 | /** 33 | * @notice Gets the deterministic CREATE2 address for MinimalProxy with a particular implementation 34 | * @param _salt Bytes32 salt to use for CREATE2 35 | * @param _implementation Address of the proxy target implementation 36 | * @param _deployer Address of the deployer that creates the contract 37 | * @return Address of the counterfactual MinimalProxy 38 | */ 39 | function getDeploymentAddress( 40 | bytes32 _salt, 41 | address _implementation, 42 | address _deployer 43 | ) public pure returns (address) { 44 | return Create2.computeAddress(_salt, keccak256(_getContractCreationCode(_implementation)), _deployer); 45 | } 46 | 47 | /** 48 | * @dev Deploys a MinimalProxy with CREATE2 49 | * @param _salt Bytes32 salt to use for CREATE2 50 | * @param _implementation Address of the proxy target implementation 51 | * @param _data Bytes with the initializer call 52 | * @return Address of the deployed MinimalProxy 53 | */ 54 | function _deployProxy2(bytes32 _salt, address _implementation, bytes memory _data) internal returns (address) { 55 | address proxyAddress = Create2.deploy(0, _salt, _getContractCreationCode(_implementation)); 56 | 57 | emit ProxyCreated(proxyAddress); 58 | 59 | // Call function with data 60 | if (_data.length > 0) { 61 | Address.functionCall(proxyAddress, _data); 62 | } 63 | 64 | return proxyAddress; 65 | } 66 | 67 | /** 68 | * @dev Gets the MinimalProxy bytecode 69 | * @param _implementation Address of the proxy target implementation 70 | * @return MinimalProxy bytecode 71 | */ 72 | function _getContractCreationCode(address _implementation) internal pure returns (bytes memory) { 73 | bytes10 creation = 0x3d602d80600a3d3981f3; 74 | bytes10 prefix = 0x363d3d373d3d3d363d73; 75 | bytes20 targetBytes = bytes20(_implementation); 76 | bytes15 suffix = 0x5af43d82803e903d91602b57fd5bf3; 77 | return abi.encodePacked(creation, prefix, targetBytes, suffix); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /contracts/Ownable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | /** 6 | * @dev Contract module which provides a basic access control mechanism, where 7 | * there is an account (an owner) that can be granted exclusive access to 8 | * specific functions. 9 | * 10 | * The owner account will be passed on initialization of the contract. This 11 | * can later be changed with {transferOwnership}. 12 | * 13 | * This module is used through inheritance. It will make available the modifier 14 | * `onlyOwner`, which can be applied to your functions to restrict their use to 15 | * the owner. 16 | */ 17 | contract Ownable { 18 | /// @dev Owner of the contract, can be retrieved with the public owner() function 19 | address private _owner; 20 | /// @dev Since upgradeable contracts might inherit this, we add a storage gap 21 | /// to allow adding variables here without breaking the proxy storage layout 22 | uint256[50] private __gap; 23 | 24 | /// @dev Emitted when ownership of the contract is transferred 25 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 26 | 27 | /** 28 | * @dev Initializes the contract setting the deployer as the initial owner. 29 | */ 30 | function _initialize(address owner) internal { 31 | _owner = owner; 32 | emit OwnershipTransferred(address(0), owner); 33 | } 34 | 35 | /** 36 | * @dev Returns the address of the current owner. 37 | */ 38 | function owner() public view returns (address) { 39 | return _owner; 40 | } 41 | 42 | /** 43 | * @dev Throws if called by any account other than the owner. 44 | */ 45 | modifier onlyOwner() { 46 | require(_owner == msg.sender, "Ownable: caller is not the owner"); 47 | _; 48 | } 49 | 50 | /** 51 | * @dev Leaves the contract without owner. It will not be possible to call 52 | * `onlyOwner` functions anymore. Can only be called by the current owner. 53 | * 54 | * NOTE: Renouncing ownership will leave the contract without an owner, 55 | * thereby removing any functionality that is only available to the owner. 56 | */ 57 | function renounceOwnership() external virtual onlyOwner { 58 | emit OwnershipTransferred(_owner, address(0)); 59 | _owner = address(0); 60 | } 61 | 62 | /** 63 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 64 | * Can only be called by the current owner. 65 | */ 66 | function transferOwnership(address newOwner) external virtual onlyOwner { 67 | require(newOwner != address(0), "Ownable: new owner is the zero address"); 68 | emit OwnershipTransferred(_owner, newOwner); 69 | _owner = newOwner; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /contracts/arbitrum/ITokenGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2020, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * Originally copied from: 19 | * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals 20 | * 21 | * MODIFIED from Offchain Labs' implementation: 22 | * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) 23 | * 24 | */ 25 | 26 | pragma solidity ^0.7.3; 27 | pragma experimental ABIEncoderV2; 28 | 29 | interface ITokenGateway { 30 | /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated 31 | // event OutboundTransferInitiated( 32 | // address token, 33 | // address indexed _from, 34 | // address indexed _to, 35 | // uint256 indexed _transferId, 36 | // uint256 _amount, 37 | // bytes _data 38 | // ); 39 | 40 | /// @notice event deprecated in favor of DepositFinalized and WithdrawalFinalized 41 | // event InboundTransferFinalized( 42 | // address token, 43 | // address indexed _from, 44 | // address indexed _to, 45 | // uint256 indexed _transferId, 46 | // uint256 _amount, 47 | // bytes _data 48 | // ); 49 | 50 | function outboundTransfer( 51 | address _token, 52 | address _to, 53 | uint256 _amount, 54 | uint256 _maxGas, 55 | uint256 _gasPriceBid, 56 | bytes calldata _data 57 | ) external payable returns (bytes memory); 58 | 59 | function finalizeInboundTransfer( 60 | address _token, 61 | address _from, 62 | address _to, 63 | uint256 _amount, 64 | bytes calldata _data 65 | ) external payable; 66 | 67 | /** 68 | * @notice Calculate the address used when bridging an ERC20 token 69 | * @dev the L1 and L2 address oracles may not always be in sync. 70 | * For example, a custom token may have been registered but not deployed or the contract self destructed. 71 | * @param l1ERC20 address of L1 token 72 | * @return L2 address of a bridged ERC20 token 73 | */ 74 | function calculateL2TokenAddress(address l1ERC20) external view returns (address); 75 | } 76 | -------------------------------------------------------------------------------- /contracts/tests/BridgeMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | import "./arbitrum/IBridge.sol"; 6 | 7 | /** 8 | * @title Arbitrum Bridge mock contract 9 | * @dev This contract implements Arbitrum's IBridge interface for testing purposes 10 | */ 11 | contract BridgeMock is IBridge { 12 | /// Address of the (mock) Arbitrum Inbox 13 | address public inbox; 14 | /// Address of the (mock) Arbitrum Outbox 15 | address public outbox; 16 | /// Index of the next message on the inbox messages array 17 | uint256 public messageIndex; 18 | /// Inbox messages array 19 | bytes32[] public override inboxAccs; 20 | 21 | /** 22 | * @notice Deliver a message to the inbox. The encoded message will be 23 | * added to the inbox array, and messageIndex will be incremented. 24 | * @param _kind Type of the message 25 | * @param _sender Address that is sending the message 26 | * @param _messageDataHash keccak256 hash of the message data 27 | * @return The next index for the inbox array 28 | */ 29 | function deliverMessageToInbox( 30 | uint8 _kind, 31 | address _sender, 32 | bytes32 _messageDataHash 33 | ) external payable override returns (uint256) { 34 | messageIndex = messageIndex + 1; 35 | inboxAccs.push(keccak256(abi.encodePacked(inbox, _kind, _sender, _messageDataHash))); 36 | emit MessageDelivered(messageIndex, inboxAccs[messageIndex - 1], msg.sender, _kind, _sender, _messageDataHash); 37 | return messageIndex; 38 | } 39 | 40 | /** 41 | * @notice Executes an L1 function call incoing from L2. This can only be called 42 | * by the Outbox. 43 | * @param _destAddr Contract to call 44 | * @param _amount ETH value to send 45 | * @param _data Calldata for the function call 46 | * @return True if the call was successful, false otherwise 47 | * @return Return data from the call 48 | */ 49 | function executeCall( 50 | address _destAddr, 51 | uint256 _amount, 52 | bytes calldata _data 53 | ) external override returns (bool, bytes memory) { 54 | require(outbox == msg.sender, "NOT_FROM_OUTBOX"); 55 | bool success; 56 | bytes memory returnData; 57 | 58 | // solhint-disable-next-line avoid-low-level-calls 59 | (success, returnData) = _destAddr.call{ value: _amount }(_data); 60 | emit BridgeCallTriggered(msg.sender, _destAddr, _amount, _data); 61 | return (success, returnData); 62 | } 63 | 64 | /** 65 | * @notice Set the address of the inbox. Anyone can call this, because it's a mock. 66 | * @param _inbox Address of the inbox 67 | * @param _enabled Enable the inbox (ignored) 68 | */ 69 | function setInbox(address _inbox, bool _enabled) external override { 70 | inbox = _inbox; 71 | emit InboxToggle(inbox, _enabled); 72 | } 73 | 74 | /** 75 | * @notice Set the address of the outbox. Anyone can call this, because it's a mock. 76 | * @param _outbox Address of the outbox 77 | * @param _enabled Enable the outbox (ignored) 78 | */ 79 | function setOutbox(address _outbox, bool _enabled) external override { 80 | outbox = _outbox; 81 | emit OutboxToggle(outbox, _enabled); 82 | } 83 | 84 | // View functions 85 | 86 | /** 87 | * @notice Getter for the active outbox (in this case there's only one) 88 | */ 89 | function activeOutbox() external view override returns (address) { 90 | return outbox; 91 | } 92 | 93 | /** 94 | * @notice Getter for whether an address is an allowed inbox (in this case there's only one) 95 | * @param _inbox Address to check 96 | * @return True if the address is the allowed inbox, false otherwise 97 | */ 98 | function allowedInboxes(address _inbox) external view override returns (bool) { 99 | return _inbox == inbox; 100 | } 101 | 102 | /** 103 | * @notice Getter for whether an address is an allowed outbox (in this case there's only one) 104 | * @param _outbox Address to check 105 | * @return True if the address is the allowed outbox, false otherwise 106 | */ 107 | function allowedOutboxes(address _outbox) external view override returns (bool) { 108 | return _outbox == outbox; 109 | } 110 | 111 | /** 112 | * @notice Getter for the count of messages in the inboxAccs 113 | * @return Number of messages in inboxAccs 114 | */ 115 | function messageCount() external view override returns (uint256) { 116 | return inboxAccs.length; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /contracts/tests/GraphTokenMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | /** 9 | * @title Graph Token Mock contract. 10 | * @dev Used for testing purposes, DO NOT USE IN PRODUCTION 11 | */ 12 | contract GraphTokenMock is Ownable, ERC20 { 13 | /** 14 | * @notice Contract Constructor. 15 | * @param _initialSupply Initial supply 16 | * @param _mintTo Address to whitch to mint the initial supply 17 | */ 18 | constructor(uint256 _initialSupply, address _mintTo) ERC20("Graph Token Mock", "GRT-Mock") { 19 | // Deploy to mint address 20 | _mint(_mintTo, _initialSupply); 21 | } 22 | 23 | /** 24 | * @notice Mint tokens to an address from the bridge. 25 | * (The real one has an onlyGateway modifier) 26 | * @param _to Address to mint tokens to 27 | * @param _amount Amount of tokens to mint 28 | */ 29 | function bridgeMint(address _to, uint256 _amount) external { 30 | _mint(_to, _amount); 31 | } 32 | 33 | /** 34 | * @notice Burn tokens from an address from the bridge. 35 | * (The real one has an onlyGateway modifier) 36 | * @param _from Address to burn tokens from 37 | * @param _amount Amount of tokens to burn 38 | */ 39 | function bridgeBurn(address _from, uint256 _amount) external { 40 | _burn(_from, _amount); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/tests/InboxMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | import "./arbitrum/IInbox.sol"; 6 | import "./arbitrum/AddressAliasHelper.sol"; 7 | 8 | /** 9 | * @title Arbitrum Inbox mock contract 10 | * @dev This contract implements (a subset of) Arbitrum's IInbox interface for testing purposes 11 | */ 12 | contract InboxMock is IInbox { 13 | /// @dev Type indicator for a standard L2 message 14 | uint8 internal constant L2_MSG = 3; 15 | /// @dev Type indicator for a retryable ticket message 16 | // solhint-disable-next-line const-name-snakecase 17 | uint8 internal constant L1MessageType_submitRetryableTx = 9; 18 | /// Address of the Bridge (mock) contract 19 | IBridge public override bridge; 20 | 21 | /** 22 | * @notice Send a message to L2 (by delivering it to the Bridge) 23 | * @param _messageData Encoded data to send in the message 24 | * @return Message number returned by the inbox 25 | */ 26 | function sendL2Message(bytes calldata _messageData) external override returns (uint256) { 27 | uint256 msgNum = deliverToBridge(L2_MSG, msg.sender, keccak256(_messageData)); 28 | emit InboxMessageDelivered(msgNum, _messageData); 29 | return msgNum; 30 | } 31 | 32 | /** 33 | * @notice Set the address of the (mock) bridge 34 | * @param _bridge Address of the bridge 35 | */ 36 | function setBridge(address _bridge) external { 37 | bridge = IBridge(_bridge); 38 | } 39 | 40 | /** 41 | * @notice Unimplemented in this mock 42 | */ 43 | function sendUnsignedTransaction( 44 | uint256, 45 | uint256, 46 | uint256, 47 | address, 48 | uint256, 49 | bytes calldata 50 | ) external pure override returns (uint256) { 51 | revert("Unimplemented"); 52 | } 53 | 54 | /** 55 | * @notice Unimplemented in this mock 56 | */ 57 | function sendContractTransaction( 58 | uint256, 59 | uint256, 60 | address, 61 | uint256, 62 | bytes calldata 63 | ) external pure override returns (uint256) { 64 | revert("Unimplemented"); 65 | } 66 | 67 | /** 68 | * @notice Unimplemented in this mock 69 | */ 70 | function sendL1FundedUnsignedTransaction( 71 | uint256, 72 | uint256, 73 | uint256, 74 | address, 75 | bytes calldata 76 | ) external payable override returns (uint256) { 77 | revert("Unimplemented"); 78 | } 79 | 80 | /** 81 | * @notice Unimplemented in this mock 82 | */ 83 | function sendL1FundedContractTransaction( 84 | uint256, 85 | uint256, 86 | address, 87 | bytes calldata 88 | ) external payable override returns (uint256) { 89 | revert("Unimplemented"); 90 | } 91 | 92 | /** 93 | * @notice Creates a retryable ticket for an L2 transaction 94 | * @param _destAddr Address of the contract to call in L2 95 | * @param _arbTxCallValue Callvalue to use in the L2 transaction 96 | * @param _maxSubmissionCost Max cost of submitting the ticket, in Wei 97 | * @param _submissionRefundAddress L2 address to refund for any remaining value from the submission cost 98 | * @param _valueRefundAddress L2 address to refund if the ticket times out or gets cancelled 99 | * @param _maxGas Max gas for the L2 transcation 100 | * @param _gasPriceBid Gas price bid on L2 101 | * @param _data Encoded calldata for the L2 transaction (including function selector) 102 | * @return Message number returned by the bridge 103 | */ 104 | function createRetryableTicket( 105 | address _destAddr, 106 | uint256 _arbTxCallValue, 107 | uint256 _maxSubmissionCost, 108 | address _submissionRefundAddress, 109 | address _valueRefundAddress, 110 | uint256 _maxGas, 111 | uint256 _gasPriceBid, 112 | bytes calldata _data 113 | ) external payable override returns (uint256) { 114 | _submissionRefundAddress = AddressAliasHelper.applyL1ToL2Alias(_submissionRefundAddress); 115 | _valueRefundAddress = AddressAliasHelper.applyL1ToL2Alias(_valueRefundAddress); 116 | return 117 | _deliverMessage( 118 | L1MessageType_submitRetryableTx, 119 | msg.sender, 120 | abi.encodePacked( 121 | uint256(uint160(bytes20(_destAddr))), 122 | _arbTxCallValue, 123 | msg.value, 124 | _maxSubmissionCost, 125 | uint256(uint160(bytes20(_submissionRefundAddress))), 126 | uint256(uint160(bytes20(_valueRefundAddress))), 127 | _maxGas, 128 | _gasPriceBid, 129 | _data.length, 130 | _data 131 | ) 132 | ); 133 | } 134 | 135 | /** 136 | * @notice Unimplemented in this mock 137 | */ 138 | function depositEth(uint256) external payable override returns (uint256) { 139 | revert("Unimplemented"); 140 | } 141 | 142 | /** 143 | * @notice Unimplemented in this mock 144 | */ 145 | function pauseCreateRetryables() external pure override { 146 | revert("Unimplemented"); 147 | } 148 | 149 | /** 150 | * @notice Unimplemented in this mock 151 | */ 152 | function unpauseCreateRetryables() external pure override { 153 | revert("Unimplemented"); 154 | } 155 | 156 | /** 157 | * @notice Unimplemented in this mock 158 | */ 159 | function startRewriteAddress() external pure override { 160 | revert("Unimplemented"); 161 | } 162 | 163 | /** 164 | * @notice Unimplemented in this mock 165 | */ 166 | function stopRewriteAddress() external pure override { 167 | revert("Unimplemented"); 168 | } 169 | 170 | /** 171 | * @dev Deliver a message to the bridge 172 | * @param _kind Type of the message 173 | * @param _sender Address that is sending the message 174 | * @param _messageData Encoded message data 175 | * @return Message number returned by the bridge 176 | */ 177 | function _deliverMessage(uint8 _kind, address _sender, bytes memory _messageData) internal returns (uint256) { 178 | uint256 msgNum = deliverToBridge(_kind, _sender, keccak256(_messageData)); 179 | emit InboxMessageDelivered(msgNum, _messageData); 180 | return msgNum; 181 | } 182 | 183 | /** 184 | * @dev Deliver a message to the bridge 185 | * @param _kind Type of the message 186 | * @param _sender Address that is sending the message 187 | * @param _messageDataHash keccak256 hash of the encoded message data 188 | * @return Message number returned by the bridge 189 | */ 190 | function deliverToBridge(uint8 _kind, address _sender, bytes32 _messageDataHash) internal returns (uint256) { 191 | return bridge.deliverMessageToInbox{ value: msg.value }(_kind, _sender, _messageDataHash); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /contracts/tests/L1TokenGatewayMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; 6 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; 8 | import { ITokenGateway } from "../arbitrum//ITokenGateway.sol"; 9 | 10 | /** 11 | * @title L1 Token Gateway mock contract 12 | * @dev Used for testing purposes, DO NOT USE IN PRODUCTION 13 | */ 14 | contract L1TokenGatewayMock is Ownable { 15 | using SafeMath for uint256; 16 | /// Next sequence number to return when outboundTransfer is called 17 | uint256 public nextSeqNum; 18 | 19 | /// @dev Emitted when a (fake) retryable ticket is created 20 | event FakeTxToL2( 21 | address from, 22 | uint256 value, 23 | uint256 maxGas, 24 | uint256 gasPriceBid, 25 | uint256 maxSubmissionCost, 26 | bytes outboundCalldata 27 | ); 28 | 29 | /// @dev Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 30 | event DepositInitiated( 31 | address l1Token, 32 | address indexed from, 33 | address indexed to, 34 | uint256 indexed sequenceNumber, 35 | uint256 amount 36 | ); 37 | 38 | /** 39 | * @notice L1 Token Gateway Contract Constructor. 40 | */ 41 | constructor() {} 42 | 43 | /** 44 | * @notice Creates and sends a fake retryable ticket to transfer GRT to L2. 45 | * This mock will actually just emit an event with parameters equivalent to what the real L1GraphTokenGateway 46 | * would send to L2. 47 | * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) 48 | * @param _to Recipient address on L2 49 | * @param _amount Amount of tokens to tranfer 50 | * @param _maxGas Gas limit for L2 execution of the ticket 51 | * @param _gasPriceBid Price per gas on L2 52 | * @param _data Encoded maxSubmissionCost and sender address along with additional calldata 53 | * @return Sequence number of the retryable ticket created by Inbox (always ) 54 | */ 55 | function outboundTransfer( 56 | address _l1Token, 57 | address _to, 58 | uint256 _amount, 59 | uint256 _maxGas, 60 | uint256 _gasPriceBid, 61 | bytes calldata _data 62 | ) external payable returns (bytes memory) { 63 | require(_amount > 0, "INVALID_ZERO_AMOUNT"); 64 | require(_to != address(0), "INVALID_DESTINATION"); 65 | 66 | // nested scopes to avoid stack too deep errors 67 | address from; 68 | uint256 seqNum = nextSeqNum; 69 | nextSeqNum += 1; 70 | { 71 | uint256 maxSubmissionCost; 72 | bytes memory outboundCalldata; 73 | { 74 | bytes memory extraData; 75 | (from, maxSubmissionCost, extraData) = _parseOutboundData(_data); 76 | require(maxSubmissionCost > 0, "NO_SUBMISSION_COST"); 77 | 78 | { 79 | // makes sure only sufficient ETH is supplied as required for successful redemption on L2 80 | // if a user does not desire immediate redemption they should provide 81 | // a msg.value of AT LEAST maxSubmissionCost 82 | uint256 expectedEth = maxSubmissionCost.add(_maxGas.mul(_gasPriceBid)); 83 | require(msg.value >= expectedEth, "WRONG_ETH_VALUE"); 84 | } 85 | outboundCalldata = getOutboundCalldata(_l1Token, from, _to, _amount, extraData); 86 | } 87 | { 88 | // transfer tokens to escrow 89 | IERC20(_l1Token).transferFrom(from, address(this), _amount); 90 | 91 | emit FakeTxToL2(from, msg.value, _maxGas, _gasPriceBid, maxSubmissionCost, outboundCalldata); 92 | } 93 | } 94 | emit DepositInitiated(_l1Token, from, _to, seqNum, _amount); 95 | 96 | return abi.encode(seqNum); 97 | } 98 | 99 | /** 100 | * @notice (Mock) Receives withdrawn tokens from L2 101 | * Actually does nothing, just keeping it here as its useful to define the expected 102 | * calldata for the outgoing transfer in tests. 103 | * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) 104 | * @param _from Address of the sender 105 | * @param _to Recepient address on L1 106 | * @param _amount Amount of tokens transferred 107 | * @param _data Additional calldata 108 | */ 109 | function finalizeInboundTransfer( 110 | address _l1Token, 111 | address _from, 112 | address _to, 113 | uint256 _amount, 114 | bytes calldata _data 115 | ) external payable {} 116 | 117 | /** 118 | * @notice Creates calldata required to create a retryable ticket 119 | * @dev encodes the target function with its params which 120 | * will be called on L2 when the retryable ticket is redeemed 121 | * @param _l1Token Address of the Graph token contract on L1 122 | * @param _from Address on L1 from which we're transferring tokens 123 | * @param _to Address on L2 to which we're transferring tokens 124 | * @param _amount Amount of GRT to transfer 125 | * @param _data Additional call data for the L2 transaction, which must be empty unless the caller is whitelisted 126 | * @return Encoded calldata (including function selector) for the L2 transaction 127 | */ 128 | function getOutboundCalldata( 129 | address _l1Token, 130 | address _from, 131 | address _to, 132 | uint256 _amount, 133 | bytes memory _data 134 | ) public pure returns (bytes memory) { 135 | bytes memory emptyBytes; 136 | 137 | return 138 | abi.encodeWithSelector( 139 | ITokenGateway.finalizeInboundTransfer.selector, 140 | _l1Token, 141 | _from, 142 | _to, 143 | _amount, 144 | abi.encode(emptyBytes, _data) 145 | ); 146 | } 147 | 148 | /** 149 | * @notice Decodes calldata required for transfer of tokens to L2 150 | * @dev Data must include maxSubmissionCost, extraData can be left empty. When the router 151 | * sends an outbound message, data also contains the from address, but this mock 152 | * doesn't consider this case 153 | * @param _data Encoded callhook data containing maxSubmissionCost and extraData 154 | * @return Sender of the tx 155 | * @return Max ether value used to submit the retryable ticket 156 | * @return Additional data sent to L2 157 | */ 158 | function _parseOutboundData(bytes memory _data) private view returns (address, uint256, bytes memory) { 159 | address from; 160 | uint256 maxSubmissionCost; 161 | bytes memory extraData; 162 | from = msg.sender; 163 | // User-encoded data contains the max retryable ticket submission cost 164 | // and additional L2 calldata 165 | (maxSubmissionCost, extraData) = abi.decode(_data, (uint256, bytes)); 166 | return (from, maxSubmissionCost, extraData); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /contracts/tests/L2TokenGatewayMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | 5 | import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; 6 | import { ITokenGateway } from "../arbitrum//ITokenGateway.sol"; 7 | import { GraphTokenMock } from "./GraphTokenMock.sol"; 8 | import { ICallhookReceiver } from "../ICallhookReceiver.sol"; 9 | 10 | /** 11 | * @title L2 Token Gateway mock contract 12 | * @dev Used for testing purposes, DO NOT USE IN PRODUCTION 13 | */ 14 | contract L2TokenGatewayMock is Ownable { 15 | /// Address of the L1 GRT contract 16 | address public immutable l1Token; 17 | /// Address of the L2 GRT contract 18 | address public immutable l2Token; 19 | /// Next ID to return when sending an outboundTransfer 20 | uint256 public nextId; 21 | 22 | /// @dev Emitted when a (fake) transaction to L1 is created 23 | event FakeTxToL1(address from, bytes outboundCalldata); 24 | /// @dev Emitted when a (fake) retryable ticket is received from L1 25 | event DepositFinalized(address token, address indexed from, address indexed to, uint256 amount); 26 | 27 | /// @dev Emitted when an outbound transfer is initiated, i.e. tokens are withdrawn to L1 from L2 28 | event WithdrawalInitiated( 29 | address l1Token, 30 | address indexed from, 31 | address indexed to, 32 | uint256 indexed sequenceNumber, 33 | uint256 amount 34 | ); 35 | 36 | /** 37 | * @notice L2 Token Gateway Contract Constructor. 38 | * @param _l1Token Address of the L1 GRT contract 39 | * @param _l2Token Address of the L2 GRT contract 40 | */ 41 | constructor(address _l1Token, address _l2Token) { 42 | l1Token = _l1Token; 43 | l2Token = _l2Token; 44 | } 45 | 46 | /** 47 | * @notice Creates and sends a (fake) transfer of GRT to L1. 48 | * This mock will actually just emit an event with parameters equivalent to what the real L2GraphTokenGateway 49 | * would send to L1. 50 | * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) 51 | * @param _to Recipient address on L2 52 | * @param _amount Amount of tokens to tranfer 53 | * @param _data Encoded maxSubmissionCost and sender address along with additional calldata 54 | * @return ID of the L2-L1 message (incrementing on every call) 55 | */ 56 | function outboundTransfer( 57 | address _l1Token, 58 | address _to, 59 | uint256 _amount, 60 | uint256, 61 | uint256, 62 | bytes calldata _data 63 | ) external payable returns (bytes memory) { 64 | require(_l1Token == l1Token, "INVALID_L1_TOKEN"); 65 | require(_amount > 0, "INVALID_ZERO_AMOUNT"); 66 | require(_to != address(0), "INVALID_DESTINATION"); 67 | 68 | // nested scopes to avoid stack too deep errors 69 | address from; 70 | uint256 id = nextId; 71 | nextId += 1; 72 | { 73 | bytes memory outboundCalldata; 74 | { 75 | bytes memory extraData; 76 | (from, extraData) = _parseOutboundData(_data); 77 | 78 | require(msg.value == 0, "!value"); 79 | require(extraData.length == 0, "!extraData"); 80 | outboundCalldata = getOutboundCalldata(_l1Token, from, _to, _amount, extraData); 81 | } 82 | { 83 | // burn tokens from the sender, they will be released from escrow in L1 84 | GraphTokenMock(l2Token).bridgeBurn(from, _amount); 85 | 86 | emit FakeTxToL1(from, outboundCalldata); 87 | } 88 | } 89 | emit WithdrawalInitiated(_l1Token, from, _to, id, _amount); 90 | 91 | return abi.encode(id); 92 | } 93 | 94 | /** 95 | * @notice (Mock) Receives withdrawn tokens from L1 96 | * Implements calling callhooks if data is non-empty. 97 | * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) 98 | * @param _from Address of the sender 99 | * @param _to Recipient address on L1 100 | * @param _amount Amount of tokens transferred 101 | * @param _data Additional calldata, will trigger an onTokenTransfer call if non-empty 102 | */ 103 | function finalizeInboundTransfer( 104 | address _l1Token, 105 | address _from, 106 | address _to, 107 | uint256 _amount, 108 | bytes calldata _data 109 | ) external payable { 110 | require(_l1Token == l1Token, "TOKEN_NOT_GRT"); 111 | require(msg.value == 0, "INVALID_NONZERO_VALUE"); 112 | 113 | GraphTokenMock(l2Token).bridgeMint(_to, _amount); 114 | 115 | if (_data.length > 0) { 116 | ICallhookReceiver(_to).onTokenTransfer(_from, _amount, _data); 117 | } 118 | 119 | emit DepositFinalized(_l1Token, _from, _to, _amount); 120 | } 121 | 122 | /** 123 | * @notice Calculate the L2 address of a bridged token 124 | * @dev In our case, this would only work for GRT. 125 | * @param l1ERC20 address of L1 GRT contract 126 | * @return L2 address of the bridged GRT token 127 | */ 128 | function calculateL2TokenAddress(address l1ERC20) public view returns (address) { 129 | if (l1ERC20 != l1Token) { 130 | return address(0); 131 | } 132 | return l2Token; 133 | } 134 | 135 | /** 136 | * @notice Creates calldata required to create a tx to L1 137 | * @param _l1Token Address of the Graph token contract on L1 138 | * @param _from Address on L2 from which we're transferring tokens 139 | * @param _to Address on L1 to which we're transferring tokens 140 | * @param _amount Amount of GRT to transfer 141 | * @param _data Additional call data for the L1 transaction, which must be empty 142 | * @return Encoded calldata (including function selector) for the L1 transaction 143 | */ 144 | function getOutboundCalldata( 145 | address _l1Token, 146 | address _from, 147 | address _to, 148 | uint256 _amount, 149 | bytes memory _data 150 | ) public pure returns (bytes memory) { 151 | return 152 | abi.encodeWithSelector( 153 | ITokenGateway.finalizeInboundTransfer.selector, 154 | _l1Token, 155 | _from, 156 | _to, 157 | _amount, 158 | abi.encode(0, _data) 159 | ); 160 | } 161 | 162 | /** 163 | * @dev Decodes calldata required for transfer of tokens to L1. 164 | * extraData can be left empty 165 | * @param _data Encoded callhook data 166 | * @return Sender of the tx 167 | * @return Any other data sent to L1 168 | */ 169 | function _parseOutboundData(bytes calldata _data) private view returns (address, bytes memory) { 170 | address from; 171 | bytes memory extraData; 172 | // The mock doesn't take messages from the Router 173 | from = msg.sender; 174 | extraData = _data; 175 | return (from, extraData); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /contracts/tests/Stakes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/contracts/math/SafeMath.sol"; 7 | 8 | /** 9 | * @title A collection of data structures and functions to manage the Indexer Stake state. 10 | * Used for low-level state changes, require() conditions should be evaluated 11 | * at the caller function scope. 12 | */ 13 | library Stakes { 14 | using SafeMath for uint256; 15 | using Stakes for Stakes.Indexer; 16 | 17 | struct Indexer { 18 | uint256 tokensStaked; // Tokens on the indexer stake (staked by the indexer) 19 | uint256 tokensAllocated; // Tokens used in allocations 20 | uint256 tokensLocked; // Tokens locked for withdrawal subject to thawing period 21 | uint256 tokensLockedUntil; // Block when locked tokens can be withdrawn 22 | } 23 | 24 | /** 25 | * @dev Deposit tokens to the indexer stake. 26 | * @param stake Stake data 27 | * @param _tokens Amount of tokens to deposit 28 | */ 29 | function deposit(Stakes.Indexer storage stake, uint256 _tokens) internal { 30 | stake.tokensStaked = stake.tokensStaked.add(_tokens); 31 | } 32 | 33 | /** 34 | * @dev Release tokens from the indexer stake. 35 | * @param stake Stake data 36 | * @param _tokens Amount of tokens to release 37 | */ 38 | function release(Stakes.Indexer storage stake, uint256 _tokens) internal { 39 | stake.tokensStaked = stake.tokensStaked.sub(_tokens); 40 | } 41 | 42 | /** 43 | * @dev Allocate tokens from the main stack to a SubgraphDeployment. 44 | * @param stake Stake data 45 | * @param _tokens Amount of tokens to allocate 46 | */ 47 | function allocate(Stakes.Indexer storage stake, uint256 _tokens) internal { 48 | stake.tokensAllocated = stake.tokensAllocated.add(_tokens); 49 | } 50 | 51 | /** 52 | * @dev Unallocate tokens from a SubgraphDeployment back to the main stack. 53 | * @param stake Stake data 54 | * @param _tokens Amount of tokens to unallocate 55 | */ 56 | function unallocate(Stakes.Indexer storage stake, uint256 _tokens) internal { 57 | stake.tokensAllocated = stake.tokensAllocated.sub(_tokens); 58 | } 59 | 60 | /** 61 | * @dev Lock tokens until a thawing period pass. 62 | * @param stake Stake data 63 | * @param _tokens Amount of tokens to unstake 64 | * @param _period Period in blocks that need to pass before withdrawal 65 | */ 66 | function lockTokens(Stakes.Indexer storage stake, uint256 _tokens, uint256 _period) internal { 67 | // Take into account period averaging for multiple unstake requests 68 | uint256 lockingPeriod = _period; 69 | if (stake.tokensLocked > 0) { 70 | lockingPeriod = stake.getLockingPeriod(_tokens, _period); 71 | } 72 | 73 | // Update balances 74 | stake.tokensLocked = stake.tokensLocked.add(_tokens); 75 | stake.tokensLockedUntil = block.number.add(lockingPeriod); 76 | } 77 | 78 | /** 79 | * @dev Unlock tokens. 80 | * @param stake Stake data 81 | * @param _tokens Amount of tokens to unkock 82 | */ 83 | function unlockTokens(Stakes.Indexer storage stake, uint256 _tokens) internal { 84 | stake.tokensLocked = stake.tokensLocked.sub(_tokens); 85 | if (stake.tokensLocked == 0) { 86 | stake.tokensLockedUntil = 0; 87 | } 88 | } 89 | 90 | /** 91 | * @dev Take all tokens out from the locked stake for withdrawal. 92 | * @param stake Stake data 93 | * @return Amount of tokens being withdrawn 94 | */ 95 | function withdrawTokens(Stakes.Indexer storage stake) internal returns (uint256) { 96 | // Calculate tokens that can be released 97 | uint256 tokensToWithdraw = stake.tokensWithdrawable(); 98 | 99 | if (tokensToWithdraw > 0) { 100 | // Reset locked tokens 101 | stake.unlockTokens(tokensToWithdraw); 102 | 103 | // Decrease indexer stake 104 | stake.release(tokensToWithdraw); 105 | } 106 | 107 | return tokensToWithdraw; 108 | } 109 | 110 | /** 111 | * @dev Get the locking period of the tokens to unstake. 112 | * If already unstaked before calculate the weighted average. 113 | * @param stake Stake data 114 | * @param _tokens Amount of tokens to unstake 115 | * @param _thawingPeriod Period in blocks that need to pass before withdrawal 116 | * @return True if staked 117 | */ 118 | function getLockingPeriod( 119 | Stakes.Indexer memory stake, 120 | uint256 _tokens, 121 | uint256 _thawingPeriod 122 | ) internal view returns (uint256) { 123 | uint256 blockNum = block.number; 124 | uint256 periodA = (stake.tokensLockedUntil > blockNum) ? stake.tokensLockedUntil.sub(blockNum) : 0; 125 | uint256 periodB = _thawingPeriod; 126 | uint256 stakeA = stake.tokensLocked; 127 | uint256 stakeB = _tokens; 128 | return periodA.mul(stakeA).add(periodB.mul(stakeB)).div(stakeA.add(stakeB)); 129 | } 130 | 131 | /** 132 | * @dev Return true if there are tokens staked by the Indexer. 133 | * @param stake Stake data 134 | * @return True if staked 135 | */ 136 | function hasTokens(Stakes.Indexer memory stake) internal pure returns (bool) { 137 | return stake.tokensStaked > 0; 138 | } 139 | 140 | /** 141 | * @dev Return the amount of tokens used in allocations and locked for withdrawal. 142 | * @param stake Stake data 143 | * @return Token amount 144 | */ 145 | function tokensUsed(Stakes.Indexer memory stake) internal pure returns (uint256) { 146 | return stake.tokensAllocated.add(stake.tokensLocked); 147 | } 148 | 149 | /** 150 | * @dev Return the amount of tokens staked not considering the ones that are already going 151 | * through the thawing period or are ready for withdrawal. We call it secure stake because 152 | * it is not subject to change by a withdraw call from the indexer. 153 | * @param stake Stake data 154 | * @return Token amount 155 | */ 156 | function tokensSecureStake(Stakes.Indexer memory stake) internal pure returns (uint256) { 157 | return stake.tokensStaked.sub(stake.tokensLocked); 158 | } 159 | 160 | /** 161 | * @dev Tokens free balance on the indexer stake that can be used for any purpose. 162 | * Any token that is allocated cannot be used as well as tokens that are going through the 163 | * thawing period or are withdrawable 164 | * Calc: tokensStaked - tokensAllocated - tokensLocked 165 | * @param stake Stake data 166 | * @return Token amount 167 | */ 168 | function tokensAvailable(Stakes.Indexer memory stake) internal pure returns (uint256) { 169 | return stake.tokensAvailableWithDelegation(0); 170 | } 171 | 172 | /** 173 | * @dev Tokens free balance on the indexer stake that can be used for allocations. 174 | * This function accepts a parameter for extra delegated capacity that takes into 175 | * account delegated tokens 176 | * @param stake Stake data 177 | * @param _delegatedCapacity Amount of tokens used from delegators to calculate availability 178 | * @return Token amount 179 | */ 180 | function tokensAvailableWithDelegation( 181 | Stakes.Indexer memory stake, 182 | uint256 _delegatedCapacity 183 | ) internal pure returns (uint256) { 184 | uint256 tokensCapacity = stake.tokensStaked.add(_delegatedCapacity); 185 | uint256 _tokensUsed = stake.tokensUsed(); 186 | // If more tokens are used than the current capacity, the indexer is overallocated. 187 | // This means the indexer doesn't have available capacity to create new allocations. 188 | // We can reach this state when the indexer has funds allocated and then any 189 | // of these conditions happen: 190 | // - The delegationCapacity ratio is reduced. 191 | // - The indexer stake is slashed. 192 | // - A delegator removes enough stake. 193 | if (_tokensUsed > tokensCapacity) { 194 | // Indexer stake is over allocated: return 0 to avoid stake to be used until 195 | // the overallocation is restored by staking more tokens, unallocating tokens 196 | // or using more delegated funds 197 | return 0; 198 | } 199 | return tokensCapacity.sub(_tokensUsed); 200 | } 201 | 202 | /** 203 | * @dev Tokens available for withdrawal after thawing period. 204 | * @param stake Stake data 205 | * @return Token amount 206 | */ 207 | function tokensWithdrawable(Stakes.Indexer memory stake) internal view returns (uint256) { 208 | // No tokens to withdraw before locking period 209 | if (stake.tokensLockedUntil == 0 || block.number < stake.tokensLockedUntil) { 210 | return 0; 211 | } 212 | return stake.tokensLocked; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /contracts/tests/StakingMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | import "./Stakes.sol"; 9 | 10 | contract StakingMock { 11 | using SafeMath for uint256; 12 | using Stakes for Stakes.Indexer; 13 | 14 | // -- State -- 15 | 16 | uint256 public minimumIndexerStake = 100e18; 17 | uint256 public thawingPeriod = 10; // 10 blocks 18 | IERC20 public token; 19 | 20 | // Indexer stakes : indexer => Stake 21 | mapping(address => Stakes.Indexer) public stakes; 22 | 23 | /** 24 | * @dev Emitted when `indexer` stake `tokens` amount. 25 | */ 26 | event StakeDeposited(address indexed indexer, uint256 tokens); 27 | 28 | /** 29 | * @dev Emitted when `indexer` unstaked and locked `tokens` amount `until` block. 30 | */ 31 | event StakeLocked(address indexed indexer, uint256 tokens, uint256 until); 32 | 33 | /** 34 | * @dev Emitted when `indexer` withdrew `tokens` staked. 35 | */ 36 | event StakeWithdrawn(address indexed indexer, uint256 tokens); 37 | 38 | // Contract constructor. 39 | constructor(IERC20 _token) { 40 | require(address(_token) != address(0), "!token"); 41 | token = _token; 42 | } 43 | 44 | receive() external payable {} 45 | 46 | /** 47 | * @dev Deposit tokens on the indexer stake. 48 | * @param _tokens Amount of tokens to stake 49 | */ 50 | function stake(uint256 _tokens) external { 51 | stakeTo(msg.sender, _tokens); 52 | } 53 | 54 | /** 55 | * @dev Deposit tokens on the indexer stake. 56 | * @param _indexer Address of the indexer 57 | * @param _tokens Amount of tokens to stake 58 | */ 59 | function stakeTo(address _indexer, uint256 _tokens) public { 60 | require(_tokens > 0, "!tokens"); 61 | 62 | // Ensure minimum stake 63 | require(stakes[_indexer].tokensSecureStake().add(_tokens) >= minimumIndexerStake, "!minimumIndexerStake"); 64 | 65 | // Transfer tokens to stake from caller to this contract 66 | require(token.transferFrom(msg.sender, address(this), _tokens), "!transfer"); 67 | 68 | // Stake the transferred tokens 69 | _stake(_indexer, _tokens); 70 | } 71 | 72 | /** 73 | * @dev Unstake tokens from the indexer stake, lock them until thawing period expires. 74 | * @param _tokens Amount of tokens to unstake 75 | */ 76 | function unstake(uint256 _tokens) external { 77 | address indexer = msg.sender; 78 | Stakes.Indexer storage indexerStake = stakes[indexer]; 79 | 80 | require(_tokens > 0, "!tokens"); 81 | require(indexerStake.hasTokens(), "!stake"); 82 | require(indexerStake.tokensAvailable() >= _tokens, "!stake-avail"); 83 | 84 | // Ensure minimum stake 85 | uint256 newStake = indexerStake.tokensSecureStake().sub(_tokens); 86 | require(newStake == 0 || newStake >= minimumIndexerStake, "!minimumIndexerStake"); 87 | 88 | // Before locking more tokens, withdraw any unlocked ones 89 | uint256 tokensToWithdraw = indexerStake.tokensWithdrawable(); 90 | if (tokensToWithdraw > 0) { 91 | _withdraw(indexer); 92 | } 93 | 94 | indexerStake.lockTokens(_tokens, thawingPeriod); 95 | 96 | emit StakeLocked(indexer, indexerStake.tokensLocked, indexerStake.tokensLockedUntil); 97 | } 98 | 99 | /** 100 | * @dev Withdraw indexer tokens once the thawing period has passed. 101 | */ 102 | function withdraw() external { 103 | _withdraw(msg.sender); 104 | } 105 | 106 | function _stake(address _indexer, uint256 _tokens) internal { 107 | // Deposit tokens into the indexer stake 108 | Stakes.Indexer storage indexerStake = stakes[_indexer]; 109 | indexerStake.deposit(_tokens); 110 | 111 | emit StakeDeposited(_indexer, _tokens); 112 | } 113 | 114 | /** 115 | * @dev Withdraw indexer tokens once the thawing period has passed. 116 | * @param _indexer Address of indexer to withdraw funds from 117 | */ 118 | function _withdraw(address _indexer) private { 119 | // Get tokens available for withdraw and update balance 120 | uint256 tokensToWithdraw = stakes[_indexer].withdrawTokens(); 121 | require(tokensToWithdraw > 0, "!tokens"); 122 | 123 | // Return tokens to the indexer 124 | require(token.transfer(_indexer, tokensToWithdraw), "!transfer"); 125 | 126 | emit StakeWithdrawn(_indexer, tokensToWithdraw); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /contracts/tests/WalletMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.3; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 7 | 8 | /** 9 | * @title WalletMock: a mock wallet contract for testing purposes 10 | * @dev For testing only, DO NOT USE IN PRODUCTION. 11 | * This is used to test L1-L2 transfer tools and to create scenarios 12 | * where an invalid wallet calls the transfer tool, e.g. a wallet that has an invalid 13 | * manager, or a wallet that has not been initialized. 14 | */ 15 | contract WalletMock { 16 | /// Target contract for the fallback function (usually a transfer tool contract) 17 | address public immutable target; 18 | /// Address of the GRT (mock) token 19 | address public immutable token; 20 | /// Address of the wallet's manager 21 | address public immutable manager; 22 | /// Whether the wallet has been initialized 23 | bool public immutable isInitialized; 24 | /// Whether the beneficiary has accepted the lock 25 | bool public immutable isAccepted; 26 | 27 | /** 28 | * @notice WalletMock constructor 29 | * @dev This constructor sets all the state variables so that 30 | * specific test scenarios can be created just by deploying this contract. 31 | * @param _target Target contract for the fallback function 32 | * @param _token Address of the GRT (mock) token 33 | * @param _manager Address of the wallet's manager 34 | * @param _isInitialized Whether the wallet has been initialized 35 | * @param _isAccepted Whether the beneficiary has accepted the lock 36 | */ 37 | constructor(address _target, address _token, address _manager, bool _isInitialized, bool _isAccepted) { 38 | target = _target; 39 | token = _token; 40 | manager = _manager; 41 | isInitialized = _isInitialized; 42 | isAccepted = _isAccepted; 43 | } 44 | 45 | /** 46 | * @notice Fallback function 47 | * @dev This function calls the target contract with the data sent to this contract. 48 | * This is used to test the L1-L2 transfer tool. 49 | */ 50 | fallback() external payable { 51 | // Call function with data 52 | Address.functionCall(target, msg.data); 53 | } 54 | 55 | /** 56 | * @notice Receive function 57 | * @dev This function is added to avoid compiler warnings, but just reverts. 58 | */ 59 | receive() external payable { 60 | revert("Invalid call"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /contracts/tests/arbitrum/AddressAliasHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2019-2021, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * Originally copied from: 19 | * https://github.com/OffchainLabs/arbitrum/tree/84e64dee6ee82adbf8ec34fd4b86c207a61d9007/packages/arb-bridge-eth 20 | * 21 | * MODIFIED from Offchain Labs' implementation: 22 | * - Changed solidity version to 0.7.3 (pablo@edgeandnode.com) 23 | * 24 | */ 25 | 26 | pragma solidity ^0.7.3; 27 | 28 | library AddressAliasHelper { 29 | uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); 30 | 31 | /// @notice Utility function that converts the address in the L1 that submitted a tx to 32 | /// the inbox to the msg.sender viewed in the L2 33 | /// @param l1Address the address in the L1 that triggered the tx to L2 34 | /// @return l2Address L2 address as viewed in msg.sender 35 | function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { 36 | l2Address = address(uint160(l1Address) + offset); 37 | } 38 | 39 | /// @notice Utility function that converts the msg.sender viewed in the L2 to the 40 | /// address in the L1 that submitted a tx to the inbox 41 | /// @param l2Address L2 address as viewed in msg.sender 42 | /// @return l1Address the address in the L1 that triggered the tx to L2 43 | function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { 44 | l1Address = address(uint160(l2Address) - offset); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /contracts/tests/arbitrum/IBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2021, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * Originally copied from: 19 | * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth 20 | * 21 | * MODIFIED from Offchain Labs' implementation: 22 | * - Changed solidity version to 0.7.3 (pablo@edgeandnode.com) 23 | * 24 | */ 25 | 26 | pragma solidity ^0.7.3; 27 | 28 | interface IBridge { 29 | event MessageDelivered( 30 | uint256 indexed messageIndex, 31 | bytes32 indexed beforeInboxAcc, 32 | address inbox, 33 | uint8 kind, 34 | address sender, 35 | bytes32 messageDataHash 36 | ); 37 | 38 | event BridgeCallTriggered(address indexed outbox, address indexed destAddr, uint256 amount, bytes data); 39 | 40 | event InboxToggle(address indexed inbox, bool enabled); 41 | 42 | event OutboxToggle(address indexed outbox, bool enabled); 43 | 44 | function deliverMessageToInbox( 45 | uint8 kind, 46 | address sender, 47 | bytes32 messageDataHash 48 | ) external payable returns (uint256); 49 | 50 | function executeCall( 51 | address destAddr, 52 | uint256 amount, 53 | bytes calldata data 54 | ) external returns (bool success, bytes memory returnData); 55 | 56 | // These are only callable by the admin 57 | function setInbox(address inbox, bool enabled) external; 58 | 59 | function setOutbox(address inbox, bool enabled) external; 60 | 61 | // View functions 62 | 63 | function activeOutbox() external view returns (address); 64 | 65 | function allowedInboxes(address inbox) external view returns (bool); 66 | 67 | function allowedOutboxes(address outbox) external view returns (bool); 68 | 69 | function inboxAccs(uint256 index) external view returns (bytes32); 70 | 71 | function messageCount() external view returns (uint256); 72 | } 73 | -------------------------------------------------------------------------------- /contracts/tests/arbitrum/IInbox.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2021, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * Originally copied from: 19 | * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth 20 | * 21 | * MODIFIED from Offchain Labs' implementation: 22 | * - Changed solidity version to 0.7.3 (pablo@edgeandnode.com) 23 | * 24 | */ 25 | 26 | pragma solidity ^0.7.3; 27 | 28 | import "./IBridge.sol"; 29 | import "./IMessageProvider.sol"; 30 | 31 | interface IInbox is IMessageProvider { 32 | function sendL2Message(bytes calldata messageData) external returns (uint256); 33 | 34 | function sendUnsignedTransaction( 35 | uint256 maxGas, 36 | uint256 gasPriceBid, 37 | uint256 nonce, 38 | address destAddr, 39 | uint256 amount, 40 | bytes calldata data 41 | ) external returns (uint256); 42 | 43 | function sendContractTransaction( 44 | uint256 maxGas, 45 | uint256 gasPriceBid, 46 | address destAddr, 47 | uint256 amount, 48 | bytes calldata data 49 | ) external returns (uint256); 50 | 51 | function sendL1FundedUnsignedTransaction( 52 | uint256 maxGas, 53 | uint256 gasPriceBid, 54 | uint256 nonce, 55 | address destAddr, 56 | bytes calldata data 57 | ) external payable returns (uint256); 58 | 59 | function sendL1FundedContractTransaction( 60 | uint256 maxGas, 61 | uint256 gasPriceBid, 62 | address destAddr, 63 | bytes calldata data 64 | ) external payable returns (uint256); 65 | 66 | function createRetryableTicket( 67 | address destAddr, 68 | uint256 arbTxCallValue, 69 | uint256 maxSubmissionCost, 70 | address submissionRefundAddress, 71 | address valueRefundAddress, 72 | uint256 maxGas, 73 | uint256 gasPriceBid, 74 | bytes calldata data 75 | ) external payable returns (uint256); 76 | 77 | function depositEth(uint256 maxSubmissionCost) external payable returns (uint256); 78 | 79 | function bridge() external view returns (IBridge); 80 | 81 | function pauseCreateRetryables() external; 82 | 83 | function unpauseCreateRetryables() external; 84 | 85 | function startRewriteAddress() external; 86 | 87 | function stopRewriteAddress() external; 88 | } 89 | -------------------------------------------------------------------------------- /contracts/tests/arbitrum/IMessageProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2021, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * Originally copied from: 19 | * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth 20 | * 21 | * MODIFIED from Offchain Labs' implementation: 22 | * - Changed solidity version to 0.7.3 (pablo@edgeandnode.com) 23 | * 24 | */ 25 | 26 | pragma solidity ^0.7.3; 27 | 28 | interface IMessageProvider { 29 | event InboxMessageDelivered(uint256 indexed messageNum, bytes data); 30 | 31 | event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); 32 | } 33 | -------------------------------------------------------------------------------- /deploy/1_test.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | import consola from 'consola' 3 | 4 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 5 | import { DeployFunction } from 'hardhat-deploy/types' 6 | 7 | const { parseEther } = utils 8 | 9 | const logger = consola.create({}) 10 | 11 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 12 | const { deploy } = hre.deployments 13 | const { deployer } = await hre.getNamedAccounts() 14 | 15 | // -- Fake Graph Token -- 16 | 17 | logger.info('Deploying GraphTokenMock...') 18 | 19 | await deploy('GraphTokenMock', { 20 | from: deployer, 21 | args: [ 22 | parseEther('10000000000'), // 10B 23 | deployer, 24 | ], 25 | log: true, 26 | }) 27 | } 28 | 29 | func.skip = (hre: HardhatRuntimeEnvironment) => Promise.resolve(hre.network.name === 'mainnet') 30 | func.tags = ['test'] 31 | 32 | export default func 33 | -------------------------------------------------------------------------------- /deploy/2_l1_manager_wallet.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { utils } from 'ethers' 3 | 4 | import '@nomiclabs/hardhat-ethers' 5 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 6 | import { DeployFunction } from 'hardhat-deploy/types' 7 | 8 | import { GraphTokenMock } from '../build/typechain/contracts/GraphTokenMock' 9 | import { GraphTokenLockManager } from '../build/typechain/contracts/GraphTokenLockManager' 10 | import { askConfirm, getDeploymentName, promptContractAddress } from './lib/utils' 11 | 12 | const { parseEther, formatEther } = utils 13 | 14 | const logger = consola.create({}) 15 | 16 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 17 | const { deploy } = hre.deployments 18 | const { deployer } = await hre.getNamedAccounts() 19 | 20 | // -- Graph Token -- 21 | 22 | // Get the token address we will use 23 | const tokenAddress = await promptContractAddress('L1 GRT', logger) 24 | if (!tokenAddress) { 25 | logger.warn('No token address provided') 26 | process.exit(1) 27 | } 28 | 29 | // -- Token Lock Manager -- 30 | 31 | // Deploy the master copy of GraphTokenLockWallet 32 | logger.info('Deploying GraphTokenLockWallet master copy...') 33 | const masterCopySaveName = await getDeploymentName('GraphTokenLockWallet') 34 | const masterCopyDeploy = await deploy(masterCopySaveName, { 35 | from: deployer, 36 | log: true, 37 | contract: 'GraphTokenLockWallet', 38 | }) 39 | 40 | // Deploy the Manager that uses the master copy to clone contracts 41 | logger.info('Deploying GraphTokenLockManager...') 42 | const managerSaveName = await getDeploymentName('GraphTokenLockManager') 43 | const managerDeploy = await deploy(managerSaveName, { 44 | from: deployer, 45 | args: [tokenAddress, masterCopyDeploy.address], 46 | log: true, 47 | contract: 'GraphTokenLockManager', 48 | }) 49 | 50 | // -- Fund -- 51 | 52 | if (await askConfirm('Do you want to fund the manager?')) { 53 | const fundAmount = parseEther('100000000') 54 | logger.info(`Funding ${managerDeploy.address} with ${formatEther(fundAmount)} GRT...`) 55 | 56 | // Approve 57 | const grt = (await hre.ethers.getContractAt('GraphTokenMock', tokenAddress)) as GraphTokenMock 58 | await grt.approve(managerDeploy.address, fundAmount) 59 | 60 | // Deposit 61 | const manager = (await hre.ethers.getContractAt( 62 | 'GraphTokenLockManager', 63 | managerDeploy.address, 64 | )) as GraphTokenLockManager 65 | await manager.deposit(fundAmount) 66 | 67 | logger.success('Done!') 68 | } 69 | } 70 | 71 | func.tags = ['manager', 'l1', 'l1-manager', 'l1-wallet'] 72 | 73 | export default func 74 | -------------------------------------------------------------------------------- /deploy/3_l2_wallet.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import '@nomiclabs/hardhat-ethers' 3 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 4 | import { DeployFunction } from 'hardhat-deploy/types' 5 | 6 | import { getDeploymentName } from './lib/utils' 7 | 8 | 9 | const logger = consola.create({}) 10 | 11 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 12 | const { deploy } = hre.deployments 13 | const { deployer } = await hre.getNamedAccounts() 14 | 15 | // Deploy the master copy of GraphTokenLockWallet 16 | logger.info('Deploying L2GraphTokenLockWallet master copy...') 17 | const masterCopySaveName = await getDeploymentName('L2GraphTokenLockWallet') 18 | await deploy(masterCopySaveName, { 19 | from: deployer, 20 | log: true, 21 | contract: 'L2GraphTokenLockWallet', 22 | }) 23 | } 24 | 25 | func.tags = ['l2-wallet', 'l2'] 26 | 27 | export default func 28 | -------------------------------------------------------------------------------- /deploy/4_l1_transfer_tool.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { utils } from 'ethers' 3 | 4 | import '@nomiclabs/hardhat-ethers' 5 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 6 | import { DeployFunction } from 'hardhat-deploy/types' 7 | 8 | import { getDeploymentName, promptContractAddress } from './lib/utils' 9 | import { ethers, upgrades } from 'hardhat' 10 | import { L1GraphTokenLockTransferTool } from '../build/typechain/contracts/L1GraphTokenLockTransferTool' 11 | import path from 'path' 12 | import { Artifacts } from 'hardhat/internal/artifacts' 13 | 14 | const logger = consola.create({}) 15 | 16 | const ARTIFACTS_PATH = path.resolve('build/artifacts') 17 | const artifacts = new Artifacts(ARTIFACTS_PATH) 18 | const l1TransferToolAbi = artifacts.readArtifactSync('L1GraphTokenLockTransferTool').abi 19 | 20 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 21 | const { deployer } = await hre.getNamedAccounts() 22 | 23 | // Get the addresses we will use 24 | const tokenAddress = await promptContractAddress('L1 GRT', logger) 25 | if (!tokenAddress) { 26 | logger.warn('No token address provided') 27 | process.exit(1) 28 | } 29 | 30 | const l2Implementation = await promptContractAddress('L2GraphTokenLockWallet implementation', logger) 31 | if (!l2Implementation) { 32 | logger.warn('No L2 implementation address provided') 33 | process.exit(1) 34 | } 35 | 36 | const l1Gateway = await promptContractAddress('L1 token gateway', logger) 37 | if (!l1Gateway) { 38 | logger.warn('No L1 gateway address provided') 39 | process.exit(1) 40 | } 41 | 42 | const l1Staking = await promptContractAddress('L1 Staking', logger) 43 | if (!l1Staking) { 44 | logger.warn('No L1 Staking address provided') 45 | process.exit(1) 46 | } 47 | 48 | let owner = await promptContractAddress('owner (optional)', logger) 49 | if (!owner) { 50 | owner = deployer 51 | logger.warn(`No owner address provided, will use the deployer address as owner: ${owner}`) 52 | } 53 | 54 | // Deploy the L1GraphTokenLockTransferTool with a proxy. 55 | // hardhat-deploy doesn't get along with constructor arguments in the implementation 56 | // combined with an OpenZeppelin transparent proxy, so we need to do this using 57 | // the OpenZeppelin hardhat-upgrades tooling, and save the deployment manually. 58 | 59 | // TODO modify this to use upgradeProxy if a deployment already exists? 60 | logger.info('Deploying L1GraphTokenLockTransferTool proxy...') 61 | const transferToolFactory = await ethers.getContractFactory('L1GraphTokenLockTransferTool') 62 | const transferTool = (await upgrades.deployProxy(transferToolFactory, [owner], { 63 | kind: 'transparent', 64 | unsafeAllow: ['state-variable-immutable', 'constructor'], 65 | constructorArgs: [tokenAddress, l2Implementation, l1Gateway, l1Staking], 66 | })) as L1GraphTokenLockTransferTool 67 | 68 | // Save the deployment 69 | const deploymentName = await getDeploymentName('L1GraphTokenLockTransferTool') 70 | await hre.deployments.save(deploymentName, { 71 | abi: l1TransferToolAbi, 72 | address: transferTool.address, 73 | transactionHash: transferTool.deployTransaction.hash, 74 | }) 75 | } 76 | 77 | func.tags = ['l1', 'l1-transfer-tool'] 78 | 79 | export default func 80 | -------------------------------------------------------------------------------- /deploy/5_l2_manager.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { utils } from 'ethers' 3 | 4 | import '@nomiclabs/hardhat-ethers' 5 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 6 | import { DeployFunction } from 'hardhat-deploy/types' 7 | 8 | import { GraphTokenMock } from '../build/typechain/contracts/GraphTokenMock' 9 | import { askConfirm, getDeploymentName, promptContractAddress } from './lib/utils' 10 | import { L2GraphTokenLockManager } from '../build/typechain/contracts/L2GraphTokenLockManager' 11 | 12 | const { parseEther, formatEther } = utils 13 | 14 | const logger = consola.create({}) 15 | 16 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 17 | const { deploy } = hre.deployments 18 | const { deployer } = await hre.getNamedAccounts() 19 | 20 | // -- Graph Token -- 21 | 22 | // Get the token address we will use 23 | const tokenAddress = await promptContractAddress('L2 GRT', logger) 24 | if (!tokenAddress) { 25 | logger.warn('No token address provided') 26 | process.exit(1) 27 | } 28 | 29 | const l2Gateway = await promptContractAddress('L2 Gateway', logger) 30 | if (!l2Gateway) { 31 | logger.warn('No L2 Gateway address provided') 32 | process.exit(1) 33 | } 34 | 35 | const l1TransferTool = await promptContractAddress('L1 Transfer Tool', logger) 36 | if (!l1TransferTool) { 37 | logger.warn('No L1 Transfer Tool address provided') 38 | process.exit(1) 39 | } 40 | 41 | // -- L2 Token Lock Manager -- 42 | // Get the deployed L2GraphTokenLockWallet master copy address 43 | const masterCopyDeploy = await hre.deployments.get('L2GraphTokenLockWallet') 44 | 45 | logger.info(`Using L2GraphTokenLockWallet at address: ${masterCopyDeploy.address}`) 46 | // Deploy the Manager that uses the master copy to clone contracts 47 | logger.info('Deploying L2GraphTokenLockManager...') 48 | const managerSaveName = await getDeploymentName('L2GraphTokenLockManager') 49 | const managerDeploy = await deploy(managerSaveName, { 50 | from: deployer, 51 | args: [tokenAddress, masterCopyDeploy.address, l2Gateway, l1TransferTool], 52 | log: true, 53 | contract: 'L2GraphTokenLockManager', 54 | }) 55 | 56 | // -- Fund -- 57 | 58 | if (await askConfirm('Do you want to fund the L2 manager?')) { 59 | const fundAmount = parseEther('100000000') 60 | logger.info(`Funding ${managerDeploy.address} with ${formatEther(fundAmount)} GRT...`) 61 | 62 | // Approve 63 | const grt = (await hre.ethers.getContractAt('GraphTokenMock', tokenAddress)) as GraphTokenMock 64 | await grt.approve(managerDeploy.address, fundAmount) 65 | 66 | // Deposit 67 | const manager = (await hre.ethers.getContractAt( 68 | 'L2GraphTokenLockManager', 69 | managerDeploy.address, 70 | )) as L2GraphTokenLockManager 71 | await manager.deposit(fundAmount) 72 | 73 | logger.success('Done!') 74 | } 75 | } 76 | 77 | func.tags = ['l2-manager', 'l2'] 78 | 79 | export default func 80 | -------------------------------------------------------------------------------- /deploy/6_l2_transfer_tool.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { utils } from 'ethers' 3 | 4 | import '@nomiclabs/hardhat-ethers' 5 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 6 | import { DeployFunction } from 'hardhat-deploy/types' 7 | 8 | import { getDeploymentName, promptContractAddress } from './lib/utils' 9 | import { ethers, upgrades } from 'hardhat' 10 | import { L1GraphTokenLockTransferTool } from '../build/typechain/contracts/L1GraphTokenLockTransferTool' 11 | import path from 'path' 12 | import { Artifacts } from 'hardhat/internal/artifacts' 13 | 14 | const logger = consola.create({}) 15 | 16 | const ARTIFACTS_PATH = path.resolve('build/artifacts') 17 | const artifacts = new Artifacts(ARTIFACTS_PATH) 18 | const l2TransferToolAbi = artifacts.readArtifactSync('L2GraphTokenLockTransferTool').abi 19 | 20 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 21 | const { deployer } = await hre.getNamedAccounts() 22 | 23 | // -- Graph Token -- 24 | 25 | // Get the addresses we will use 26 | const tokenAddress = await promptContractAddress('L2 GRT', logger) 27 | if (!tokenAddress) { 28 | logger.warn('No token address provided') 29 | process.exit(1) 30 | } 31 | 32 | const l2Gateway = await promptContractAddress('L2 token gateway', logger) 33 | if (!l2Gateway) { 34 | logger.warn('No L2 gateway address provided') 35 | process.exit(1) 36 | } 37 | 38 | const l1Token = await promptContractAddress('L1 GRT', logger) 39 | if (!l1Token) { 40 | logger.warn('No L1 GRT address provided') 41 | process.exit(1) 42 | } 43 | 44 | // Deploy the L2GraphTokenLockTransferTool with a proxy. 45 | // hardhat-deploy doesn't get along with constructor arguments in the implementation 46 | // combined with an OpenZeppelin transparent proxy, so we need to do this using 47 | // the OpenZeppelin hardhat-upgrades tooling, and save the deployment manually. 48 | 49 | // TODO modify this to use upgradeProxy if a deployment already exists? 50 | logger.info('Deploying L2GraphTokenLockTransferTool proxy...') 51 | const transferToolFactory = await ethers.getContractFactory('L2GraphTokenLockTransferTool') 52 | const transferTool = (await upgrades.deployProxy(transferToolFactory, [], { 53 | kind: 'transparent', 54 | unsafeAllow: ['state-variable-immutable', 'constructor'], 55 | constructorArgs: [tokenAddress, l2Gateway, l1Token], 56 | })) as L1GraphTokenLockTransferTool 57 | 58 | // Save the deployment 59 | const deploymentName = await getDeploymentName('L2GraphTokenLockTransferTool') 60 | await hre.deployments.save(deploymentName, { 61 | abi: l2TransferToolAbi, 62 | address: transferTool.address, 63 | transactionHash: transferTool.deployTransaction.hash, 64 | }) 65 | } 66 | 67 | func.tags = ['l2', 'l2-transfer-tool'] 68 | 69 | export default func 70 | -------------------------------------------------------------------------------- /deploy/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { Consola } from 'consola' 2 | import inquirer from 'inquirer' 3 | import { utils } from 'ethers' 4 | 5 | import '@nomiclabs/hardhat-ethers' 6 | 7 | const { getAddress } = utils 8 | 9 | export const askConfirm = async (message: string) => { 10 | const res = await inquirer.prompt({ 11 | name: 'confirm', 12 | type: 'confirm', 13 | message, 14 | }) 15 | return res.confirm 16 | } 17 | 18 | export const promptContractAddress = async (name: string, logger: Consola): Promise => { 19 | const res1 = await inquirer.prompt({ 20 | name: 'contract', 21 | type: 'input', 22 | message: `What is the ${name} address?`, 23 | }) 24 | 25 | try { 26 | return getAddress(res1.contract) 27 | } catch (err) { 28 | logger.error(err) 29 | return null 30 | } 31 | } 32 | 33 | export const getDeploymentName = async (defaultName: string): Promise => { 34 | const res = await inquirer.prompt({ 35 | name: 'deployment-name', 36 | type: 'input', 37 | default: defaultName, 38 | message: 'Save deployment as?', 39 | }) 40 | return res['deployment-name'] 41 | } 42 | -------------------------------------------------------------------------------- /deployments/arbitrum-goerli/.chainId: -------------------------------------------------------------------------------- 1 | 421613 -------------------------------------------------------------------------------- /deployments/arbitrum-goerli/L2GraphTokenLockTransferTool.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0xc1A9C2E76171e64Cd5669B3E89D9A25a6b0FAfB7", 3 | "abi": [ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "contract IERC20", 8 | "name": "_graphToken", 9 | "type": "address" 10 | }, 11 | { 12 | "internalType": "contract ITokenGateway", 13 | "name": "_l2Gateway", 14 | "type": "address" 15 | }, 16 | { 17 | "internalType": "address", 18 | "name": "_l1GraphToken", 19 | "type": "address" 20 | } 21 | ], 22 | "stateMutability": "nonpayable", 23 | "type": "constructor" 24 | }, 25 | { 26 | "anonymous": false, 27 | "inputs": [ 28 | { 29 | "indexed": true, 30 | "internalType": "address", 31 | "name": "l1Wallet", 32 | "type": "address" 33 | }, 34 | { 35 | "indexed": true, 36 | "internalType": "address", 37 | "name": "l2Wallet", 38 | "type": "address" 39 | }, 40 | { 41 | "indexed": true, 42 | "internalType": "address", 43 | "name": "l2LockManager", 44 | "type": "address" 45 | }, 46 | { 47 | "indexed": false, 48 | "internalType": "uint256", 49 | "name": "amount", 50 | "type": "uint256" 51 | } 52 | ], 53 | "name": "LockedFundsSentToL1", 54 | "type": "event" 55 | }, 56 | { 57 | "inputs": [], 58 | "name": "graphToken", 59 | "outputs": [ 60 | { 61 | "internalType": "contract IERC20", 62 | "name": "", 63 | "type": "address" 64 | } 65 | ], 66 | "stateMutability": "view", 67 | "type": "function" 68 | }, 69 | { 70 | "inputs": [], 71 | "name": "l1GraphToken", 72 | "outputs": [ 73 | { 74 | "internalType": "address", 75 | "name": "", 76 | "type": "address" 77 | } 78 | ], 79 | "stateMutability": "view", 80 | "type": "function" 81 | }, 82 | { 83 | "inputs": [], 84 | "name": "l2Gateway", 85 | "outputs": [ 86 | { 87 | "internalType": "contract ITokenGateway", 88 | "name": "", 89 | "type": "address" 90 | } 91 | ], 92 | "stateMutability": "view", 93 | "type": "function" 94 | }, 95 | { 96 | "inputs": [ 97 | { 98 | "internalType": "uint256", 99 | "name": "_amount", 100 | "type": "uint256" 101 | } 102 | ], 103 | "name": "withdrawToL1Locked", 104 | "outputs": [], 105 | "stateMutability": "nonpayable", 106 | "type": "function" 107 | } 108 | ], 109 | "transactionHash": "0x4c0fdb3290d0e247de1d0863bc2a7b13ea9414a86e5bfe94f1e2eba7c5c47f70" 110 | } -------------------------------------------------------------------------------- /deployments/arbitrum-one/.chainId: -------------------------------------------------------------------------------- 1 | 42161 -------------------------------------------------------------------------------- /deployments/arbitrum-one/L2GraphTokenLockTransferTool.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x23C9c8575E6bA0349a497b6D0E8F0b9239e68028", 3 | "abi": [ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "contract IERC20", 8 | "name": "_graphToken", 9 | "type": "address" 10 | }, 11 | { 12 | "internalType": "contract ITokenGateway", 13 | "name": "_l2Gateway", 14 | "type": "address" 15 | }, 16 | { 17 | "internalType": "address", 18 | "name": "_l1GraphToken", 19 | "type": "address" 20 | } 21 | ], 22 | "stateMutability": "nonpayable", 23 | "type": "constructor" 24 | }, 25 | { 26 | "anonymous": false, 27 | "inputs": [ 28 | { 29 | "indexed": true, 30 | "internalType": "address", 31 | "name": "l1Wallet", 32 | "type": "address" 33 | }, 34 | { 35 | "indexed": true, 36 | "internalType": "address", 37 | "name": "l2Wallet", 38 | "type": "address" 39 | }, 40 | { 41 | "indexed": true, 42 | "internalType": "address", 43 | "name": "l2LockManager", 44 | "type": "address" 45 | }, 46 | { 47 | "indexed": false, 48 | "internalType": "uint256", 49 | "name": "amount", 50 | "type": "uint256" 51 | } 52 | ], 53 | "name": "LockedFundsSentToL1", 54 | "type": "event" 55 | }, 56 | { 57 | "inputs": [], 58 | "name": "graphToken", 59 | "outputs": [ 60 | { 61 | "internalType": "contract IERC20", 62 | "name": "", 63 | "type": "address" 64 | } 65 | ], 66 | "stateMutability": "view", 67 | "type": "function" 68 | }, 69 | { 70 | "inputs": [], 71 | "name": "l1GraphToken", 72 | "outputs": [ 73 | { 74 | "internalType": "address", 75 | "name": "", 76 | "type": "address" 77 | } 78 | ], 79 | "stateMutability": "view", 80 | "type": "function" 81 | }, 82 | { 83 | "inputs": [], 84 | "name": "l2Gateway", 85 | "outputs": [ 86 | { 87 | "internalType": "contract ITokenGateway", 88 | "name": "", 89 | "type": "address" 90 | } 91 | ], 92 | "stateMutability": "view", 93 | "type": "function" 94 | }, 95 | { 96 | "inputs": [ 97 | { 98 | "internalType": "uint256", 99 | "name": "_amount", 100 | "type": "uint256" 101 | } 102 | ], 103 | "name": "withdrawToL1Locked", 104 | "outputs": [], 105 | "stateMutability": "nonpayable", 106 | "type": "function" 107 | } 108 | ], 109 | "transactionHash": "0xecb5b61a0d6fbca8f01174fea87d34172d4321650ba0566b0a9c87c7eca8df73" 110 | } -------------------------------------------------------------------------------- /deployments/arbitrum-sepolia/.chainId: -------------------------------------------------------------------------------- 1 | 421614 -------------------------------------------------------------------------------- /deployments/arbitrum-sepolia/L2GraphTokenLockTransferTool.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0xe21cd62E1E0CD68476C47F518980226C0a05fB19", 3 | "abi": [ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "contract IERC20", 8 | "name": "_graphToken", 9 | "type": "address" 10 | }, 11 | { 12 | "internalType": "contract ITokenGateway", 13 | "name": "_l2Gateway", 14 | "type": "address" 15 | }, 16 | { 17 | "internalType": "address", 18 | "name": "_l1GraphToken", 19 | "type": "address" 20 | } 21 | ], 22 | "stateMutability": "nonpayable", 23 | "type": "constructor" 24 | }, 25 | { 26 | "anonymous": false, 27 | "inputs": [ 28 | { 29 | "indexed": true, 30 | "internalType": "address", 31 | "name": "l1Wallet", 32 | "type": "address" 33 | }, 34 | { 35 | "indexed": true, 36 | "internalType": "address", 37 | "name": "l2Wallet", 38 | "type": "address" 39 | }, 40 | { 41 | "indexed": true, 42 | "internalType": "address", 43 | "name": "l2LockManager", 44 | "type": "address" 45 | }, 46 | { 47 | "indexed": false, 48 | "internalType": "uint256", 49 | "name": "amount", 50 | "type": "uint256" 51 | } 52 | ], 53 | "name": "LockedFundsSentToL1", 54 | "type": "event" 55 | }, 56 | { 57 | "inputs": [], 58 | "name": "graphToken", 59 | "outputs": [ 60 | { 61 | "internalType": "contract IERC20", 62 | "name": "", 63 | "type": "address" 64 | } 65 | ], 66 | "stateMutability": "view", 67 | "type": "function" 68 | }, 69 | { 70 | "inputs": [], 71 | "name": "l1GraphToken", 72 | "outputs": [ 73 | { 74 | "internalType": "address", 75 | "name": "", 76 | "type": "address" 77 | } 78 | ], 79 | "stateMutability": "view", 80 | "type": "function" 81 | }, 82 | { 83 | "inputs": [], 84 | "name": "l2Gateway", 85 | "outputs": [ 86 | { 87 | "internalType": "contract ITokenGateway", 88 | "name": "", 89 | "type": "address" 90 | } 91 | ], 92 | "stateMutability": "view", 93 | "type": "function" 94 | }, 95 | { 96 | "inputs": [ 97 | { 98 | "internalType": "uint256", 99 | "name": "_amount", 100 | "type": "uint256" 101 | } 102 | ], 103 | "name": "withdrawToL1Locked", 104 | "outputs": [], 105 | "stateMutability": "nonpayable", 106 | "type": "function" 107 | } 108 | ], 109 | "transactionHash": "0x4785cb6bfeae00d727ed1199ad2724772507d6631135c73797069382a58af7d3" 110 | } -------------------------------------------------------------------------------- /deployments/goerli/.chainId: -------------------------------------------------------------------------------- 1 | 5 -------------------------------------------------------------------------------- /deployments/mainnet/.chainId: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /deployments/rinkeby/.chainId: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /deployments/sepolia/.chainId: -------------------------------------------------------------------------------- 1 | 11155111 -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import { extendEnvironment, task } from 'hardhat/config' 3 | 4 | dotenv.config() 5 | 6 | // Plugins 7 | 8 | import '@nomiclabs/hardhat-ethers' 9 | import '@nomiclabs/hardhat-etherscan' 10 | import '@nomiclabs/hardhat-waffle' 11 | import 'hardhat-deploy' 12 | import 'hardhat-abi-exporter' 13 | import '@typechain/hardhat' 14 | import 'hardhat-gas-reporter' 15 | import '@openzeppelin/hardhat-upgrades' 16 | 17 | // Tasks 18 | 19 | import './ops/create' 20 | import './ops/delete' 21 | import './ops/info' 22 | import './ops/manager' 23 | import './ops/beneficiary' 24 | 25 | // Networks 26 | 27 | interface NetworkConfig { 28 | network: string 29 | chainId: number 30 | url?: string 31 | gas?: number | 'auto' 32 | gasPrice?: number | 'auto' 33 | } 34 | 35 | const networkConfigs: NetworkConfig[] = [ 36 | { network: 'mainnet', chainId: 1 }, 37 | { network: 'ropsten', chainId: 3 }, 38 | { network: 'rinkeby', chainId: 4 }, 39 | { network: 'goerli', chainId: 5 }, 40 | { network: 'kovan', chainId: 42 }, 41 | { network: 'sepolia', chainId: 11155111 }, 42 | { 43 | network: 'arbitrum-one', 44 | chainId: 42161, 45 | url: 'https://arb1.arbitrum.io/rpc', 46 | }, 47 | { 48 | network: 'arbitrum-goerli', 49 | chainId: 421613, 50 | url: 'https://goerli-rollup.arbitrum.io/rpc', 51 | }, 52 | { 53 | network: 'arbitrum-sepolia', 54 | chainId: 421614, 55 | url: 'https://sepolia-rollup.arbitrum.io/rpcblock', 56 | }, 57 | ] 58 | 59 | function getAccountMnemonic() { 60 | return process.env.MNEMONIC || '' 61 | } 62 | 63 | function getDefaultProviderURL(network: string) { 64 | return `https://${network}.infura.io/v3/${process.env.INFURA_KEY}` 65 | } 66 | 67 | function setupNetworkConfig(config) { 68 | for (const netConfig of networkConfigs) { 69 | config.networks[netConfig.network] = { 70 | chainId: netConfig.chainId, 71 | url: netConfig.url ? netConfig.url : getDefaultProviderURL(netConfig.network), 72 | gas: netConfig.gas || 'auto', 73 | gasPrice: netConfig.gasPrice || 'auto', 74 | accounts: { 75 | mnemonic: getAccountMnemonic(), 76 | }, 77 | } 78 | } 79 | } 80 | 81 | // Env 82 | 83 | extendEnvironment(async (hre) => { 84 | const accounts = await hre.ethers.getSigners() 85 | try { 86 | const deployment = await hre.deployments.get('GraphTokenLockManager') 87 | const contract = await hre.ethers.getContractAt('GraphTokenLockManager', deployment.address) 88 | await contract.deployed() // test if deployed 89 | 90 | hre['c'] = { 91 | GraphTokenLockManager: contract.connect(accounts[0]), 92 | } 93 | } catch (err) { 94 | // do not load the contract 95 | } 96 | }) 97 | 98 | // Tasks 99 | 100 | task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => { 101 | const accounts = await hre.ethers.getSigners() 102 | for (const account of accounts) { 103 | console.log(await account.getAddress()) 104 | } 105 | }) 106 | 107 | // Config 108 | 109 | const config = { 110 | paths: { 111 | sources: './contracts', 112 | tests: './test', 113 | cache: './cache', 114 | artifacts: './build/artifacts', 115 | }, 116 | settings: { 117 | optimizer: { 118 | enabled: true, 119 | runs: 200, 120 | }, 121 | }, 122 | solidity: { 123 | compilers: [ 124 | { 125 | version: '0.7.3', 126 | settings: { 127 | optimizer: { 128 | enabled: false, 129 | runs: 200, 130 | }, 131 | }, 132 | }, 133 | ], 134 | }, 135 | defaultNetwork: 'hardhat', 136 | networks: { 137 | hardhat: { 138 | chainId: 1337, 139 | loggingEnabled: false, 140 | gas: 12000000, 141 | gasPrice: 'auto', 142 | blockGasLimit: 12000000, 143 | }, 144 | ganache: { 145 | chainId: 1337, 146 | url: 'http://localhost:8545', 147 | }, 148 | }, 149 | namedAccounts: { 150 | deployer: { 151 | default: 0, 152 | }, 153 | }, 154 | etherscan: { 155 | //url: process.env.ETHERSCAN_API_URL, 156 | apiKey: process.env.ETHERSCAN_API_KEY, 157 | customChains: [ 158 | { 159 | network: 'arbitrum-sepolia', 160 | chainId: 421614, 161 | urls: { 162 | apiURL: 'https://api-sepolia.arbiscan.io/api', 163 | browserURL: 'https://sepolia.arbiscan.io', 164 | }, 165 | }, 166 | ] 167 | }, 168 | typechain: { 169 | outDir: 'build/typechain/contracts', 170 | target: 'ethers-v5', 171 | }, 172 | abiExporter: { 173 | path: './build/abis', 174 | clear: false, 175 | flat: true, 176 | runOnCompile: true, 177 | }, 178 | contractSizer: { 179 | alphaSort: true, 180 | runOnCompile: false, 181 | }, 182 | gasReporter: { 183 | enabled: process.env.REPORT_GAS ? true : false, 184 | showTimeSpent: true, 185 | currency: 'USD', 186 | outputFile: 'reports/gas-report.log', 187 | }, 188 | } 189 | 190 | setupNetworkConfig(config) 191 | 192 | export default config 193 | -------------------------------------------------------------------------------- /ops/beneficiary.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config' 2 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 3 | import { askConfirm, waitTransaction } from './create' 4 | import consola from 'consola' 5 | 6 | const logger = consola.create({}) 7 | 8 | task('beneficiary-accept-lock', 'Accept token lock. Only callable by beneficiary') 9 | .addParam('contract', 'Address of the vesting contract') 10 | .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { 11 | const { deployer } = await hre.getNamedAccounts() 12 | 13 | const vestingContract = await hre.ethers.getContractAt('GraphTokenLockWallet', taskArgs.contract) 14 | const beneficiary = await vestingContract.beneficiary() 15 | let isAccepted = await vestingContract.isAccepted() 16 | 17 | logger.info(`Vesting contract address: ${vestingContract.address}}`) 18 | logger.info(`Beneficiary: ${beneficiary}`) 19 | logger.info(`Connected account: ${deployer}`) 20 | logger.info(`Lock accepted: ${isAccepted}`) 21 | 22 | // Check lock status 23 | if (isAccepted) { 24 | logger.warn('Lock already accepted, exiting...') 25 | process.exit(0) 26 | } 27 | 28 | // Check beneficiary 29 | if (beneficiary !== deployer) { 30 | logger.error('Only the beneficiary can accept the vesting contract lock!') 31 | process.exit(1) 32 | } 33 | 34 | // Confirm 35 | logger.info('Preparing transaction to accept token lock...') 36 | if (!(await askConfirm())) { 37 | logger.log('Cancelled') 38 | process.exit(1) 39 | } 40 | 41 | // Accept lock 42 | const tx = await vestingContract.acceptLock() 43 | await waitTransaction(tx) 44 | 45 | // Verify lock state 46 | isAccepted = await vestingContract.isAccepted() 47 | if (isAccepted) { 48 | logger.info(`Lock accepted successfully!`) 49 | } else { 50 | logger.error(`Lock not accepted! Unknown error, please try again`) 51 | } 52 | }) 53 | 54 | task('beneficiary-vesting-info', 'Print vesting contract info') 55 | .addParam('contract', 'Address of the vesting contract') 56 | .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { 57 | const vestingContract = await hre.ethers.getContractAt('GraphTokenLockWallet', taskArgs.contract) 58 | const beneficiary = await vestingContract.beneficiary() 59 | const isAccepted = await vestingContract.isAccepted() 60 | const startTime = await vestingContract.startTime() 61 | const endTime = await vestingContract.endTime() 62 | const periods = await vestingContract.periods() 63 | const releaseStartTime = await vestingContract.releaseStartTime() 64 | const vestingCliffTime = await vestingContract.vestingCliffTime() 65 | const managedAmount = await vestingContract.managedAmount() 66 | const revocable = await vestingContract.revocable() 67 | 68 | logger.info(`Vesting contract address: ${vestingContract.address}}`) 69 | logger.info(`Beneficiary: ${beneficiary}`) 70 | logger.info(`Managed amount: ${managedAmount}`) 71 | logger.info(`Lock accepted: ${isAccepted}`) 72 | logger.info(`Revocable: ${revocable}`) 73 | logger.info(`Start time: ${startTime}`) 74 | logger.info(`End time: ${endTime}`) 75 | logger.info(`Periods: ${periods}`) 76 | logger.info(`Release start time: ${releaseStartTime}`) 77 | logger.info(`Vesting cliff time: ${vestingCliffTime}`) 78 | }) 79 | -------------------------------------------------------------------------------- /ops/delete.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config' 2 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 3 | import { prettyEnv, askConfirm, waitTransaction } from './create' 4 | import consola from 'consola' 5 | import { TxBuilder } from './tx-builder' 6 | 7 | const logger = consola.create({}) 8 | 9 | const getTokenLockWalletOrFail = async (hre: HardhatRuntimeEnvironment, address: string) => { 10 | const wallet = await hre.ethers.getContractAt('GraphTokenLockWallet', address) 11 | try { 12 | await wallet.deployed() 13 | } catch (err) { 14 | logger.error('GraphTokenLockWallet not deployed at', wallet.address) 15 | process.exit(1) 16 | } 17 | 18 | return wallet 19 | } 20 | 21 | task('cancel-token-lock', 'Cancel token lock contract') 22 | .addParam('contract', 'Address of the vesting contract to be cancelled') 23 | .addFlag('dryRun', 'Get the deterministic contract addresses but do not deploy') 24 | .addFlag( 25 | 'txBuilder', 26 | 'Output transaction batch in JSON format, compatible with Gnosis Safe transaction builder. Does not deploy contracts', 27 | ) 28 | .addOptionalParam('txBuilderTemplate', 'File to use as a template for the transaction builder') 29 | .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { 30 | // Get contracts 31 | const lockWallet = await getTokenLockWalletOrFail(hre, taskArgs.contract) 32 | 33 | // Prepare 34 | logger.log(await prettyEnv(hre)) 35 | 36 | logger.info('Cancelling token lock contract...') 37 | logger.log(`> GraphTokenLockWallet: ${lockWallet.address}`) 38 | 39 | // Check lock status 40 | logger.log('Veryfing lock status...') 41 | const lockAccepted = await lockWallet.isAccepted() 42 | if (lockAccepted) { 43 | logger.error('Lock was already accepted, use revoke() to revoke the vesting schedule') 44 | process.exit(1) 45 | } else { 46 | logger.success(`Lock not accepted yet, preparing to cancel!`) 47 | } 48 | 49 | // Nothing else to do, exit if dry run 50 | if (taskArgs.dryRun) { 51 | logger.info('Running in dry run mode!') 52 | process.exit(0) 53 | } 54 | 55 | if (!(await askConfirm())) { 56 | logger.log('Cancelled') 57 | process.exit(1) 58 | } 59 | 60 | if (!taskArgs.txBuilder) { 61 | const { deployer } = await hre.getNamedAccounts() 62 | const lockOwner = await lockWallet.owner() 63 | if (lockOwner !== deployer) { 64 | logger.error('Only the owner can cancell the token lock') 65 | process.exit(1) 66 | } 67 | 68 | logger.info(`Cancelling contract...`) 69 | const tx = await lockWallet.cancelLock() 70 | await waitTransaction(tx) 71 | logger.success(`Token lock at ${lockWallet.address} was cancelled`) 72 | } else { 73 | logger.info(`Creating transaction builder JSON file...`) 74 | const chainId = (await hre.ethers.provider.getNetwork()).chainId.toString() 75 | const txBuilder = new TxBuilder(chainId, taskArgs.txBuilderTemplate) 76 | 77 | const tx = await lockWallet.populateTransaction.cancelLock() 78 | txBuilder.addTx({ 79 | to: lockWallet.address, 80 | data: tx.data, 81 | value: 0, 82 | }) 83 | 84 | // Save result into json file 85 | const outputFile = txBuilder.saveToFile() 86 | logger.success(`Transaction saved to ${outputFile}`) 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /ops/deploy-data.csv: -------------------------------------------------------------------------------- 1 | beneficiary,managedAmount,startTime,endTime,periods,revocable,releaseStartTime,vestingCliffTime 2 | -------------------------------------------------------------------------------- /ops/manager.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config' 2 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 3 | import { askConfirm, getTokenLockManagerOrFail, isValidAddressOrFail, prettyEnv, waitTransaction } from './create' 4 | import consola from 'consola' 5 | import { formatEther, parseEther } from 'ethers/lib/utils' 6 | 7 | const logger = consola.create({}) 8 | 9 | task('manager-setup-auth', 'Setup default authorized functions in the manager') 10 | .addParam('targetAddress', 'Target address for function calls') 11 | .addParam('managerName', 'Name of the token lock manager deployment', 'GraphTokenLockManager') 12 | .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { 13 | // Get contracts 14 | const manager = await getTokenLockManagerOrFail(hre, taskArgs.managerName) 15 | 16 | logger.info('Setting up authorized functions...') 17 | logger.log(`> GraphTokenLockManager: ${manager.address}`) 18 | logger.log(`> Staking: ${taskArgs.targetAddress}`) 19 | 20 | // Prepare 21 | logger.log(await prettyEnv(hre)) 22 | 23 | // Validations 24 | isValidAddressOrFail(taskArgs.targetAddress) 25 | 26 | // Setup authorized functions 27 | const signatures = [ 28 | 'stake(uint256)', 29 | 'unstake(uint256)', 30 | 'withdraw()', 31 | 'delegate(address,uint256)', 32 | 'undelegate(address,uint256)', 33 | 'withdrawDelegated(address,address)', 34 | 'setDelegationParameters(uint32,uint32,uint32)', 35 | 'setOperator(address,bool)', 36 | ] 37 | 38 | logger.info('The following signatures will be authorized:') 39 | logger.info(signatures) 40 | 41 | if (await askConfirm()) { 42 | // Setup authorized functions 43 | logger.info('Setup authorized functions...') 44 | const targets = Array(signatures.length).fill(taskArgs.targetAddress) 45 | const tx1 = await manager.setAuthFunctionCallMany(signatures, targets) 46 | await waitTransaction(tx1) 47 | logger.success('Done!\n') 48 | 49 | // Setup authorized token destinations 50 | logger.info('Setup authorized destinations...') 51 | const tx2 = await manager.addTokenDestination(taskArgs.targetAddress) 52 | await waitTransaction(tx2) 53 | } 54 | }) 55 | 56 | task('manager-deposit', 'Deposit fund into the manager') 57 | .addParam('amount', 'Amount to deposit in GRT') 58 | .addParam('managerName', 'Name of the token lock manager deployment', 'GraphTokenLockManager') 59 | .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { 60 | // Get contracts 61 | const manager = await getTokenLockManagerOrFail(hre, taskArgs.managerName) 62 | 63 | // Prepare 64 | logger.log(await prettyEnv(hre)) 65 | 66 | const tokenAddress = await manager.token() 67 | 68 | logger.info('Using:') 69 | logger.log(`> GraphToken: ${tokenAddress}`) 70 | logger.log(`> GraphTokenLockMasterCopy: ${await manager.masterCopy()}`) 71 | logger.log(`> GraphTokenLockManager: ${manager.address}`) 72 | 73 | // Deposit funds 74 | logger.log(`You are depositing ${taskArgs.amount} into ${manager.address}...`) 75 | if (await askConfirm()) { 76 | const weiAmount = parseEther(taskArgs.amount) 77 | 78 | logger.log('Approve...') 79 | const grt = await hre.ethers.getContractAt('ERC20', tokenAddress) 80 | const tx1 = await grt.approve(manager.address, weiAmount) 81 | await waitTransaction(tx1) 82 | 83 | logger.log('Deposit...') 84 | const tx2 = await manager.deposit(weiAmount) 85 | await waitTransaction(tx2) 86 | } 87 | }) 88 | 89 | task('manager-withdraw', 'Withdraw fund from the manager') 90 | .addParam('amount', 'Amount to deposit in GRT') 91 | .addParam('managerName', 'Name of the token lock manager deployment', 'GraphTokenLockManager') 92 | .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { 93 | // Get contracts 94 | const manager = await getTokenLockManagerOrFail(hre, taskArgs.managerName) 95 | 96 | // Prepare 97 | logger.log(await prettyEnv(hre)) 98 | 99 | const tokenAddress = await manager.token() 100 | 101 | logger.info('Using:') 102 | logger.log(`> GraphToken: ${tokenAddress}`) 103 | logger.log(`> GraphTokenLockMasterCopy: ${await manager.masterCopy()}`) 104 | logger.log(`> GraphTokenLockManager: ${manager.address}`) 105 | 106 | // Withdraw funds 107 | logger.log(`You are withdrawing ${taskArgs.amount} from ${manager.address}...`) 108 | if (await askConfirm()) { 109 | const weiAmount = parseEther(taskArgs.amount) 110 | 111 | logger.log('Deposit...') 112 | const tx = await manager.withdraw(weiAmount) 113 | await waitTransaction(tx) 114 | } 115 | }) 116 | 117 | task('manager-balance', 'Get current manager balance') 118 | .addParam('managerName', 'Name of the token lock manager deployment', 'GraphTokenLockManager') 119 | .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { 120 | // Get contracts 121 | const manager = await getTokenLockManagerOrFail(hre, taskArgs.managerName) 122 | 123 | // Prepare 124 | logger.log(await prettyEnv(hre)) 125 | 126 | const tokenAddress = await manager.token() 127 | const managerOwnerAddress = await manager.owner() 128 | 129 | logger.info('Using:') 130 | logger.log(`> GraphToken: ${tokenAddress}`) 131 | logger.log(`> GraphTokenLockMasterCopy: ${await manager.masterCopy()}`) 132 | logger.log(`> GraphTokenLockManager: ${manager.address} owner: ${managerOwnerAddress}`) 133 | 134 | const grt = await hre.ethers.getContractAt('ERC20', tokenAddress) 135 | const balance = await grt.balanceOf(manager.address) 136 | logger.log('Current Manager balance is ', formatEther(balance)) 137 | }) 138 | 139 | task('manager-transfer-ownership', 'Transfer ownership of the manager') 140 | .addParam('owner', 'Address of the new owner') 141 | .addParam('managerName', 'Name of the token lock manager deployment', 'GraphTokenLockManager') 142 | .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { 143 | const manager = await getTokenLockManagerOrFail(hre, taskArgs.managerName) 144 | 145 | // Validate current owner 146 | const tokenLockManagerOwner = await manager.owner() 147 | const { deployer } = await hre.getNamedAccounts() 148 | if (tokenLockManagerOwner !== deployer) { 149 | logger.error('Only the owner can transfer ownership') 150 | process.exit(1) 151 | } 152 | 153 | logger.info(`Manager address: ${manager.address}}`) 154 | logger.info(`Current owner: ${tokenLockManagerOwner}`) 155 | logger.info(`New owner: ${taskArgs.owner}`) 156 | 157 | if (!(await askConfirm())) { 158 | logger.log('Cancelled') 159 | process.exit(1) 160 | } 161 | 162 | // Transfer ownership 163 | await manager.transferOwnership(taskArgs.owner) 164 | }) 165 | -------------------------------------------------------------------------------- /ops/queries/account.graphql: -------------------------------------------------------------------------------- 1 | query GraphAccount($accountId: ID!, $blockNumber: Int) { 2 | graphAccount(id: $accountId, block: { number: $blockNumber }) { 3 | id 4 | indexer { 5 | stakedTokens 6 | } 7 | curator { 8 | totalSignalledTokens 9 | totalUnsignalledTokens 10 | } 11 | delegator { 12 | totalStakedTokens 13 | totalUnstakedTokens 14 | totalRealizedRewards 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ops/queries/curators.graphql: -------------------------------------------------------------------------------- 1 | query CuratorWallets($blockNumber: Int, $first: Int) { 2 | tokenLockWallets( 3 | block: { number: $blockNumber } 4 | where: { periods: 16, startTime: 1608224400, endTime: 1734454800, revocable: Disabled } 5 | first: $first 6 | orderBy: blockNumberCreated 7 | ) { 8 | id 9 | beneficiary 10 | managedAmount 11 | periods 12 | startTime 13 | endTime 14 | revocable 15 | releaseStartTime 16 | vestingCliffTime 17 | initHash 18 | txHash 19 | manager 20 | tokensReleased 21 | tokensWithdrawn 22 | tokensRevoked 23 | blockNumberCreated 24 | } 25 | } -------------------------------------------------------------------------------- /ops/queries/network.graphql: -------------------------------------------------------------------------------- 1 | query GraphNetwork($blockNumber: Int) { 2 | graphNetwork(id: 1, block: { number: $blockNumber }) { 3 | id 4 | totalSupply 5 | } 6 | } -------------------------------------------------------------------------------- /ops/queries/tokenLockWallets.graphql: -------------------------------------------------------------------------------- 1 | query TokenLockWallets($blockNumber: Int, $first: Int) { 2 | tokenLockWallets(block: { number: $blockNumber }, first: $first, orderBy: id) { 3 | id 4 | beneficiary 5 | managedAmount 6 | periods 7 | startTime 8 | endTime 9 | revocable 10 | releaseStartTime 11 | vestingCliffTime 12 | initHash 13 | txHash 14 | manager 15 | tokensReleased 16 | tokensWithdrawn 17 | tokensRevoked 18 | blockNumberCreated 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ops/results.csv: -------------------------------------------------------------------------------- 1 | beneficiary,managedAmount,startTime,endTime,periods,revocable,releaseStartTime,vestingCliffTime,contractAddress,salt,tx 2 | -------------------------------------------------------------------------------- /ops/tx-builder-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "chainId": "5", 4 | "createdAt": 1664999924896, 5 | "meta": { 6 | "name": "Vesting Contracts", 7 | "description": "", 8 | "txBuilderVersion": "1.11.1", 9 | "createdFromSafeAddress": "", 10 | "createdFromOwnerAddress": "", 11 | "checksum": "0xaa4f6084a39579ddecb1224904d703183c5086d1e3d7e63ba94a8b6819dd2122" 12 | }, 13 | "transactions": [ 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ops/tx-builder.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | export class TxBuilder { 5 | contents: any 6 | outputFile: string 7 | 8 | constructor(chainId: string, _template?: string) { 9 | // Template file 10 | const template = _template ?? 'tx-builder-template.json' 11 | const templateFilename = path.join(__dirname, template) 12 | 13 | // Output file 14 | const dateTime = new Date().getTime() 15 | this.outputFile = path.join(__dirname, `tx-builder-${dateTime}.json`) 16 | 17 | // Load template 18 | this.contents = JSON.parse(fs.readFileSync(templateFilename, 'utf8')) 19 | this.contents.createdAt = dateTime 20 | this.contents.chainId = chainId 21 | } 22 | 23 | addTx(tx: any) { 24 | this.contents.transactions.push({ ...tx, contractMethod: null, contractInputsValues: null }) 25 | } 26 | 27 | saveToFile() { 28 | fs.writeFileSync(this.outputFile, JSON.stringify(this.contents, null, 2)) 29 | return this.outputFile 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphprotocol/token-distribution", 3 | "version": "1.2.0", 4 | "description": "Graph Token Distribution", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepublishOnly": "scripts/prepublish", 8 | "build": "scripts/build", 9 | "clean": "rm -rf build/ cache/ dist/ && hardhat clean", 10 | "compile": "hardhat compile --show-stack-traces", 11 | "deploy": "yarn run build && hardhat deploy", 12 | "test": "scripts/test", 13 | "test:gas": "RUN_EVM=true REPORT_GAS=true scripts/test", 14 | "test:coverage": "scripts/coverage", 15 | "lint": "yarn run lint:ts && yarn run lint:sol", 16 | "lint:fix": "yarn run lint:ts:fix && yarn run lint:sol:fix", 17 | "lint:ts": "eslint '*/**/*.{js,ts}'", 18 | "lint:ts:fix": "eslint '*/**/*.{js,ts}' --fix", 19 | "lint:sol": "solhint './contracts/**/*.sol'", 20 | "lint:sol:fix": "yarn prettier:sol && solhint --fix './contracts/**/*.sol'", 21 | "prettier": "yarn run prettier:ts && yarn run prettier:sol", 22 | "prettier:ts": "prettier --write 'test/**/*.ts'", 23 | "prettier:sol": "prettier --write 'contracts/**/*.sol'", 24 | "security": "scripts/security", 25 | "flatten": "scripts/flatten", 26 | "typechain": "hardhat typechain", 27 | "verify": "hardhat verify", 28 | "size": "hardhat size-contracts" 29 | }, 30 | "files": [ 31 | "dist/**/*", 32 | "README.md", 33 | "LICENSE" 34 | ], 35 | "author": "The Graph Team", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@ethersproject/experimental": "^5.0.7", 39 | "@graphprotocol/client-cli": "^2.0.2", 40 | "@graphprotocol/contracts": "^5.0.0", 41 | "@nomiclabs/hardhat-ethers": "^2.0.0", 42 | "@nomiclabs/hardhat-etherscan": "^3.1.7", 43 | "@nomiclabs/hardhat-waffle": "^2.0.0", 44 | "@openzeppelin/contracts": "^3.3.0-solc-0.7", 45 | "@openzeppelin/contracts-upgradeable": "3.4.2", 46 | "@openzeppelin/hardhat-upgrades": "^1.22.1", 47 | "@typechain/ethers-v5": "^7.0.0", 48 | "@typechain/hardhat": "^2.0.0", 49 | "@types/mocha": "^9.1.0", 50 | "@types/node": "^20.4.2", 51 | "@typescript-eslint/eslint-plugin": "^5.20.0", 52 | "@typescript-eslint/parser": "^5.20.0", 53 | "chai": "^4.2.0", 54 | "coingecko-api": "^1.0.10", 55 | "consola": "^2.15.0", 56 | "dotenv": "^16.0.0", 57 | "eslint": "^8.13.0", 58 | "eslint-config-prettier": "^8.5.0", 59 | "eslint-config-standard": "^16.0.3", 60 | "eslint-plugin-import": "^2.22.0", 61 | "eslint-plugin-mocha-no-only": "^1.1.1", 62 | "eslint-plugin-node": "^11.1.0", 63 | "eslint-plugin-prettier": "^4.0.0", 64 | "eslint-plugin-promise": "^6.0.0", 65 | "eslint-plugin-standard": "5.0.0", 66 | "ethereum-waffle": "^3.1.1", 67 | "ethers": "^5.0.18", 68 | "graphql": "^16.5.0", 69 | "hardhat": "^2.6.1", 70 | "hardhat-abi-exporter": "^2.0.1", 71 | "hardhat-contract-sizer": "^2.0.1", 72 | "hardhat-deploy": "^0.7.0-beta.9", 73 | "hardhat-gas-reporter": "^1.0.1", 74 | "inquirer": "8.0.0", 75 | "p-queue": "^6.6.2", 76 | "prettier": "^2.1.1", 77 | "prettier-plugin-solidity": "^1.0.0-alpha.56", 78 | "solhint": "^3.3.7", 79 | "solhint-plugin-prettier": "^0.0.5", 80 | "ts-node": "^10.9.1", 81 | "typechain": "^5.0.0", 82 | "typescript": "^4.0.2" 83 | }, 84 | "dependencies": {} 85 | } 86 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | yarn graphclient build 6 | yarn run compile -------------------------------------------------------------------------------- /scripts/coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | yarn run compile 6 | npx hardhat coverage $@ 7 | -------------------------------------------------------------------------------- /scripts/flatten: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUT_DIR="build/flatten" 4 | 5 | mkdir -p ${OUT_DIR} 6 | 7 | echo "Flattening contracts..." 8 | 9 | FILES=( 10 | "contracts/GraphTokenDistributor.sol" 11 | "contracts/GraphTokenLockSimple.sol" 12 | "contracts/GraphTokenLockWallet.sol" 13 | "contracts/GraphTokenLockManager.sol" 14 | ) 15 | 16 | for path in ${FILES[@]}; do 17 | IFS='/' 18 | parts=( $path ) 19 | name=${parts[${#parts[@]}-1]} 20 | echo "Flatten > ${name}" 21 | hardhat flatten "${path}" > "${OUT_DIR}/${name}" 22 | done 23 | 24 | echo "Done!" 25 | -------------------------------------------------------------------------------- /scripts/prepublish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TYPECHAIN_DIR=dist/types 4 | 5 | set -eo pipefail 6 | 7 | # Build contracts 8 | yarn run clean 9 | yarn run build 10 | 11 | # Refresh distribution folder 12 | rm -rf dist && mkdir -p dist 13 | mkdir -p ${TYPECHAIN_DIR}/_src 14 | cp -R build/abis/ dist/abis 15 | cp -R build/typechain/contracts/ ${TYPECHAIN_DIR}/_src 16 | cp -R deployments/ dist/deployments 17 | cp -R .openzeppelin/ dist/.openzeppelin 18 | 19 | ### Build Typechain bindings 20 | 21 | # Build and create TS declarations 22 | tsc -d ${TYPECHAIN_DIR}/_src/*.ts --outdir ${TYPECHAIN_DIR}/contracts --esModuleInterop 23 | # Copy back sources 24 | cp ${TYPECHAIN_DIR}/_src/*.ts ${TYPECHAIN_DIR}/contracts 25 | # Delete temporary src dir 26 | rm -rf ${TYPECHAIN_DIR}/_src 27 | -------------------------------------------------------------------------------- /scripts/security: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Before running: 4 | # This tool requires to have solc installed. 5 | # Ensure that you have the binaries installed by pip3 in your path. 6 | # Install: https://github.com/crytic/slither#how-to-install 7 | # Usage: https://github.com/crytic/slither/wiki/Usage 8 | 9 | mkdir -p reports 10 | 11 | pip3 install --user slither-analyzer && \ 12 | yarn run build && \ 13 | 14 | echo "Analyzing contracts..." 15 | slither . &> reports/analyzer-report.log && \ 16 | 17 | echo "Done!" 18 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | MNEMONIC="myth like bonus scare over problem client lizard pioneer submit female collect" 6 | TESTRPC_PORT=8545 7 | 8 | ### Functions 9 | 10 | evm_running() { 11 | nc -z localhost "$TESTRPC_PORT" 12 | } 13 | 14 | evm_start() { 15 | echo "Starting our own evm instance at port $TESTRPC_PORT" 16 | npx ganache-cli -m "$MNEMONIC" -i 1337 --gasLimit 8000000 --port "$TESTRPC_PORT" > /dev/null & 17 | evm_pid=$! 18 | } 19 | 20 | evm_kill() { 21 | if evm_running; then 22 | echo "Killing evm instance at port $TESTRPC_PORT" 23 | kill -9 $(lsof -i:$TESTRPC_PORT -t) 24 | fi 25 | } 26 | 27 | ### Setup evm 28 | 29 | # Gas reporter needs to run in its own evm instance 30 | if [ "$RUN_EVM" = true ]; then 31 | evm_kill 32 | evm_start 33 | sleep 5 34 | fi 35 | 36 | ### Main 37 | 38 | mkdir -p reports 39 | 40 | yarn run compile 41 | 42 | if [ "$RUN_EVM" = true ]; then 43 | # Run using the standalone evm instance 44 | npx hardhat test --network ganache 45 | result=$? 46 | else 47 | # Run using the default evm 48 | npx hardhat test "$@" 49 | result=$? 50 | fi 51 | 52 | ### Cleanup 53 | 54 | # Exit error mode so the evm instance always gets killed 55 | set +e 56 | result=0 57 | 58 | if [ "$RUN_EVM" = true ]; then 59 | evm_kill 60 | fi 61 | 62 | exit $result 63 | -------------------------------------------------------------------------------- /test/config.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Contract } from 'ethers' 2 | 3 | import { Account } from './network' 4 | 5 | export enum Revocability { 6 | NotSet, 7 | Enabled, 8 | Disabled, 9 | } 10 | 11 | export interface TokenLockSchedule { 12 | startTime: number 13 | endTime: number 14 | periods: number 15 | revocable: Revocability 16 | releaseStartTime: number 17 | vestingCliffTime: number 18 | } 19 | export interface TokenLockParameters { 20 | owner: string 21 | beneficiary: string 22 | token: string 23 | managedAmount: BigNumber 24 | startTime: number 25 | endTime: number 26 | periods: number 27 | revocable: Revocability 28 | releaseStartTime: number 29 | vestingCliffTime: number 30 | } 31 | 32 | export interface DateRange { 33 | startTime: number 34 | endTime: number 35 | } 36 | 37 | const dateRange = (months: number): DateRange => { 38 | const date = new Date(+new Date() - 120) // set start time for a few seconds before 39 | const newDate = new Date().setMonth(date.getMonth() + months) 40 | return { startTime: Math.round(+date / 1000), endTime: Math.round(+newDate / 1000) } 41 | } 42 | 43 | const moveTime = (time: number, months: number) => { 44 | const date = new Date(time * 1000) 45 | return Math.round(+date.setMonth(date.getMonth() + months) / 1000) 46 | } 47 | 48 | const moveDateRange = (dateRange: DateRange, months: number) => { 49 | return { 50 | startTime: moveTime(dateRange.startTime, months), 51 | endTime: moveTime(dateRange.endTime, months), 52 | } 53 | } 54 | 55 | const createSchedule = ( 56 | startMonths: number, 57 | durationMonths: number, 58 | periods: number, 59 | revocable: Revocability, 60 | releaseStartMonths = 0, 61 | vestingCliffMonths = 0, 62 | ) => { 63 | const range = dateRange(durationMonths) 64 | return { 65 | ...moveDateRange(range, startMonths), 66 | periods, 67 | revocable, 68 | releaseStartTime: releaseStartMonths > 0 ? moveTime(range.startTime, releaseStartMonths) : 0, 69 | vestingCliffTime: vestingCliffMonths > 0 ? moveTime(range.startTime, vestingCliffMonths) : 0, 70 | } 71 | } 72 | 73 | export const createScheduleScenarios = (): Array => { 74 | return [ 75 | createSchedule(0, 6, 1, Revocability.Disabled), // 6m lock-up + full release + fully vested 76 | createSchedule(0, 12, 1, Revocability.Disabled), // 12m lock-up + full release + fully vested 77 | createSchedule(12, 12, 12, Revocability.Disabled), // 12m lock-up + 1/12 releases + fully vested 78 | createSchedule(0, 12, 12, Revocability.Disabled), // no-lockup + 1/12 releases + fully vested 79 | createSchedule(-12, 48, 48, Revocability.Enabled, 0), // 1/48 releases + vested + past start + start time override 80 | createSchedule(-12, 48, 48, Revocability.Enabled, 0, 12), // 1/48 releases + vested + past start + start time override + cliff 81 | ] 82 | } 83 | 84 | export const defaultInitArgs = ( 85 | deployer: Account, 86 | beneficiary: Account, 87 | token: Contract, 88 | managedAmount: BigNumber, 89 | ): TokenLockParameters => { 90 | const constantData = { 91 | owner: deployer.address, 92 | beneficiary: beneficiary.address, 93 | token: token.address, 94 | managedAmount, 95 | } 96 | 97 | return { 98 | ...createSchedule(0, 6, 1, Revocability.Disabled), 99 | ...constantData, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/distributor.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { deployments } from 'hardhat' 3 | import 'hardhat-deploy' 4 | 5 | import { GraphTokenMock } from '../build/typechain/contracts/GraphTokenMock' 6 | import { GraphTokenDistributor } from '../build/typechain/contracts/GraphTokenDistributor' 7 | 8 | import { getContract, getAccounts, toGRT, Account } from './network' 9 | 10 | // Fixture 11 | const setupTest = deployments.createFixture(async ({ deployments }) => { 12 | const { deploy } = deployments 13 | const [deployer] = await getAccounts() 14 | 15 | // Start from a fresh snapshot 16 | await deployments.fixture([]) 17 | 18 | // Deploy token 19 | await deploy('GraphTokenMock', { 20 | from: deployer.address, 21 | args: [toGRT('400000000'), deployer.address], 22 | }) 23 | const grt = await getContract('GraphTokenMock') 24 | 25 | // Deploy distributor 26 | await deploy('GraphTokenDistributor', { 27 | from: deployer.address, 28 | args: [grt.address], 29 | }) 30 | const distributor = await getContract('GraphTokenDistributor') 31 | 32 | return { 33 | grt: grt as GraphTokenMock, 34 | distributor: distributor as GraphTokenDistributor, 35 | } 36 | }) 37 | 38 | describe('GraphTokenDistributor', () => { 39 | let deployer: Account 40 | let beneficiary1: Account 41 | let beneficiary2: Account 42 | 43 | let grt: GraphTokenMock 44 | let distributor: GraphTokenDistributor 45 | 46 | before(async function () { 47 | ;[deployer, beneficiary1, beneficiary2] = await getAccounts() 48 | }) 49 | 50 | beforeEach(async () => { 51 | ;({ grt, distributor } = await setupTest()) 52 | }) 53 | 54 | describe('init', function () { 55 | it('should deploy locked', async function () { 56 | const isLocked = await distributor.locked() 57 | expect(isLocked).eq(true) 58 | }) 59 | }) 60 | 61 | describe('setup beneficiary', function () { 62 | const amount = toGRT('100') 63 | 64 | describe('add', function () { 65 | it('should add tokens to beneficiary', async function () { 66 | const tx = distributor.connect(deployer.signer).addBeneficiaryTokens(beneficiary1.address, amount) 67 | await expect(tx).emit(distributor, 'BeneficiaryUpdated').withArgs(beneficiary1.address, amount) 68 | }) 69 | 70 | it('reject add tokens to beneficiary if not allowed', async function () { 71 | const tx = distributor.connect(beneficiary1.signer).addBeneficiaryTokens(beneficiary1.address, amount) 72 | await expect(tx).revertedWith('Ownable: caller is not the owner') 73 | }) 74 | 75 | it('should add tokens to multiple beneficiaries', async function () { 76 | const accounts = [beneficiary1.address, beneficiary2.address] 77 | const amounts = [amount, amount] 78 | 79 | await distributor.connect(deployer.signer).addBeneficiaryTokensMulti(accounts, amounts) 80 | }) 81 | 82 | it('reject add token to multiple beneficiaries if not allowed', async function () { 83 | const accounts = [beneficiary1.address, beneficiary2.address] 84 | const amounts = [amount, amount] 85 | 86 | const tx = distributor.connect(beneficiary1.signer).addBeneficiaryTokensMulti(accounts, amounts) 87 | await expect(tx).revertedWith('Ownable: caller is not the owner') 88 | }) 89 | }) 90 | 91 | describe('sub', function () { 92 | it('should remove tokens from beneficiary', async function () { 93 | await distributor.addBeneficiaryTokens(beneficiary1.address, amount) 94 | 95 | const tx = distributor.subBeneficiaryTokens(beneficiary1.address, amount) 96 | await expect(tx).emit(distributor, 'BeneficiaryUpdated').withArgs(beneficiary1.address, toGRT('0')) 97 | }) 98 | 99 | it('reject remove more tokens than available ', async function () { 100 | const tx = distributor.subBeneficiaryTokens(beneficiary1.address, toGRT('1000')) 101 | await expect(tx).revertedWith('SafeMath: subtraction overflow') 102 | }) 103 | }) 104 | }) 105 | 106 | describe('unlocking', function () { 107 | it('should lock', async function () { 108 | const tx = distributor.connect(deployer.signer).setLocked(true) 109 | await expect(tx).emit(distributor, 'LockUpdated').withArgs(true) 110 | expect(await distributor.locked()).eq(true) 111 | }) 112 | 113 | it('should unlock', async function () { 114 | const tx = distributor.connect(deployer.signer).setLocked(false) 115 | await expect(tx).emit(distributor, 'LockUpdated').withArgs(false) 116 | expect(await distributor.locked()).eq(false) 117 | }) 118 | 119 | it('reject unlock if not allowed', async function () { 120 | const tx = distributor.connect(beneficiary1.signer).setLocked(false) 121 | await expect(tx).revertedWith('Ownable: caller is not the owner') 122 | }) 123 | }) 124 | 125 | describe('claim', function () { 126 | const totalAmount = toGRT('1000000') 127 | const amount = toGRT('10000') 128 | 129 | beforeEach(async function () { 130 | // Setup 131 | await grt.transfer(distributor.address, totalAmount) 132 | await distributor.connect(deployer.signer).addBeneficiaryTokens(beneficiary1.address, amount) 133 | }) 134 | 135 | it('should claim outstanding token amount', async function () { 136 | await distributor.connect(deployer.signer).setLocked(false) 137 | 138 | const tx = distributor.connect(beneficiary1.signer).claim() 139 | await expect(tx).emit(distributor, 'TokensClaimed').withArgs(beneficiary1.address, beneficiary1.address, amount) 140 | }) 141 | 142 | it('reject claim if locked', async function () { 143 | const tx = distributor.connect(beneficiary1.signer).claim() 144 | await expect(tx).revertedWith('Distributor: Claim is locked') 145 | }) 146 | 147 | it('reject claim if no available tokens', async function () { 148 | await distributor.connect(deployer.signer).setLocked(false) 149 | 150 | const tx = distributor.connect(beneficiary2.signer).claim() 151 | await expect(tx).revertedWith('Distributor: Unavailable funds') 152 | }) 153 | 154 | it('reject claim if beneficiary already claimed all tokens', async function () { 155 | await distributor.connect(deployer.signer).setLocked(false) 156 | 157 | await distributor.connect(beneficiary1.signer).claim() 158 | const tx = distributor.connect(beneficiary1.signer).claim() 159 | await expect(tx).revertedWith('Distributor: Unavailable funds') 160 | }) 161 | }) 162 | 163 | describe('deposit & withdraw', function () { 164 | it('should deposit funds into the distributor', async function () { 165 | const beforeBalance = await grt.balanceOf(distributor.address) 166 | 167 | const amount = toGRT('1000') 168 | await grt.approve(distributor.address, amount) 169 | const tx = distributor.connect(distributor.signer).deposit(amount) 170 | await expect(tx).emit(distributor, 'TokensDeposited').withArgs(deployer.address, amount) 171 | 172 | const afterBalance = await grt.balanceOf(distributor.address) 173 | expect(afterBalance).eq(beforeBalance.add(amount)) 174 | }) 175 | 176 | it('should withdraw tokens from the contract if owner', async function () { 177 | // Setup 178 | const amount = toGRT('1000') 179 | await grt.approve(distributor.address, amount) 180 | await distributor.connect(distributor.signer).deposit(amount) 181 | 182 | const tx = distributor.connect(deployer.signer).withdraw(amount) 183 | await expect(tx).emit(distributor, 'TokensWithdrawn').withArgs(deployer.address, amount) 184 | 185 | const afterBalance = await grt.balanceOf(distributor.address) 186 | expect(afterBalance).eq(0) 187 | }) 188 | 189 | it('reject withdraw tokens from the contract if no balance', async function () { 190 | const amount = toGRT('1000') 191 | const tx = distributor.connect(deployer.signer).withdraw(amount) 192 | await expect(tx).revertedWith('ERC20: transfer amount exceeds balance') 193 | }) 194 | 195 | it('reject withdraw tokens from the contract if not allowed', async function () { 196 | const amount = toGRT('1000') 197 | const tx = distributor.connect(beneficiary1.signer).withdraw(amount) 198 | await expect(tx).revertedWith('Ownable: caller is not the owner') 199 | }) 200 | }) 201 | }) 202 | -------------------------------------------------------------------------------- /test/l2TokenLockTransferTool.test.ts: -------------------------------------------------------------------------------- 1 | import { constants } from 'ethers' 2 | import { expect } from 'chai' 3 | import { deployments, ethers, upgrades } from 'hardhat' 4 | 5 | import '@nomiclabs/hardhat-ethers' 6 | import 'hardhat-deploy' 7 | 8 | import { GraphTokenMock } from '../build/typechain/contracts/GraphTokenMock' 9 | import { L2GraphTokenLockWallet } from '../build/typechain/contracts/L2GraphTokenLockWallet' 10 | import { L2GraphTokenLockManager } from '../build/typechain/contracts/L2GraphTokenLockManager' 11 | import { L2TokenGatewayMock } from '../build/typechain/contracts/L2TokenGatewayMock' 12 | import { L2GraphTokenLockTransferTool } from '../build/typechain/contracts/L2GraphTokenLockTransferTool' 13 | import { L2GraphTokenLockTransferTool__factory } from '../build/typechain/contracts/factories/L2GraphTokenLockTransferTool__factory' 14 | 15 | import { defaultInitArgs, TokenLockParameters } from './config' 16 | import { getAccounts, getContract, toGRT, Account, toBN } from './network' 17 | import { defaultAbiCoder, keccak256 } from 'ethers/lib/utils' 18 | 19 | const { AddressZero } = constants 20 | 21 | // Fixture 22 | const setupTest = deployments.createFixture(async ({ deployments }) => { 23 | const { deploy } = deployments 24 | const [deployer, , l1TransferToolMock, l1GRTMock] = await getAccounts() 25 | 26 | // Start from a fresh snapshot 27 | await deployments.fixture([]) 28 | 29 | // Deploy token 30 | await deploy('GraphTokenMock', { 31 | from: deployer.address, 32 | args: [toGRT('1000000000'), deployer.address], 33 | }) 34 | const grt = await getContract('GraphTokenMock') 35 | 36 | // Deploy token lock masterCopy 37 | await deploy('L2GraphTokenLockWallet', { 38 | from: deployer.address, 39 | }) 40 | const tokenLockWallet = await getContract('L2GraphTokenLockWallet') 41 | 42 | // Deploy the gateway mock 43 | await deploy('L2TokenGatewayMock', { 44 | from: deployer.address, 45 | args: [l1GRTMock.address, grt.address], 46 | }) 47 | const gateway = await getContract('L2TokenGatewayMock') 48 | 49 | // Deploy token lock manager 50 | await deploy('L2GraphTokenLockManager', { 51 | from: deployer.address, 52 | args: [grt.address, tokenLockWallet.address, gateway.address, l1TransferToolMock.address], 53 | }) 54 | const tokenLockManager = await getContract('L2GraphTokenLockManager') 55 | 56 | // Deploy the L2GraphTokenLockTransferTool using a proxy 57 | 58 | // Deploy transfer tool using a proxy 59 | const transferToolFactory = await ethers.getContractFactory('L2GraphTokenLockTransferTool') 60 | const tokenLockTransferTool = (await upgrades.deployProxy(transferToolFactory, [], { 61 | kind: 'transparent', 62 | unsafeAllow: ['state-variable-immutable', 'constructor'], 63 | constructorArgs: [grt.address, gateway.address, l1GRTMock.address], 64 | })) as L2GraphTokenLockTransferTool 65 | 66 | // Fund the manager contract 67 | await grt.connect(deployer.signer).transfer(tokenLockManager.address, toGRT('100000000')) 68 | 69 | return { 70 | grt: grt as GraphTokenMock, 71 | gateway: gateway as L2TokenGatewayMock, 72 | tokenLockTransferTool: tokenLockTransferTool as L2GraphTokenLockTransferTool, 73 | tokenLockImplementation: tokenLockWallet as L2GraphTokenLockWallet, 74 | tokenLockManager: tokenLockManager as L2GraphTokenLockManager, 75 | } 76 | }) 77 | 78 | async function authProtocolFunctions(tokenLockManager: L2GraphTokenLockManager, tokenLockTransferToolAddress: string) { 79 | await tokenLockManager.setAuthFunctionCall('withdrawToL1Locked(uint256)', tokenLockTransferToolAddress) 80 | } 81 | 82 | describe('L2GraphTokenLockTransferTool', () => { 83 | let deployer: Account 84 | let beneficiary: Account 85 | let l1TransferToolMock: Account 86 | let l1GRTMock: Account 87 | let l1TokenLock: Account 88 | 89 | let grt: GraphTokenMock 90 | let tokenLock: L2GraphTokenLockWallet 91 | let tokenLockImplementation: L2GraphTokenLockWallet 92 | let tokenLockManager: L2GraphTokenLockManager 93 | let tokenLockTransferTool: L2GraphTokenLockTransferTool 94 | let gateway: L2TokenGatewayMock 95 | let lockAsTransferTool: L2GraphTokenLockTransferTool 96 | 97 | let initArgs: TokenLockParameters 98 | 99 | const initWithArgs = async (args: TokenLockParameters): Promise => { 100 | const tx = await tokenLockManager.createTokenLockWallet( 101 | args.owner, 102 | args.beneficiary, 103 | args.managedAmount, 104 | args.startTime, 105 | args.endTime, 106 | args.periods, 107 | args.releaseStartTime, 108 | args.vestingCliffTime, 109 | args.revocable, 110 | ) 111 | const receipt = await tx.wait() 112 | const contractAddress = receipt.events[0].args['proxy'] 113 | return ethers.getContractAt('L2GraphTokenLockWallet', contractAddress) as Promise 114 | } 115 | 116 | const initFromL1 = async (): Promise => { 117 | // First we mock creating a token lock wallet through the gateway 118 | // ABI-encoded callhook data 119 | initArgs = defaultInitArgs(deployer, beneficiary, grt, toGRT('35000000')) 120 | const walletDataType = 'tuple(address,address,address,uint256,uint256,uint256)' 121 | const data = defaultAbiCoder.encode( 122 | [walletDataType], 123 | [ 124 | [ 125 | l1TokenLock.address, 126 | initArgs.owner, 127 | initArgs.beneficiary, 128 | initArgs.managedAmount, 129 | initArgs.startTime, 130 | initArgs.endTime, 131 | ], 132 | ], 133 | ) 134 | 135 | // Mock the gateway call 136 | const tx = gateway.finalizeInboundTransfer( 137 | l1GRTMock.address, 138 | l1TransferToolMock.address, 139 | tokenLockManager.address, 140 | toGRT('35000000'), 141 | data, 142 | ) 143 | 144 | await expect(tx).emit(tokenLockManager, 'TokenLockCreatedFromL1') 145 | 146 | const expectedL2Address = await tokenLockManager['getDeploymentAddress(bytes32,address,address)']( 147 | keccak256(data), 148 | tokenLockImplementation.address, 149 | tokenLockManager.address, 150 | ) 151 | 152 | return ethers.getContractAt('L2GraphTokenLockWallet', expectedL2Address) as Promise 153 | } 154 | 155 | before(async function () { 156 | ;[deployer, beneficiary, l1TransferToolMock, l1GRTMock, l1TokenLock] = await getAccounts() 157 | }) 158 | 159 | beforeEach(async () => { 160 | ;({ grt, gateway, tokenLockTransferTool, tokenLockImplementation, tokenLockManager } = await setupTest()) 161 | 162 | // Setup authorized functions in Manager 163 | await authProtocolFunctions(tokenLockManager, tokenLockTransferTool.address) 164 | 165 | // Add the transfer tool contract as token destination 166 | await tokenLockManager.addTokenDestination(tokenLockTransferTool.address) 167 | }) 168 | 169 | describe('Upgrades', function () { 170 | it('should be upgradeable', async function () { 171 | const transferToolFactory = await ethers.getContractFactory('L2GraphTokenLockTransferTool') 172 | tokenLockTransferTool = (await upgrades.upgradeProxy(tokenLockTransferTool.address, transferToolFactory, { 173 | kind: 'transparent', 174 | unsafeAllow: ['state-variable-immutable', 'constructor'], 175 | constructorArgs: [beneficiary.address, gateway.address, l1GRTMock.address], 176 | })) as L2GraphTokenLockTransferTool 177 | expect(await tokenLockTransferTool.graphToken()).to.eq(beneficiary.address) 178 | tokenLockTransferTool = (await upgrades.upgradeProxy(tokenLockTransferTool.address, transferToolFactory, { 179 | kind: 'transparent', 180 | unsafeAllow: ['state-variable-immutable', 'constructor'], 181 | constructorArgs: [grt.address, gateway.address, l1GRTMock.address], 182 | })) as L2GraphTokenLockTransferTool 183 | expect(await tokenLockTransferTool.graphToken()).to.eq(grt.address) 184 | }) 185 | }) 186 | describe('withdrawToL1Locked', function () { 187 | it('allows a token lock wallet to send GRT to L1 through the gateway', async function () { 188 | tokenLock = await initFromL1() 189 | // Approve contracts to pull tokens from the token lock 190 | await tokenLock.connect(beneficiary.signer).approveProtocol() 191 | 192 | lockAsTransferTool = L2GraphTokenLockTransferTool__factory.connect(tokenLock.address, deployer.signer) 193 | 194 | const amountToSend = toGRT('1000000') 195 | const tx = await lockAsTransferTool.connect(beneficiary.signer).withdrawToL1Locked(amountToSend) 196 | 197 | await expect(tx).emit(gateway, 'WithdrawalInitiated').withArgs( 198 | l1GRTMock.address, 199 | tokenLockTransferTool.address, 200 | l1TokenLock.address, 201 | toBN('0'), // sequence number 202 | amountToSend, 203 | ) 204 | await expect(tx) 205 | .emit(tokenLockTransferTool, 'LockedFundsSentToL1') 206 | .withArgs(l1TokenLock.address, tokenLock.address, tokenLockManager.address, amountToSend) 207 | }) 208 | it('rejects calls from a lock that was not transferred from L1', async function () { 209 | tokenLock = await initWithArgs(defaultInitArgs(deployer, beneficiary, grt, toGRT('35000000'))) 210 | // Approve contracts to pull tokens from the token lock 211 | await tokenLock.connect(beneficiary.signer).approveProtocol() 212 | 213 | lockAsTransferTool = L2GraphTokenLockTransferTool__factory.connect(tokenLock.address, deployer.signer) 214 | 215 | const amountToSend = toGRT('1000000') 216 | const tx = lockAsTransferTool.connect(beneficiary.signer).withdrawToL1Locked(amountToSend) 217 | 218 | await expect(tx).to.be.revertedWith('NOT_L1_WALLET') 219 | }) 220 | it('rejects calls from an address that is not a lock (has no manager)', async function () { 221 | const tx = tokenLockTransferTool.connect(beneficiary.signer).withdrawToL1Locked(toGRT('1000000')) 222 | await expect(tx).to.be.reverted // Function call to a non-contract account 223 | }) 224 | it('rejects calls from an address that has a manager() function that returns zero', async function () { 225 | // Use WalletMock to simulate an invalid wallet with no manager 226 | // WalletMock constructor args are: target, token, manager, isInitialized, isAccepted 227 | await deployments.deploy('WalletMock', { 228 | from: deployer.address, 229 | args: [tokenLockTransferTool.address, grt.address, AddressZero, true, true], 230 | }) 231 | const invalidWallet = await getContract('WalletMock') 232 | const walletAsTransferTool = L2GraphTokenLockTransferTool__factory.connect(invalidWallet.address, deployer.signer) 233 | 234 | const tx = walletAsTransferTool.connect(beneficiary.signer).withdrawToL1Locked(toGRT('1000000')) 235 | await expect(tx).to.be.revertedWith('INVALID_SENDER') 236 | }) 237 | it('rejects calls from a lock that has insufficient GRT balance', async function () { 238 | tokenLock = await initFromL1() 239 | // Approve contracts to pull tokens from the token lock 240 | await tokenLock.connect(beneficiary.signer).approveProtocol() 241 | 242 | lockAsTransferTool = L2GraphTokenLockTransferTool__factory.connect(tokenLock.address, deployer.signer) 243 | 244 | const amountToSend = toGRT('35000001') 245 | const tx = lockAsTransferTool.connect(beneficiary.signer).withdrawToL1Locked(amountToSend) 246 | 247 | await expect(tx).to.be.revertedWith('INSUFFICIENT_BALANCE') 248 | }) 249 | it('rejects calls trying to send a zero amount', async function () { 250 | tokenLock = await initFromL1() 251 | // Approve contracts to pull tokens from the token lock 252 | await tokenLock.connect(beneficiary.signer).approveProtocol() 253 | 254 | lockAsTransferTool = L2GraphTokenLockTransferTool__factory.connect(tokenLock.address, deployer.signer) 255 | 256 | const amountToSend = toGRT('0') 257 | const tx = lockAsTransferTool.connect(beneficiary.signer).withdrawToL1Locked(amountToSend) 258 | 259 | await expect(tx).to.be.revertedWith('ZERO_AMOUNT') 260 | }) 261 | }) 262 | }) 263 | -------------------------------------------------------------------------------- /test/network.ts: -------------------------------------------------------------------------------- 1 | import { providers, utils, BigNumber, Contract, Signer } from 'ethers' 2 | import { deployments, ethers, network, waffle } from 'hardhat' 3 | 4 | // Plugins 5 | 6 | import '@nomiclabs/hardhat-ethers' 7 | import '@nomiclabs/hardhat-waffle' 8 | import 'hardhat-deploy' 9 | 10 | const { hexlify, parseUnits, formatUnits, randomBytes } = utils 11 | 12 | // Utils 13 | 14 | export const toBN = (value: string | number): BigNumber => BigNumber.from(value) 15 | export const toGRT = (value: string): BigNumber => parseUnits(value, '18') 16 | export const formatGRT = (value: BigNumber): string => formatUnits(value, '18') 17 | export const randomHexBytes = (n = 32): string => hexlify(randomBytes(n)) 18 | 19 | // Contracts 20 | 21 | export const getContract = async (contractName: string): Promise => { 22 | const deployment = await deployments.get(contractName) 23 | return ethers.getContractAt(contractName, deployment.address) 24 | } 25 | 26 | // Network 27 | 28 | export interface Account { 29 | readonly signer: Signer 30 | readonly address: string 31 | } 32 | 33 | export const provider = (): providers.JsonRpcProvider => waffle.provider 34 | 35 | export const getAccounts = async (): Promise => { 36 | const accounts = [] 37 | const signers: Signer[] = await ethers.getSigners() 38 | for (const signer of signers) { 39 | accounts.push({ signer, address: await signer.getAddress() }) 40 | } 41 | return accounts 42 | } 43 | 44 | export const getChainID = (): Promise => { 45 | // HACK: this fixes ganache returning always 1 when a contract calls the chainid() opcode 46 | if (network.name == 'ganache') { 47 | return Promise.resolve(1) 48 | } 49 | return provider() 50 | .getNetwork() 51 | .then((r) => r.chainId) 52 | } 53 | 54 | export const latestBlockNum = (): Promise => provider().getBlockNumber().then(toBN) 55 | export const latestBlock = async (): Promise => provider().getBlock(await provider().getBlockNumber()) 56 | export const latestBlockTime = async (): Promise => latestBlock().then((block) => block.timestamp) 57 | 58 | export const advanceBlock = (): Promise => { 59 | return provider().send('evm_mine', []) 60 | } 61 | 62 | export const advanceBlockTo = async (blockNumber: string | number | BigNumber): Promise => { 63 | const target = typeof blockNumber === 'number' || typeof blockNumber === 'string' ? toBN(blockNumber) : blockNumber 64 | const currentBlock = await latestBlockNum() 65 | const start = Date.now() 66 | let notified 67 | if (target.lt(currentBlock)) throw Error(`Target block #(${target}) is lower than current block #(${currentBlock})`) 68 | while ((await latestBlockNum()).lt(target)) { 69 | if (!notified && Date.now() - start >= 5000) { 70 | notified = true 71 | console.log(`advanceBlockTo: Advancing too ` + 'many blocks is causing this test to be slow.') 72 | } 73 | await advanceBlock() 74 | } 75 | } 76 | 77 | export const advanceBlocks = async (blocks: string | number | BigNumber): Promise => { 78 | const steps = typeof blocks === 'number' || typeof blocks === 'string' ? toBN(blocks) : blocks 79 | const currentBlock = await latestBlockNum() 80 | const toBlock = currentBlock.add(steps) 81 | await advanceBlockTo(toBlock) 82 | } 83 | 84 | export const advanceTime = async (time: number): Promise => { 85 | return provider().send('evm_increaseTime', [time]) 86 | } 87 | 88 | export const advanceTimeAndBlock = async (time: number): Promise => { 89 | await advanceTime(time) 90 | await advanceBlock() 91 | return latestBlockNum() 92 | } 93 | 94 | export const evmSnapshot = async (): Promise => provider().send('evm_snapshot', []) 95 | export const evmRevert = async (id: number): Promise => provider().send('evm_revert', [id]) 96 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2020", "dom"], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "ES2020", 7 | "outDir": "dist", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true 10 | }, 11 | "exclude": ["dist", "node_modules"], 12 | "files": [ 13 | "./hardhat.config.ts", 14 | "./scripts/**/*.ts", 15 | "./deploy/**/*.ts", 16 | "./test/**/*.ts", 17 | "node_modules/@nomiclabs/hardhat-ethers/src/type-extensions.d.ts", 18 | "node_modules/@nomiclabs/hardhat-etherscan/src/type-extensions.d.ts", 19 | "node_modules/@nomiclabs/hardhat-waffle/src/type-extensions.d.ts", 20 | "node_modules/hardhat-typechain/src/type-extensions.d.ts" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------