From f5e17bc71ddfcd4125179a51d6b288ec10103490 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Wed, 23 Mar 2022 18:36:27 -0300 Subject: [PATCH 01/78] feat: add Arbitrum GRT bridge Deployment commands in the CLI are also updated to include an L2 deployment. Configuration and address book entries for Arbitrum are added as well. --- .solcover.js | 2 +- cli/commands/migrate.ts | 29 + config/graph.arbitrum-one.yml | 23 + config/graph.mainnet.yml | 11 + contracts/arbitrum/IArbToken.sol | 47 ++ contracts/arbitrum/IBridge.sol | 77 +++ contracts/arbitrum/IInbox.sol | 88 +++ contracts/arbitrum/IMessageProvider.sol | 32 + contracts/arbitrum/IOutbox.sol | 58 ++ contracts/arbitrum/ITokenGateway.sol | 74 +++ contracts/arbitrum/L1ArbitrumMessenger.sol | 103 ++++ contracts/arbitrum/L2ArbitrumMessenger.sol | 47 ++ contracts/arbitrum/README.md | 5 + contracts/gateway/BridgeEscrow.sol | 42 ++ contracts/gateway/GraphTokenGateway.sol | 57 ++ contracts/gateway/L1GraphTokenGateway.sol | 297 ++++++++++ contracts/governance/Managed.sol | 3 +- contracts/l2/gateway/L2GraphTokenGateway.sol | 272 +++++++++ contracts/l2/token/L2GraphToken.sol | 284 +++++++++ contracts/tests/arbitrum/BridgeMock.sol | 120 ++++ contracts/tests/arbitrum/InboxMock.sol | 209 +++++++ contracts/tests/arbitrum/OutboxMock.sol | 157 +++++ contracts/token/IGraphToken.sol | 6 + hardhat.config.ts | 2 + package.json | 2 + test/epochs.test.ts | 2 + test/gateway/bridgeEscrow.test.ts | 77 +++ test/gateway/l1GraphTokenGateway.test.ts | 580 +++++++++++++++++++ test/graphToken.test.ts | 274 +-------- test/l2/l2GraphToken.test.ts | 107 ++++ test/l2/l2GraphTokenGateway.test.ts | 379 ++++++++++++ test/lib/deployment.ts | 55 ++ test/lib/fixtures.ts | 59 +- test/lib/graphTokenTests.ts | 287 +++++++++ test/lib/testHelpers.ts | 19 + test/staking/allocation.test.ts | 2 +- test/staking/rebate.test.ts | 3 +- yarn.lock | 150 ++++- 38 files changed, 3749 insertions(+), 292 deletions(-) create mode 100644 config/graph.arbitrum-one.yml create mode 100644 contracts/arbitrum/IArbToken.sol create mode 100644 contracts/arbitrum/IBridge.sol create mode 100644 contracts/arbitrum/IInbox.sol create mode 100644 contracts/arbitrum/IMessageProvider.sol create mode 100644 contracts/arbitrum/IOutbox.sol create mode 100644 contracts/arbitrum/ITokenGateway.sol create mode 100644 contracts/arbitrum/L1ArbitrumMessenger.sol create mode 100644 contracts/arbitrum/L2ArbitrumMessenger.sol create mode 100644 contracts/arbitrum/README.md create mode 100644 contracts/gateway/BridgeEscrow.sol create mode 100644 contracts/gateway/GraphTokenGateway.sol create mode 100644 contracts/gateway/L1GraphTokenGateway.sol create mode 100644 contracts/l2/gateway/L2GraphTokenGateway.sol create mode 100644 contracts/l2/token/L2GraphToken.sol create mode 100644 contracts/tests/arbitrum/BridgeMock.sol create mode 100644 contracts/tests/arbitrum/InboxMock.sol create mode 100644 contracts/tests/arbitrum/OutboxMock.sol create mode 100644 test/gateway/bridgeEscrow.test.ts create mode 100644 test/gateway/l1GraphTokenGateway.test.ts create mode 100644 test/l2/l2GraphToken.test.ts create mode 100644 test/l2/l2GraphTokenGateway.test.ts create mode 100644 test/lib/graphTokenTests.ts diff --git a/.solcover.js b/.solcover.js index 747f078e5..b10738c1f 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,4 +1,4 @@ -const skipFiles = ['bancor', 'ens', 'erc1056'] +const skipFiles = ['bancor', 'ens', 'erc1056', 'arbitrum', 'tests/arbitrum'] module.exports = { providerOptions: { diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 82fe1e71b..0e538ed77 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -32,8 +32,35 @@ let allContracts = [ 'RewardsManager', 'DisputeManager', 'AllocationExchange', + 'L1GraphTokenGateway', + 'BridgeEscrow', ] +// This is all we'll want to deploy to L2 eventually: +// const l2Contracts = [ +// 'GraphProxyAdmin', +// 'BancorFormula', +// 'Controller', +// 'EpochManager', +// 'L2GraphToken', +// 'GraphCurationToken', +// 'ServiceRegistry', +// 'Curation', +// 'SubgraphNFTDescriptor', +// 'SubgraphNFT', +// 'GNS', +// 'Staking', +// 'RewardsManager', +// 'DisputeManager', +// 'AllocationExchange', +// 'L2GraphTokenGateway', +// ] +// +// But for now we'll only include a subset: +const l2Contracts = ['GraphProxyAdmin', 'Controller', 'L2GraphToken', 'L2GraphTokenGateway'] + +const l2ChainIds = [42161, 421611] + export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { const graphConfigPath = cliArgs.graphConfig const force = cliArgs.force @@ -42,6 +69,8 @@ export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise>> Migrating contracts <<<\n`) diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml new file mode 100644 index 000000000..80d7cceeb --- /dev/null +++ b/config/graph.arbitrum-one.yml @@ -0,0 +1,23 @@ +general: + arbitrator: &arbitrator "0x113DC95e796836b8F0Fa71eE7fB42f221740c3B0" # Arbitration Council + governor: &governor "0x3e43EF77fAAd296F65eF172E8eF06F8231c9DeAd" # Graph Council + authority: &authority "0x79fd74da4c906509862c8fe93e87a9602e370bc4" # Authority that signs payment vouchers + +contracts: + Controller: + calls: + - fn: "setContractProxy" + id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') + contractAddress: "${{L2GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L2GraphTokenGateway.address}}" + L2GraphToken: + proxy: true + init: + owner: *governor + initialSupply: "0" + L2GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index 72cadcc67..9ba114ec5 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -27,6 +27,9 @@ contracts: - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L1GraphTokenGateway.address}}" ServiceRegistry: proxy: true init: @@ -112,3 +115,11 @@ contracts: authority: *authority calls: - fn: "approveAll" + L1GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" + BridgeEscrow: + proxy: true + init: + controller: "${{Controller.address}}" diff --git a/contracts/arbitrum/IArbToken.sol b/contracts/arbitrum/IArbToken.sol new file mode 100644 index 000000000..d7d5a2d8c --- /dev/null +++ b/contracts/arbitrum/IArbToken.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2020, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +/** + * @title Minimum expected interface for L2 token that interacts with the L2 token bridge (this is the interface necessary + * for a custom token that interacts with the bridge, see TestArbCustomToken.sol for an example implementation). + */ +pragma solidity ^0.7.6; + +interface IArbToken { + /** + * @notice should increase token supply by amount, and should (probably) only be callable by the L1 bridge. + */ + function bridgeMint(address account, uint256 amount) external; + + /** + * @notice should decrease token supply by amount, and should (probably) only be callable by the L1 bridge. + */ + function bridgeBurn(address account, uint256 amount) external; + + /** + * @return address of layer 1 token + */ + function l1Address() external view returns (address); +} diff --git a/contracts/arbitrum/IBridge.sol b/contracts/arbitrum/IBridge.sol new file mode 100644 index 000000000..ff78253fc --- /dev/null +++ b/contracts/arbitrum/IBridge.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +interface IBridge { + event MessageDelivered( + uint256 indexed messageIndex, + bytes32 indexed beforeInboxAcc, + address inbox, + uint8 kind, + address sender, + bytes32 messageDataHash + ); + + event BridgeCallTriggered( + address indexed outbox, + address indexed destAddr, + uint256 amount, + bytes data + ); + + event InboxToggle(address indexed inbox, bool enabled); + + event OutboxToggle(address indexed outbox, bool enabled); + + function deliverMessageToInbox( + uint8 kind, + address sender, + bytes32 messageDataHash + ) external payable returns (uint256); + + function executeCall( + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (bool success, bytes memory returnData); + + // These are only callable by the admin + function setInbox(address inbox, bool enabled) external; + + function setOutbox(address inbox, bool enabled) external; + + // View functions + + function activeOutbox() external view returns (address); + + function allowedInboxes(address inbox) external view returns (bool); + + function allowedOutboxes(address outbox) external view returns (bool); + + function inboxAccs(uint256 index) external view returns (bytes32); + + function messageCount() external view returns (uint256); +} diff --git a/contracts/arbitrum/IInbox.sol b/contracts/arbitrum/IInbox.sol new file mode 100644 index 000000000..a9315bbf8 --- /dev/null +++ b/contracts/arbitrum/IInbox.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +import "./IBridge.sol"; +import "./IMessageProvider.sol"; + +interface IInbox is IMessageProvider { + function sendL2Message(bytes calldata messageData) external returns (uint256); + + function sendUnsignedTransaction( + uint256 maxGas, + uint256 gasPriceBid, + uint256 nonce, + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (uint256); + + function sendContractTransaction( + uint256 maxGas, + uint256 gasPriceBid, + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (uint256); + + function sendL1FundedUnsignedTransaction( + uint256 maxGas, + uint256 gasPriceBid, + uint256 nonce, + address destAddr, + bytes calldata data + ) external payable returns (uint256); + + function sendL1FundedContractTransaction( + uint256 maxGas, + uint256 gasPriceBid, + address destAddr, + bytes calldata data + ) external payable returns (uint256); + + function createRetryableTicket( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external payable returns (uint256); + + function depositEth(uint256 maxSubmissionCost) external payable returns (uint256); + + function bridge() external view returns (IBridge); + + function pauseCreateRetryables() external; + + function unpauseCreateRetryables() external; + + function startRewriteAddress() external; + + function stopRewriteAddress() external; +} diff --git a/contracts/arbitrum/IMessageProvider.sol b/contracts/arbitrum/IMessageProvider.sol new file mode 100644 index 000000000..8fbfdb171 --- /dev/null +++ b/contracts/arbitrum/IMessageProvider.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +interface IMessageProvider { + event InboxMessageDelivered(uint256 indexed messageNum, bytes data); + + event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); +} diff --git a/contracts/arbitrum/IOutbox.sol b/contracts/arbitrum/IOutbox.sol new file mode 100644 index 000000000..687c86abf --- /dev/null +++ b/contracts/arbitrum/IOutbox.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +interface IOutbox { + event OutboxEntryCreated( + uint256 indexed batchNum, + uint256 outboxEntryIndex, + bytes32 outputRoot, + uint256 numInBatch + ); + event OutBoxTransactionExecuted( + address indexed destAddr, + address indexed l2Sender, + uint256 indexed outboxEntryIndex, + uint256 transactionIndex + ); + + function l2ToL1Sender() external view returns (address); + + function l2ToL1Block() external view returns (uint256); + + function l2ToL1EthBlock() external view returns (uint256); + + function l2ToL1Timestamp() external view returns (uint256); + + function l2ToL1BatchNum() external view returns (uint256); + + function l2ToL1OutputId() external view returns (bytes32); + + function processOutgoingMessages(bytes calldata sendsData, uint256[] calldata sendLengths) + external; + + function outboxEntryExists(uint256 batchNum) external view returns (bool); +} diff --git a/contracts/arbitrum/ITokenGateway.sol b/contracts/arbitrum/ITokenGateway.sol new file mode 100644 index 000000000..c59af47a6 --- /dev/null +++ b/contracts/arbitrum/ITokenGateway.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2020, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +interface ITokenGateway { + /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated + // event OutboundTransferInitiated( + // address token, + // address indexed _from, + // address indexed _to, + // uint256 indexed _transferId, + // uint256 _amount, + // bytes _data + // ); + + /// @notice event deprecated in favor of DepositFinalized and WithdrawalFinalized + // event InboundTransferFinalized( + // address token, + // address indexed _from, + // address indexed _to, + // uint256 indexed _transferId, + // uint256 _amount, + // bytes _data + // ); + + function outboundTransfer( + address _token, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable returns (bytes memory); + + function finalizeInboundTransfer( + address _token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable; + + /** + * @notice Calculate the address used when bridging an ERC20 token + * @dev the L1 and L2 address oracles may not always be in sync. + * For example, a custom token may have been registered but not deploy or the contract self destructed. + * @param l1ERC20 address of L1 token + * @return L2 address of a bridged ERC20 token + */ + function calculateL2TokenAddress(address l1ERC20) external view returns (address); +} diff --git a/contracts/arbitrum/L1ArbitrumMessenger.sol b/contracts/arbitrum/L1ArbitrumMessenger.sol new file mode 100644 index 000000000..b893fa262 --- /dev/null +++ b/contracts/arbitrum/L1ArbitrumMessenger.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2020, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +import "./IInbox.sol"; +import "./IOutbox.sol"; + +/// @notice L1 utility contract to assist with L1 <=> L2 interactions +/// @dev this is an abstract contract instead of library so the functions can be easily overriden when testing +abstract contract L1ArbitrumMessenger { + event TxToL2(address indexed _from, address indexed _to, uint256 indexed _seqNum, bytes _data); + + struct L2GasParams { + uint256 _maxSubmissionCost; + uint256 _maxGas; + uint256 _gasPriceBid; + } + + function sendTxToL2( + address _inbox, + address _to, + address _user, + uint256 _l1CallValue, + uint256 _l2CallValue, + L2GasParams memory _l2GasParams, + bytes memory _data + ) internal virtual returns (uint256) { + // alternative function entry point when struggling with the stack size + return + sendTxToL2( + _inbox, + _to, + _user, + _l1CallValue, + _l2CallValue, + _l2GasParams._maxSubmissionCost, + _l2GasParams._maxGas, + _l2GasParams._gasPriceBid, + _data + ); + } + + function sendTxToL2( + address _inbox, + address _to, + address _user, + uint256 _l1CallValue, + uint256 _l2CallValue, + uint256 _maxSubmissionCost, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes memory _data + ) internal virtual returns (uint256) { + uint256 seqNum = IInbox(_inbox).createRetryableTicket{ value: _l1CallValue }( + _to, + _l2CallValue, + _maxSubmissionCost, + _user, + _user, + _maxGas, + _gasPriceBid, + _data + ); + emit TxToL2(_user, _to, seqNum, _data); + return seqNum; + } + + function getBridge(address _inbox) internal view virtual returns (IBridge) { + return IInbox(_inbox).bridge(); + } + + /// @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies + function getL2ToL1Sender(address _inbox) internal view virtual returns (address) { + IOutbox outbox = IOutbox(getBridge(_inbox).activeOutbox()); + address l2ToL1Sender = outbox.l2ToL1Sender(); + + require(l2ToL1Sender != address(0), "NO_SENDER"); + return l2ToL1Sender; + } +} diff --git a/contracts/arbitrum/L2ArbitrumMessenger.sol b/contracts/arbitrum/L2ArbitrumMessenger.sol new file mode 100644 index 000000000..e03985bef --- /dev/null +++ b/contracts/arbitrum/L2ArbitrumMessenger.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2020, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +import "arbos-precompiles/arbos/builtin/ArbSys.sol"; + +/// @notice L2 utility contract to assist with L1 <=> L2 interactions +/// @dev this is an abstract contract instead of library so the functions can be easily overriden when testing +abstract contract L2ArbitrumMessenger { + address internal constant ARB_SYS_ADDRESS = address(100); + + event TxToL1(address indexed _from, address indexed _to, uint256 indexed _id, bytes _data); + + function sendTxToL1( + uint256 _l1CallValue, + address _from, + address _to, + bytes memory _data + ) internal virtual returns (uint256) { + uint256 _id = ArbSys(ARB_SYS_ADDRESS).sendTxToL1{ value: _l1CallValue }(_to, _data); + emit TxToL1(_from, _to, _id, _data); + return _id; + } +} diff --git a/contracts/arbitrum/README.md b/contracts/arbitrum/README.md new file mode 100644 index 000000000..abc87553e --- /dev/null +++ b/contracts/arbitrum/README.md @@ -0,0 +1,5 @@ +# Arbitrum contracts + +These contracts have been copied from the [Arbitrum repo](https://github.com/OffchainLabs/arbitrum). + +They are also available as part of the npm packages [arb-bridge-eth](https://www.npmjs.com/package/arb-bridge-eth) and [arb-bridge-peripherals](https://www.npmjs.com/package/arb-bridge-peripherals). The reason for copying them rather than installing those packages is the contracts only support Solidity `^0.6.11`, so we had to change the version to `^0.7.6` for it to be compatible with our other contracts. diff --git a/contracts/gateway/BridgeEscrow.sol b/contracts/gateway/BridgeEscrow.sol new file mode 100644 index 000000000..42cea78b8 --- /dev/null +++ b/contracts/gateway/BridgeEscrow.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../upgrades/GraphUpgradeable.sol"; +import "../governance/Managed.sol"; +import "../token/IGraphToken.sol"; + +/** + * @title Bridge Escrow + * @dev This contracts acts as a gateway for an L2 bridge (or several). It simply holds GRT and has + * a set of spenders that can transfer the tokens; the L1 side of each L2 bridge has to be + * approved as a spender. + */ +contract BridgeEscrow is GraphUpgradeable, Managed { + uint256 private constant MAX_UINT256 = 2**256 - 1; + + /** + * @dev Initialize this contract. + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + } + + /** + * @dev Approve a spender (i.e. a bridge that manages the GRT funds held by the escrow) + * @param spender Address of the spender that will be approved + */ + function approveAll(address spender) external onlyGovernor { + graphToken().approve(spender, MAX_UINT256); + } + + /** + * @dev Revoke a spender (i.e. a bridge that will no longer manage the GRT funds held by the escrow) + * @param spender Address of the spender that will be revoked + */ + function revokeAll(address spender) external onlyGovernor { + IGraphToken grt = graphToken(); + grt.decreaseAllowance(spender, grt.allowance(address(this), spender)); + } +} diff --git a/contracts/gateway/GraphTokenGateway.sol b/contracts/gateway/GraphTokenGateway.sol new file mode 100644 index 000000000..6cd33a432 --- /dev/null +++ b/contracts/gateway/GraphTokenGateway.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../upgrades/GraphUpgradeable.sol"; +import "../arbitrum/ITokenGateway.sol"; +import "../governance/Pausable.sol"; +import "../governance/Managed.sol"; + +/** + * @title L1/L2 Graph Token Gateway + * @dev This includes everything that's shared between the L1 and L2 sides of the bridge. + */ +abstract contract GraphTokenGateway is GraphUpgradeable, Pausable, Managed, ITokenGateway { + /** + * @dev Check if the caller is the Controller's governor or this contract's pause guardian. + */ + modifier onlyGovernorOrGuardian() { + require( + msg.sender == controller.getGovernor() || msg.sender == pauseGuardian, + "Only Governor or Guardian can call" + ); + _; + } + + /** + * @notice Change the Pause Guardian for this contract + * @param _newPauseGuardian The address of the new Pause Guardian + */ + function setPauseGuardian(address _newPauseGuardian) external onlyGovernor { + require(_newPauseGuardian != address(0), "PauseGuardian must be set"); + _setPauseGuardian(_newPauseGuardian); + } + + /** + * @dev Override the default pausing from Managed to allow pausing this + * particular contract instead of pausing from the Controller. + */ + function _notPaused() internal view override { + require(!_paused, "Paused (contract)"); + } + + /** + * @notice Change the paused state of the contract + * @param newPaused New value for the pause state (true means the transfers will be paused) + */ + function setPaused(bool newPaused) external onlyGovernorOrGuardian { + _setPaused(newPaused); + } + + /** + * @notice Getter to access paused state of this contract + */ + function paused() external view returns (bool) { + return _paused; + } +} diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol new file mode 100644 index 000000000..a1503eb8d --- /dev/null +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../arbitrum/L1ArbitrumMessenger.sol"; +import "./GraphTokenGateway.sol"; + +/** + * @title L1 Graph Token Gateway Contract + * @dev Provides the L1 side of the Ethereum-Arbitrum GRT bridge. Sends GRT to the L2 chain + * by escrowing them and sending a message to the L2 gateway, and receives tokens from L2 by + * releasing them from escrow. + * Based on Offchain Labs' reference implementation and Livepeer's arbitrum-lpt-bridge + * (See: https://github.com/OffchainLabs/arbitrum/tree/master/packages/arb-bridge-peripherals/contracts/tokenbridge + * and https://github.com/livepeer/arbitrum-lpt-bridge) + */ +contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { + using SafeMath for uint256; + + // Address of the Graph Token contract on L2 + address public l2GRT; + // Address of the Arbitrum Inbox + address public inbox; + // Address of the Arbitrum Gateway Router on L1 + address public l1Router; + // Address of the L2GraphTokenGateway on L2 that is the counterpart of this gateway + address public l2Counterpart; + // Address of the BridgeEscrow contract that holds the GRT in the bridge + address public escrow; + + // Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 + event DepositInitiated( + address _l1Token, + address indexed _from, + address indexed _to, + uint256 indexed _sequenceNumber, + uint256 _amount + ); + + // Emitted when an incoming transfer is finalized, i.e tokens are withdrawn from L2 to L1 + event WithdrawalFinalized( + address _l1Token, + address indexed _from, + address indexed _to, + uint256 indexed _exitNum, + uint256 _amount + ); + + // Emitted when the Arbitrum Inbox and Gateway Router addresses have been updated + event ArbitrumAddressesSet(address _inbox, address _l1Router); + // Emitted when the L2 GRT address has been updated + event L2TokenAddressSet(address _l2GRT); + // Emitted when the counterpart L2GraphTokenGateway address has been updated + event L2CounterpartAddressSet(address _l2Counterpart); + // Emitted when the escrow address has been updated + event EscrowAddressSet(address _escrow); + + /** + * @dev Allows a function to be called only by the gateway's L2 counterpart. + * The message will actually come from the Arbitrum Bridge, but the Outbox + * can tell us who the sender from L2 is. + */ + modifier onlyL2Counterpart() { + // a message coming from the counterpart gateway was executed by the bridge + IBridge bridge = IInbox(inbox).bridge(); + require(msg.sender == address(bridge), "NOT_FROM_BRIDGE"); + + // and the outbox reports that the L2 address of the sender is the counterpart gateway + address l2ToL1Sender = IOutbox(bridge.activeOutbox()).l2ToL1Sender(); + require(l2ToL1Sender == l2Counterpart, "ONLY_COUNTERPART_GATEWAY"); + _; + } + + /** + * @dev Initialize this contract. + * The contract will be paused. + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + _paused = true; + } + + /** + * @dev sets the addresses for L1 contracts provided by Arbitrum + * @param _inbox Address of the Inbox that is part of the Arbitrum Bridge + * @param _l1Router Address of the Gateway Router + */ + function setArbitrumAddresses(address _inbox, address _l1Router) external onlyGovernor { + inbox = _inbox; + l1Router = _l1Router; + emit ArbitrumAddressesSet(_inbox, _l1Router); + } + + /** + * @dev Sets the address of the L2 Graph Token + * @param _l2GRT Address of the GRT contract on L2 + */ + function setL2TokenAddress(address _l2GRT) external onlyGovernor { + l2GRT = _l2GRT; + emit L2TokenAddressSet(_l2GRT); + } + + /** + * @dev Sets the address of the counterpart gateway on L2 + * @param _l2Counterpart Address of the corresponding L2GraphTokenGateway on Arbitrum + */ + function setL2CounterpartAddress(address _l2Counterpart) external onlyGovernor { + l2Counterpart = _l2Counterpart; + emit L2CounterpartAddressSet(_l2Counterpart); + } + + /** + * @dev Sets the address of the escrow contract on L1 + * @param _escrow Address of the BridgeEscrow + */ + function setEscrowAddress(address _escrow) external onlyGovernor { + escrow = _escrow; + emit EscrowAddressSet(_escrow); + } + + /** + * @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox. + * The tokens are escrowed by the gateway until they are withdrawn back to L1. + * The ticket must be redeemed on L2 to receive tokens at the specified address. + * @dev maxGas and gasPriceBid must be set using Arbitrum's NodeInterface.estimateRetryableTicket method. + * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) + * @param _to Recipient address on L2 + * @param _amount Amount of tokens to tranfer + * @param _maxGas Gas limit for L2 execution of the ticket + * @param _gasPriceBid Price per gas on L2 + * @param _data Encoded maxSubmissionCost and sender address along with additional calldata + * @return Sequence number of the retryable ticket created by Inbox + */ + function outboundTransfer( + address _l1Token, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable override notPaused returns (bytes memory) { + IGraphToken token = graphToken(); + require(_l1Token == address(token), "TOKEN_NOT_GRT"); + require(_amount > 0, "INVALID_ZERO_AMOUNT"); + + // nested scopes to avoid stack too deep errors + address from; + uint256 seqNum; + { + uint256 maxSubmissionCost; + bytes memory outboundCalldata; + { + bytes memory extraData; + (from, maxSubmissionCost, extraData) = parseOutboundData(_data); + require(extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); + require(maxSubmissionCost > 0, "NO_SUBMISSION_COST"); + + { + // makes sure only sufficient ETH is supplied required for successful redemption on L2 + // if a user does not desire immediate redemption they should provide + // a msg.value of AT LEAST maxSubmissionCost + uint256 expectedEth = maxSubmissionCost + (_maxGas * _gasPriceBid); + require(msg.value == expectedEth, "WRONG_ETH_VALUE"); + } + outboundCalldata = getOutboundCalldata(_l1Token, from, _to, _amount, extraData); + } + { + L2GasParams memory gasParams = L2GasParams( + maxSubmissionCost, + _maxGas, + _gasPriceBid + ); + // transfer tokens to escrow + token.transferFrom(from, escrow, _amount); + seqNum = sendTxToL2( + inbox, + l2Counterpart, + from, + msg.value, + 0, + gasParams, + outboundCalldata + ); + } + } + emit DepositInitiated(_l1Token, from, _to, seqNum, _amount); + + return abi.encode(seqNum); + } + + /** + * @notice Receives withdrawn tokens from L2 + * The equivalent tokens are released from escrow and sent to the destination. + * @dev can only accept transactions coming from the L2 GRT Gateway + * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) + * @param _from Address of the sender + * @param _to Recepient address on L1 + * @param _amount Amount of tokens transferred + * @param _data Contains exitNum which is always set to 0 + */ + function finalizeInboundTransfer( + address _l1Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable override notPaused onlyL2Counterpart { + IGraphToken token = graphToken(); + require(_l1Token == address(token), "TOKEN_NOT_GRT"); + (uint256 exitNum, ) = abi.decode(_data, (uint256, bytes)); + + uint256 escrowBalance = token.balanceOf(escrow); + // If the bridge doesn't have enough tokens, something's very wrong! + require(_amount <= escrowBalance, "BRIDGE_OUT_OF_FUNDS"); + token.transferFrom(escrow, _to, _amount); + + emit WithdrawalFinalized(_l1Token, _from, _to, exitNum, _amount); + } + + /** + * @notice decodes calldata required for migration of tokens + * @dev data must include maxSubmissionCost, extraData can be left empty. When the router + * sends an outbound message, data also contains the from address. + * @param data encoded callhook data + * @return from sender of the tx + * @return maxSubmissionCost base ether value required to keep retyrable ticket alive + * @return extraData any other data sent to L2 + */ + function parseOutboundData(bytes memory data) + private + view + returns ( + address from, + uint256 maxSubmissionCost, + bytes memory extraData + ) + { + if (msg.sender == l1Router) { + // Data encoded by the Gateway Router includes the sender address + (from, extraData) = abi.decode(data, (address, bytes)); + } else { + from = msg.sender; + extraData = data; + } + // User-encoded data contains the max retryable ticket submission cost + // and additional L2 calldata + (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); + } + + /** + * @notice Creates calldata required to create a retryable ticket + * @dev encodes the target function with its params which + * will be called on L2 when the retryable ticket is redeemed + * @param l1Token Address of the Graph token contract on L1 + * @param from Address on L1 from which we're transferring tokens + * @param to Address on L2 to which we're transferring tokens + * @param amount Amount of GRT to transfer + * @param data Additional call data for the L2 transaction, which must be empty + * @return outboundCalldata Encoded calldata (including function selector) for the L2 transaction + */ + function getOutboundCalldata( + address l1Token, + address from, + address to, + uint256 amount, + bytes memory data + ) public pure returns (bytes memory outboundCalldata) { + bytes memory emptyBytes; + + outboundCalldata = abi.encodeWithSelector( + ITokenGateway.finalizeInboundTransfer.selector, + l1Token, + from, + to, + amount, + abi.encode(emptyBytes, data) + ); + } + + /** + * @notice Calculate the L2 address of a bridged token + * @dev In our case, this would only work for GRT. + * @param l1ERC20 address of L1 GRT contract + * @return L2 address of the bridged GRT token + */ + function calculateL2TokenAddress(address l1ERC20) external view override returns (address) { + IGraphToken token = graphToken(); + if (l1ERC20 != address(token)) { + return address(0); + } + return l2GRT; + } +} diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index 716403111..561d5244d 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -44,7 +44,7 @@ contract Managed { require(!controller.partialPaused(), "Partial-paused"); } - function _notPaused() internal view { + function _notPaused() internal view virtual { require(!controller.paused(), "Paused"); } @@ -182,5 +182,6 @@ contract Managed { _syncContract("RewardsManager"); _syncContract("Staking"); _syncContract("GraphToken"); + _syncContract("GraphTokenGateway"); } } diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol new file mode 100644 index 000000000..94d87f279 --- /dev/null +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../arbitrum/L2ArbitrumMessenger.sol"; +import "../../gateway/GraphTokenGateway.sol"; +import "../token/L2GraphToken.sol"; + +/** + * @title L2 Graph Token Gateway Contract + * @dev Provides the L2 side of the Ethereum-Arbitrum GRT bridge. Receives GRT from the L1 chain + * and mints them on the L2 side. Sending GRT back to L1 by burning them on the L2 side. + * Based on Offchain Labs' reference implementation and Livepeer's arbitrum-lpt-bridge + * (See: https://github.com/OffchainLabs/arbitrum/tree/master/packages/arb-bridge-peripherals/contracts/tokenbridge + * and https://github.com/livepeer/arbitrum-lpt-bridge) + */ +contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { + using SafeMath for uint256; + + // Offset applied by the bridge to L1 addresses sending messages to L2 + uint160 internal constant L2_ADDRESS_OFFSET = + uint160(0x1111000000000000000000000000000000001111); + + // Address of the Graph Token contract on L1 + address public l1GRT; + // Address of the L1GraphTokenGateway that is the counterpart of this gateway on L1 + address public l1Counterpart; + // Address of the Arbitrum Gateway Router on L2 + address public l2Router; + + // Calldata included in an outbound transfer, stored as a structure for convenience and stack depth + struct OutboundCalldata { + address from; + bytes extraData; + } + + // Emitted when an incoming transfer is finalized, i.e. tokens were deposited from L1 to L2 + event DepositFinalized( + address indexed l1Token, + address indexed _from, + address indexed _to, + uint256 _amount + ); + + // Emitted when an outbound transfer is initiated, i.e. tokens are being withdrawn from L2 back to L1 + event WithdrawalInitiated( + address l1Token, + address indexed _from, + address indexed _to, + uint256 indexed _l2ToL1Id, + uint256 _exitNum, + uint256 _amount + ); + + // Emitted when the Arbitrum Gateway Router address on L2 has been updated + event L2RouterSet(address _l2Router); + // Emitted when the L1 Graph Token address has been updated + event L1TokenAddressSet(address _l1GRT); + // Emitted when the address of the counterpart gateway on L1 has been updated + event L1CounterpartAddressSet(address _l1Counterpart); + + /** + * @dev Checks that the sender is the L2 alias of the counterpart + * gateway on L1. + */ + modifier onlyL1Counterpart() { + require(msg.sender == l1ToL2Alias(l1Counterpart), "ONLY_COUNTERPART_GATEWAY"); + _; + } + + /** + * @dev Initialize this contract. + * The contract will be paused. + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + _paused = true; + } + + /** + * @dev Sets the address of the Arbitrum Gateway Router on L2 + * @param _l2Router Address of the L2 Router (provided by Arbitrum) + */ + function setL2Router(address _l2Router) external onlyGovernor { + l2Router = _l2Router; + emit L2RouterSet(_l2Router); + } + + /** + * @dev Sets the address of the Graph Token on L1 + * @param _l1GRT L1 address of the Graph Token contract + */ + function setL1TokenAddress(address _l1GRT) external onlyGovernor { + l1GRT = _l1GRT; + emit L1TokenAddressSet(_l1GRT); + } + + /** + * @dev Sets the address of the counterpart gateway on L1 + * @param _l1Counterpart Address of the L1GraphTokenGateway on L1 + */ + function setL1CounterpartAddress(address _l1Counterpart) external onlyGovernor { + l1Counterpart = _l1Counterpart; + emit L1CounterpartAddressSet(_l1Counterpart); + } + + /** + * @notice Burns L2 tokens and initiates a transfer to L1. + * The tokens will be available on L1 only after the wait period (7 days) is over, + * and will require an Outbox.executeTransaction to finalize. + * @dev no additional callhook data is allowed. The two unused params are needed + * for compatibility with Arbitrum's gateway router. + * The function is payable for ITokenGateway compatibility, but msg.value must be zero. + * @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) + * @param _to Recipient address on L1 + * @param _amount Amount of tokens to burn + * @param _data Contains sender and additional data (always zero) to send to L1 + * @return ID of the withdraw transaction + */ + function outboundTransfer( + address _l1Token, + address _to, + uint256 _amount, + uint256, // unused on L2 + uint256, // unused on L2 + bytes calldata _data + ) public payable override notPaused returns (bytes memory) { + require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); + require(_amount > 0, "INVALID_ZERO_AMOUNT"); + require(msg.value == 0, "INVALID_NONZERO_VALUE"); + + OutboundCalldata memory s; + + (s.from, s.extraData) = parseOutboundData(_data); + require(s.extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); + + // from needs to approve this contract to burn the amount first + L2GraphToken(this.calculateL2TokenAddress(l1GRT)).bridgeBurn(s.from, _amount); + + uint256 id = sendTxToL1( + 0, + s.from, + l1Counterpart, + getOutboundCalldata(_l1Token, s.from, _to, _amount, s.extraData) + ); + + // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 + emit WithdrawalInitiated(_l1Token, s.from, _to, id, 0, _amount); + + return abi.encode(id); + } + + /** + * @notice Burns L2 tokens and initiates a transfer to L1. + * The tokens will be received on L1 only after the wait period (7 days) is over, + * and will require an Outbox.executeTransaction to finalize. + * @dev no additional callhook data is allowed + * @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) + * @param _to Recipient address on L1 + * @param _amount Amount of tokens to burn + * @param _data Contains sender and additional data to send to L1 + * @return ID of the withdraw tx + */ + function outboundTransfer( + address _l1Token, + address _to, + uint256 _amount, + bytes calldata _data + ) external returns (bytes memory) { + return outboundTransfer(_l1Token, _to, _amount, uint256(0), uint256(0), _data); + } + + /** + * @notice Calculate the L2 address of a bridged token + * @dev In our case, this would only work for GRT. + * @param l1ERC20 address of L1 GRT contract + * @return L2 address of the bridged GRT token + */ + function calculateL2TokenAddress(address l1ERC20) public view override returns (address) { + if (l1ERC20 != l1GRT) { + return address(0); + } + return Managed._resolveContract(keccak256("GraphToken")); + } + + /** + * @notice Receives token amount from L1 and mints the equivalent tokens to the receiving address + * @dev Only accepts transactions from the L1 GRT Gateway + * data param is unused because no additional data is allowed from L1. + * The function is payable for ITokenGateway compatibility, but msg.value must be zero. + * @param _l1Token L1 Address of GRT + * @param _from Address of the sender on L1 + * @param _to Recipient address on L2 + * @param _amount Amount of tokens transferred + */ + function finalizeInboundTransfer( + address _l1Token, + address _from, + address _to, + uint256 _amount, + bytes calldata // _data unused in L2 + ) external payable override notPaused onlyL1Counterpart { + require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); + require(msg.value == 0, "INVALID_NONZERO_VALUE"); + + L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount); + + emit DepositFinalized(_l1Token, _from, _to, _amount); + } + + /** + * @notice Creates calldata required to send tx to L1 + * @dev encodes the target function with its params which + * will be called on L1 when the message is received on L1 + * @param token Address of the token on L1 + * @param from Address of the token sender on L2 + * @param to Address to which we're sending tokens on L1 + * @param amount Amount of GRT to transfer + * @param data Additional calldata for the transaction + */ + function getOutboundCalldata( + address token, + address from, + address to, + uint256 amount, + bytes memory data + ) public pure returns (bytes memory outboundCalldata) { + outboundCalldata = abi.encodeWithSelector( + ITokenGateway.finalizeInboundTransfer.selector, + token, + from, + to, + amount, + abi.encode(0, data) // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 + ); + } + + /** + * @notice Decodes calldata required for migration of tokens + * @dev extraData can be left empty + * @param data Encoded callhook data + * @return from Sender of the tx + * @return extraData Any other data sent to L1 + */ + function parseOutboundData(bytes memory data) + private + view + returns (address from, bytes memory extraData) + { + if (msg.sender == l2Router) { + (from, extraData) = abi.decode(data, (address, bytes)); + } else { + from = msg.sender; + extraData = data; + } + } + + /** + * @notice Converts L1 address to its L2 alias used when sending messages + * @dev The Arbitrum bridge adds an offset to addresses when sending messages, + * so we need to apply it to check any L1 address from a message in L2 + * @param _l1Address The L1 address + * @return _l2Address the L2 alias of _l1Address + */ + function l1ToL2Alias(address _l1Address) internal pure returns (address _l2Address) { + _l2Address = address(uint160(_l1Address) + L2_ADDRESS_OFFSET); + } +} diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol new file mode 100644 index 000000000..2307385c5 --- /dev/null +++ b/contracts/l2/token/L2GraphToken.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../upgrades/GraphUpgradeable.sol"; +import "../../token/GraphToken.sol"; +import "../../arbitrum/IArbToken.sol"; +import "../../governance/Governed.sol"; + +/** + * @title GraphTokenUpgradeable contract + * @dev This is the implementation of the ERC20 Graph Token. + * The implementation exposes a Permit() function to allow for a spender to send a signed message + * and approve funds to a spender following EIP2612 to make integration with other contracts easier. + * + * The token is initially owned by the deployer address that can mint tokens to create the initial + * distribution. For convenience, an initial supply can be passed in the constructor that will be + * assigned to the deployer. + * + * The governor can add contracts allowed to mint indexing rewards. + * + * Note this is an exact copy of the original GraphToken contract, but using + * initializer functions and upgradeable OpenZeppelin contracts instead of + * the original's constructor + non-upgradeable approach. + */ +contract GraphTokenUpgradeable is + GraphUpgradeable, + Governed, + ERC20Upgradeable, + ERC20BurnableUpgradeable +{ + using SafeMath for uint256; + + // -- EIP712 -- + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-domainseparator + + bytes32 private constant DOMAIN_TYPE_HASH = + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ); + bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Token"); + bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); + bytes32 private constant DOMAIN_SALT = + 0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591; // Randomly generated salt + bytes32 private constant PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + // -- State -- + + // solhint-disable-next-line var-name-mixedcase + bytes32 private DOMAIN_SEPARATOR; + mapping(address => bool) private _minters; + mapping(address => uint256) public nonces; + + // -- Events -- + + event MinterAdded(address indexed account); + event MinterRemoved(address indexed account); + + modifier onlyMinter() { + require(isMinter(msg.sender), "Only minter can call"); + _; + } + + /** + * @dev Graph Token Contract initializer. + * @param _initialSupply Initial supply of GRT + */ + function _initialize(address owner, uint256 _initialSupply) internal { + __ERC20_init("Graph Token", "GRT"); + Governed._initialize(owner); + + // The Governor has the initial supply of tokens + _mint(owner, _initialSupply); + + // The Governor is the default minter + _addMinter(owner); + + // EIP-712 domain separator + DOMAIN_SEPARATOR = keccak256( + abi.encode( + DOMAIN_TYPE_HASH, + DOMAIN_NAME_HASH, + DOMAIN_VERSION_HASH, + _getChainID(), + address(this), + DOMAIN_SALT + ) + ); + } + + /** + * @dev Approve token allowance by validating a message signed by the holder. + * @param _owner Address of the token holder + * @param _spender Address of the approved spender + * @param _value Amount of tokens to approve the spender + * @param _deadline Expiration time of the signed permit + * @param _v Signature version + * @param _r Signature r value + * @param _s Signature s value + */ + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonces[_owner], _deadline) + ) + ) + ); + nonces[_owner] = nonces[_owner].add(1); + + address recoveredAddress = ECDSA.recover(digest, abi.encodePacked(_r, _s, _v)); + require(_owner == recoveredAddress, "GRT: invalid permit"); + require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); + + _approve(_owner, _spender, _value); + } + + /** + * @dev Add a new minter. + * @param _account Address of the minter + */ + function addMinter(address _account) external onlyGovernor { + _addMinter(_account); + } + + /** + * @dev Remove a minter. + * @param _account Address of the minter + */ + function removeMinter(address _account) external onlyGovernor { + _removeMinter(_account); + } + + /** + * @dev Renounce to be a minter. + */ + function renounceMinter() external { + _removeMinter(msg.sender); + } + + /** + * @dev Mint new tokens. + * @param _to Address to send the newly minted tokens + * @param _amount Amount of tokens to mint + */ + function mint(address _to, uint256 _amount) external onlyMinter { + _mint(_to, _amount); + } + + /** + * @dev Return if the `_account` is a minter or not. + * @param _account Address to check + * @return True if the `_account` is minter + */ + function isMinter(address _account) public view returns (bool) { + return _minters[_account]; + } + + /** + * @dev Add a new minter. + * @param _account Address of the minter + */ + function _addMinter(address _account) private { + _minters[_account] = true; + emit MinterAdded(_account); + } + + /** + * @dev Remove a minter. + * @param _account Address of the minter + */ + function _removeMinter(address _account) private { + _minters[_account] = false; + emit MinterRemoved(_account); + } + + /** + * @dev Get the running network chain ID. + * @return The chain ID + */ + function _getChainID() private pure returns (uint256) { + uint256 id; + // solhint-disable-next-line no-inline-assembly + assembly { + id := chainid() + } + return id; + } +} + +/** + * @title L2 Graph Token Contract + * @dev Provides the L2 version of the GRT token, meant to be minted/burned + * through the L2GraphTokenGateway. + */ +contract L2GraphToken is GraphTokenUpgradeable, IArbToken { + using SafeMath for uint256; + + // Address of the gateway (on L2) that is allowed to mint tokens + address public gateway; + // Address of the corresponding Graph Token contract on L1 + address public override l1Address; + + // Emitted when the bridge / gateway has minted new tokens, i.e. tokens were transferred to L2 + event BridgeMinted(address indexed account, uint256 amount); + // Emitted when the bridge / gateway has burned tokens, i.e. tokens were transferred back to L1 + event BridgeBurned(address indexed account, uint256 amount); + // Emitted when the address of the gateway has been updated + event GatewaySet(address gateway); + // Emitted when the address of the Graph Token contract on L1 has been updated + event L1AddressSet(address l1Address); + + /** + * @dev Checks that the sender is the L2 gateway from the L1/L2 token bridge + */ + modifier onlyGateway() { + require(msg.sender == gateway, "NOT_GATEWAY"); + _; + } + + /** + * @dev L2 Graph Token Contract initializer. + * @param owner Governance address that owns this contract + * @param _initialSupply Initial supply of GRT + */ + function initialize(address owner, uint256 _initialSupply) external onlyImpl { + require(owner != address(0), "Owner must be set"); + GraphTokenUpgradeable._initialize(owner, _initialSupply); + } + + /** + * @dev Sets the address of the L2 gateway allowed to mint tokens + */ + function setGateway(address gw) external onlyGovernor { + gateway = gw; + emit GatewaySet(gateway); + } + + /** + * @dev Sets the address of the counterpart token on L1 + */ + function setL1Address(address addr) external onlyGovernor { + l1Address = addr; + emit L1AddressSet(addr); + } + + /** + * @dev Increases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L2) + * @param account Address to credit with the new tokens + * @param amount Number of tokens to mint + */ + function bridgeMint(address account, uint256 amount) external override onlyGateway { + _mint(account, amount); + emit BridgeMinted(account, amount); + } + + /** + * @dev Decreases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L1). + * @param account Address from which to extract the tokens + * @param amount Number of tokens to burn + */ + function bridgeBurn(address account, uint256 amount) external override onlyGateway { + burnFrom(account, amount); + emit BridgeBurned(account, amount); + } +} diff --git a/contracts/tests/arbitrum/BridgeMock.sol b/contracts/tests/arbitrum/BridgeMock.sol new file mode 100644 index 000000000..7f18084d6 --- /dev/null +++ b/contracts/tests/arbitrum/BridgeMock.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../arbitrum/IBridge.sol"; + +/** + * @title Arbitrum Bridge mock contract + * @dev This contract implements Arbitrum's IBridge interface for testing purposes + */ +contract BridgeMock is IBridge { + // Address of the (mock) Arbitrum Inbox + address public inbox; + // Address of the (mock) Arbitrum Outbox + address public outbox; + // Index of the next message on the inbox messages array + uint256 public messageIndex; + // Inbox messages array + bytes32[] public override inboxAccs; + + /** + * @dev Deliver a message to the inbox. The encoded message will be + * added to the inbox array, and messageIndex will be incremented. + * @param kind Type of the message + * @param sender Address that is sending the message + * @param messageDataHash keccak256 hash of the message data + * @return The next index for the inbox array + */ + function deliverMessageToInbox( + uint8 kind, + address sender, + bytes32 messageDataHash + ) external payable override returns (uint256) { + messageIndex = messageIndex + 1; + inboxAccs.push(keccak256(abi.encodePacked(inbox, kind, sender, messageDataHash))); + emit MessageDelivered( + messageIndex, + inboxAccs[messageIndex - 1], + msg.sender, + kind, + sender, + messageDataHash + ); + return messageIndex; + } + + /** + * @dev Executes an L1 function call incoing from L2. This can only be called + * by the Outbox. + * @param destAddr Contract to call + * @param amount ETH value to send + * @param data Calldata for the function call + */ + function executeCall( + address destAddr, + uint256 amount, + bytes calldata data + ) external override returns (bool success, bytes memory returnData) { + require(outbox == msg.sender, "NOT_FROM_OUTBOX"); + + // solhint-disable-next-line avoid-low-level-calls + (success, returnData) = destAddr.call{ value: amount }(data); + emit BridgeCallTriggered(msg.sender, destAddr, amount, data); + } + + /** + * @dev Set the address of the inbox. Anyone can call this, because it's a mock. + * @param _inbox Address of the inbox + * @param enabled Enable the inbox (ignored) + */ + function setInbox(address _inbox, bool enabled) external override { + inbox = _inbox; + emit InboxToggle(inbox, enabled); + } + + /** + * @dev Set the address of the outbox. Anyone can call this, because it's a mock. + * @param _outbox Address of the outbox + * @param enabled Enable the outbox (ignored) + */ + function setOutbox(address _outbox, bool enabled) external override { + outbox = _outbox; + emit OutboxToggle(outbox, enabled); + } + + // View functions + + /** + * @dev Getter for the active outbox (in this case there's only one) + */ + function activeOutbox() external view override returns (address) { + return outbox; + } + + /** + * @dev Getter for whether an address is an allowed inbox (in this case there's only one) + * @param _inbox Address to check + * @return True if the address is the allowed inbox, false otherwise + */ + function allowedInboxes(address _inbox) external view override returns (bool) { + return _inbox == inbox; + } + + /** + * @dev Getter for whether an address is an allowed outbox (in this case there's only one) + * @param _outbox Address to check + * @return True if the address is the allowed outbox, false otherwise + */ + function allowedOutboxes(address _outbox) external view override returns (bool) { + return _outbox == outbox; + } + + /** + * @dev Getter for the count of messages in the inboxAccs + * @return Number of messages in inboxAccs + */ + function messageCount() external view override returns (uint256) { + return inboxAccs.length; + } +} diff --git a/contracts/tests/arbitrum/InboxMock.sol b/contracts/tests/arbitrum/InboxMock.sol new file mode 100644 index 000000000..940e33bfd --- /dev/null +++ b/contracts/tests/arbitrum/InboxMock.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../arbitrum/IInbox.sol"; + +/** + * @title Arbitrum Inbox mock contract + * @dev This contract implements (a subset of) Arbitrum's IInbox interface for testing purposes + */ +contract InboxMock is IInbox { + // Offset used when calculating the L2 alias of an L1 address + uint160 internal constant OFFSET = uint160(0x1111000000000000000000000000000000001111); + // Type indicator for a standard L2 message + uint8 internal constant L2_MSG = 3; + // Type indicator for a retryable ticket message + // solhint-disable-next-line const-name-snakecase + uint8 internal constant L1MessageType_submitRetryableTx = 9; + // Address of the Bridge (mock) contract + IBridge public override bridge; + + /** + * @dev Send a message to L2 (by delivering it to the Bridge) + * @param messageData Encoded data to send in the message + * @return message number returned by the inbox + */ + function sendL2Message(bytes calldata messageData) external override returns (uint256) { + uint256 msgNum = deliverToBridge(L2_MSG, msg.sender, keccak256(messageData)); + emit InboxMessageDelivered(msgNum, messageData); + return msgNum; + } + + /** + * @dev Set the address of the (mock) bridge + * @param _bridge Address of the bridge + */ + function setBridge(address _bridge) external { + bridge = IBridge(_bridge); + } + + /** + * @dev Unimplemented in this mock + */ + function sendUnsignedTransaction( + uint256, + uint256, + uint256, + address, + uint256, + bytes calldata + ) external pure override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function sendContractTransaction( + uint256, + uint256, + address, + uint256, + bytes calldata + ) external pure override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function sendL1FundedUnsignedTransaction( + uint256, + uint256, + uint256, + address, + bytes calldata + ) external payable override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function sendL1FundedContractTransaction( + uint256, + uint256, + address, + bytes calldata + ) external payable override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Utility function that converts the address in the L1 that submitted a tx to + * the inbox to the msg.sender viewed in the L2 + * @param l1Address the address in the L1 that triggered the tx to L2 + * @return l2Address L2 address as viewed in msg.sender + */ + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + l2Address = address(uint160(l1Address) + OFFSET); + } + + /** + * @dev Creates a retryable ticket for an L2 transaction + * @param destAddr Address of the contract to call in L2 + * @param arbTxCallValue Callvalue to use in the L2 transaction + * @param maxSubmissionCost Max cost of submitting the ticket, in Wei + * @param submissionRefundAddress L2 address to refund for any remaining value from the submission cost + * @param valueRefundAddress L2 address to refund if the ticket times out or gets cancelled + * @param maxGas Max gas for the L2 transcation + * @param gasPriceBid Gas price bid on L2 + * @param data Encoded calldata for the L2 transaction (including function selector) + * @return message number returned by the bridge + */ + function createRetryableTicket( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external payable override returns (uint256) { + submissionRefundAddress = applyL1ToL2Alias(submissionRefundAddress); + valueRefundAddress = applyL1ToL2Alias(valueRefundAddress); + return + _deliverMessage( + L1MessageType_submitRetryableTx, + msg.sender, + abi.encodePacked( + uint256(uint160(bytes20(destAddr))), + arbTxCallValue, + msg.value, + maxSubmissionCost, + uint256(uint160(bytes20(submissionRefundAddress))), + uint256(uint160(bytes20(valueRefundAddress))), + maxGas, + gasPriceBid, + data.length, + data + ) + ); + } + + function depositEth(uint256) external payable override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function pauseCreateRetryables() external pure override { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function unpauseCreateRetryables() external pure override { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function startRewriteAddress() external pure override { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function stopRewriteAddress() external pure override { + revert("Unimplemented"); + } + + /** + * @dev Deliver a message to the bridge + * @param _kind Type of the message + * @param _sender Address that is sending the message + * @param _messageData Encoded message data + * @return Message number returned by the bridge + */ + function _deliverMessage( + uint8 _kind, + address _sender, + bytes memory _messageData + ) internal returns (uint256) { + uint256 msgNum = deliverToBridge(_kind, _sender, keccak256(_messageData)); + emit InboxMessageDelivered(msgNum, _messageData); + return msgNum; + } + + /** + * @dev Deliver a message to the bridge + * @param kind Type of the message + * @param sender Address that is sending the message + * @param messageDataHash keccak256 hash of the encoded message data + * @return Message number returned by the bridge + */ + function deliverToBridge( + uint8 kind, + address sender, + bytes32 messageDataHash + ) internal returns (uint256) { + return bridge.deliverMessageToInbox{ value: msg.value }(kind, sender, messageDataHash); + } +} diff --git a/contracts/tests/arbitrum/OutboxMock.sol b/contracts/tests/arbitrum/OutboxMock.sol new file mode 100644 index 000000000..af16bdfdb --- /dev/null +++ b/contracts/tests/arbitrum/OutboxMock.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../arbitrum/IOutbox.sol"; +import "../../arbitrum/IBridge.sol"; + +/** + * @title Arbitrum Outbox mock contract + * @dev This contract implements (a subset of) Arbitrum's IOutbox interface for testing purposes + */ +contract OutboxMock is IOutbox { + // Context of an L2-to-L1 function call + struct L2ToL1Context { + uint128 l2Block; + uint128 l1Block; + uint128 timestamp; + uint128 batchNum; + bytes32 outputId; + address sender; + } + // Context of the current L2-to-L1 function call (set and cleared in each transaction) + L2ToL1Context internal context; + + // Address of the (mock) Arbitrum Bridge + IBridge public bridge; + + /** + * @dev Set the address of the (mock) bridge + * @param _bridge Address of the bridge + */ + function setBridge(address _bridge) external { + bridge = IBridge(_bridge); + } + + /** + * @dev Getter for the L2 sender of the current incoming message + */ + function l2ToL1Sender() external view override returns (address) { + return context.sender; + } + + /** + * @dev Getter for the L2 block of the current incoming message + */ + function l2ToL1Block() external view override returns (uint256) { + return context.l2Block; + } + + /** + * @dev Getter for the L1 block of the current incoming message + */ + function l2ToL1EthBlock() external view override returns (uint256) { + return context.l1Block; + } + + /** + * @dev Getter for the L1 timestamp of the current incoming message + */ + function l2ToL1Timestamp() external view override returns (uint256) { + return context.timestamp; + } + + /** + * @dev Getter for the L2 batch number of the current incoming message + */ + function l2ToL1BatchNum() external view override returns (uint256) { + return context.batchNum; + } + + /** + * @dev Getter for the output ID of the current incoming message + */ + function l2ToL1OutputId() external view override returns (bytes32) { + return context.outputId; + } + + /** + * @dev Unimplemented in this mock + */ + function processOutgoingMessages(bytes calldata, uint256[] calldata) external pure override { + revert("Unimplemented"); + } + + /** + * @dev Check whether an outbox entry for a message exists. + * This mock returns always true. + */ + function outboxEntryExists(uint256) external pure override returns (bool) { + return true; + } + + /** + * @notice (Mock) Executes a messages in an Outbox entry. + * @dev This mocks what has to be called when finalizing an L2 to L1 transfer. + * In our mock scenario, we don't validate and execute unconditionally. + * @param batchNum Index of OutboxEntry in outboxEntries array + * @param l2Sender sender of original message (i.e., caller of ArbSys.sendTxToL1) + * @param destAddr destination address for L1 contract call + * @param l2Block l2 block number at which sendTxToL1 call was made + * @param l1Block l1 block number at which sendTxToL1 call was made + * @param l2Timestamp l2 Timestamp at which sendTxToL1 call was made + * @param amount value in L1 message in wei + * @param calldataForL1 abi-encoded L1 message data + */ + function executeTransaction( + uint256 batchNum, + bytes32[] calldata, // proof + uint256, // index + address l2Sender, + address destAddr, + uint256 l2Block, + uint256 l1Block, + uint256 l2Timestamp, + uint256 amount, + bytes calldata calldataForL1 + ) external virtual { + bytes32 outputId; + + context = L2ToL1Context({ + sender: l2Sender, + l2Block: uint128(l2Block), + l1Block: uint128(l1Block), + timestamp: uint128(l2Timestamp), + batchNum: uint128(batchNum), + outputId: outputId + }); + + // set and reset vars around execution so they remain valid during call + executeBridgeCall(destAddr, amount, calldataForL1); + } + + /** + * @dev Execute an L2-to-L1 function call by calling the bridge + * @param destAddr Address of the contract to call + * @param amount Callvalue for the function call + * @param data Calldata for the function call + */ + function executeBridgeCall( + address destAddr, + uint256 amount, + bytes memory data + ) internal { + (bool success, bytes memory returndata) = bridge.executeCall(destAddr, amount, data); + if (!success) { + if (returndata.length > 0) { + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert("BRIDGE_CALL_FAILED"); + } + } + } +} diff --git a/contracts/token/IGraphToken.sol b/contracts/token/IGraphToken.sol index 2b7dbaa20..41ca4838b 100644 --- a/contracts/token/IGraphToken.sol +++ b/contracts/token/IGraphToken.sol @@ -32,4 +32,10 @@ interface IGraphToken is IERC20 { bytes32 _r, bytes32 _s ) external; + + // -- Allowance -- + + function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); } diff --git a/hardhat.config.ts b/hardhat.config.ts index 2c4a08209..80475e136 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -59,6 +59,8 @@ const networkConfigs: NetworkConfig[] = [ { network: 'mainnet', chainId: 1 }, { network: 'rinkeby', chainId: 4 }, { network: 'kovan', chainId: 42 }, + { network: 'arbitrum-rinkeby', chainId: 421611, url: 'https://rinkeby.arbitrum.io/rpc' }, + { network: 'arbitrum-one', chainId: 42161, url: 'https://arb1.arbitrum.io/rpc' }, ] function getAccountMnemonic() { diff --git a/package.json b/package.json index 5510d8ae9..da7e6acb7 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "devDependencies": { "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", + "@defi-wonderland/smock": "^2.0.7", "@ethersproject/experimental": "^5.4.0", "@graphprotocol/common-ts": "^1.6.0", "@nomiclabs/hardhat-ethers": "^2.0.2", @@ -39,6 +40,7 @@ "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^4.0.0", "@urql/core": "^2.1.3", + "arbos-precompiles": "^1.0.2", "bignumber.js": "^9.0.0", "chai": "^4.3.4", "cli-table": "^0.3.6", diff --git a/test/epochs.test.ts b/test/epochs.test.ts index 7ac178c33..1c4e17ee8 100644 --- a/test/epochs.test.ts +++ b/test/epochs.test.ts @@ -12,6 +12,7 @@ import { getAccounts, toBN, Account, + initNetwork, } from './lib/testHelpers' describe('EpochManager', () => { @@ -23,6 +24,7 @@ describe('EpochManager', () => { const epochLength: BigNumber = toBN('3') before(async function () { + await initNetwork() ;[me, governor] = await getAccounts() }) diff --git a/test/gateway/bridgeEscrow.test.ts b/test/gateway/bridgeEscrow.test.ts new file mode 100644 index 000000000..aea195159 --- /dev/null +++ b/test/gateway/bridgeEscrow.test.ts @@ -0,0 +1,77 @@ +import { expect } from 'chai' +import { BigNumber } from 'ethers' + +import { GraphToken } from '../../build/types/GraphToken' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' + +import { NetworkFixture } from '../lib/fixtures' + +import { getAccounts, toGRT, Account } from '../lib/testHelpers' + +describe('BridgeEscrow', () => { + let governor: Account + let tokenReceiver: Account + let spender: Account + + let fixture: NetworkFixture + + let grt: GraphToken + let bridgeEscrow: BridgeEscrow + + const nTokens = toGRT('1000') + + before(async function () { + ;[governor, tokenReceiver, spender] = await getAccounts() + + fixture = new NetworkFixture() + ;({ grt, bridgeEscrow } = await fixture.load(governor.signer)) + + // Give some funds to the Escrow + await grt.connect(governor.signer).mint(bridgeEscrow.address, nTokens) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('approveAll', function () { + it('cannot be called by someone other than the governor', async function () { + const tx = bridgeEscrow.connect(tokenReceiver.signer).approveAll(spender.address) + expect(tx).to.be.revertedWith('Caller must be Controller governor') + }) + it('allows a spender to transfer GRT held by the contract', async function () { + expect(await grt.allowance(bridgeEscrow.address, spender.address)).eq(0) + const tx = grt + .connect(spender.signer) + .transferFrom(bridgeEscrow.address, tokenReceiver.address, nTokens) + expect(tx).to.be.revertedWith('ERC20: transfer amount exceeds allowance') + await bridgeEscrow.connect(governor.signer).approveAll(spender.address) + expect( + await grt + .connect(spender.signer) + .transferFrom(bridgeEscrow.address, tokenReceiver.address, nTokens), + ).to.emit(grt, 'Transfer') + expect(await grt.balanceOf(tokenReceiver.address)).to.eq(nTokens) + }) + }) + + describe('revokeAll', function () { + it('cannot be called by someone other than the governor', async function () { + const tx = bridgeEscrow.connect(tokenReceiver.signer).revokeAll(spender.address) + expect(tx).to.be.revertedWith('Caller must be Controller governor') + }) + it("revokes a spender's permission to transfer GRT held by the contract", async function () { + await bridgeEscrow.connect(governor.signer).approveAll(spender.address) + await bridgeEscrow.connect(governor.signer).revokeAll(spender.address) + // We shouldn't be able to transfer _anything_ + const tx = grt + .connect(spender.signer) + .transferFrom(bridgeEscrow.address, tokenReceiver.address, BigNumber.from('1')) + expect(tx).to.be.revertedWith('ERC20: transfer amount exceeds allowance') + }) + }) +}) diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts new file mode 100644 index 000000000..61ee8ce02 --- /dev/null +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -0,0 +1,580 @@ +import { expect } from 'chai' +import { constants, Signer, utils } from 'ethers' + +import { GraphToken } from '../../build/types/GraphToken' +import { BridgeMock } from '../../build/types/BridgeMock' +import { InboxMock } from '../../build/types/InboxMock' +import { OutboxMock } from '../../build/types/OutboxMock' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' + +import { NetworkFixture } from '../lib/fixtures' +import { deployContract } from '../lib/deployment' + +import { + getAccounts, + latestBlock, + toBN, + toGRT, + Account, + applyL1ToL2Alias, +} from '../lib/testHelpers' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' + +const { AddressZero } = constants + +describe('L1GraphTokenGateway', () => { + let governor: Account + let tokenSender: Account + let l2Receiver: Account + let mockRouter: Account + let mockL2GRT: Account + let mockL2Gateway: Account + let pauseGuardian: Account + let fixture: NetworkFixture + + let grt: GraphToken + let l1GraphTokenGateway: L1GraphTokenGateway + let bridgeEscrow: BridgeEscrow + let bridgeMock: BridgeMock + let inboxMock: InboxMock + let outboxMock: OutboxMock + + const senderTokens = toGRT('1000') + const maxGas = toBN('1000000') + const maxSubmissionCost = toBN('7') + const gasPriceBid = toBN('2') + const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) + const emptyCallHookData = '0x' + const defaultData = utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [maxSubmissionCost, emptyCallHookData], + ) + const notEmptyCallHookData = '0x12' + const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [maxSubmissionCost, notEmptyCallHookData], + ) + + before(async function () { + ;[governor, tokenSender, l2Receiver, mockRouter, mockL2GRT, mockL2Gateway, pauseGuardian] = + await getAccounts() + + fixture = new NetworkFixture() + ;({ grt, l1GraphTokenGateway, bridgeEscrow } = await fixture.load(governor.signer)) + + // Give some funds to the token sender + await grt.connect(governor.signer).mint(tokenSender.address, senderTokens) + // Deploy contracts that mock Arbitrum's bridge contracts + bridgeMock = (await deployContract('BridgeMock', governor.signer)) as unknown as BridgeMock + inboxMock = (await deployContract('InboxMock', governor.signer)) as unknown as InboxMock + outboxMock = (await deployContract('OutboxMock', governor.signer)) as unknown as OutboxMock + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + context('> immediately after deploy', function () { + describe('calculateL2TokenAddress', function () { + it('should return address zero as it was not set', async function () { + expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(AddressZero) + }) + }) + + describe('outboundTransfer', function () { + it('reverts because it is paused', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + grt.address, + l2Receiver.address, + toGRT('10'), + maxGas, + gasPriceBid, + defaultData, + { + value: defaultEthValue, + }, + ) + await expect(tx).revertedWith('Paused (contract)') + }) + }) + + describe('finalizeInboundTransfer', function () { + it('revert because it is paused', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .finalizeInboundTransfer( + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('Paused (contract)') + }) + }) + + describe('setArbitrumAddresses', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setArbitrumAddresses(inboxMock.address, mockRouter.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets inbox and router address', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .setArbitrumAddresses(inboxMock.address, mockRouter.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'ArbitrumAddressesSet') + .withArgs(inboxMock.address, mockRouter.address) + expect(await l1GraphTokenGateway.l1Router()).eq(mockRouter.address) + expect(await l1GraphTokenGateway.inbox()).eq(inboxMock.address) + }) + }) + + describe('setL2TokenAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setL2TokenAddress(mockL2GRT.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets l2GRT', async function () { + const tx = l1GraphTokenGateway.connect(governor.signer).setL2TokenAddress(mockL2GRT.address) + await expect(tx).emit(l1GraphTokenGateway, 'L2TokenAddressSet').withArgs(mockL2GRT.address) + expect(await l1GraphTokenGateway.l2GRT()).eq(mockL2GRT.address) + }) + }) + + describe('setL2CounterpartAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setL2CounterpartAddress(mockL2Gateway.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets L2Counterpart', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .setL2CounterpartAddress(mockL2Gateway.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'L2CounterpartAddressSet') + .withArgs(mockL2Gateway.address) + expect(await l1GraphTokenGateway.l2Counterpart()).eq(mockL2Gateway.address) + }) + }) + describe('setEscrowAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setEscrowAddress(bridgeEscrow.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets escrow', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .setEscrowAddress(bridgeEscrow.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'EscrowAddressSet') + .withArgs(bridgeEscrow.address) + expect(await l1GraphTokenGateway.escrow()).eq(bridgeEscrow.address) + }) + }) + describe('Pausable behavior', () => { + it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { + let tx = l1GraphTokenGateway.connect(tokenSender.signer).setPaused(false) + await expect(tx).revertedWith('Only Governor or Guardian can call') + tx = l1GraphTokenGateway.connect(tokenSender.signer).setPaused(true) + await expect(tx).revertedWith('Only Governor or Guardian can call') + }) + it('can be paused and unpaused by the governor', async function () { + let tx = l1GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l1GraphTokenGateway.paused()).eq(false) + tx = l1GraphTokenGateway.connect(governor.signer).setPaused(true) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) + await expect(await l1GraphTokenGateway.paused()).eq(true) + }) + describe('setPauseGuardian', function () { + it('cannot be called by someone other than governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setPauseGuardian(pauseGuardian.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets a new pause guardian', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .setPauseGuardian(pauseGuardian.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'NewPauseGuardian') + .withArgs(AddressZero, pauseGuardian.address) + }) + it('allows a pause guardian to pause and unpause', async function () { + await l1GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) + let tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(false) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l1GraphTokenGateway.paused()).eq(false) + tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) + await expect(await l1GraphTokenGateway.paused()).eq(true) + }) + }) + }) + }) + + context('> after configuring and unpausing', function () { + const createMsgData = function () { + const selector = l1GraphTokenGateway.interface.getSighash('finalizeInboundTransfer') + const params = utils.defaultAbiCoder.encode( + ['address', 'address', 'address', 'uint256', 'bytes'], + [ + grt.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + utils.defaultAbiCoder.encode(['bytes', 'bytes'], [emptyCallHookData, emptyCallHookData]), + ], + ) + const outboundData = utils.hexlify(utils.concat([selector, params])) + + const msgData = utils.solidityPack( + [ + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'bytes', + ], + [ + toBN(mockL2Gateway.address), + toBN('0'), + defaultEthValue, + maxSubmissionCost, + applyL1ToL2Alias(tokenSender.address), + applyL1ToL2Alias(tokenSender.address), + maxGas, + gasPriceBid, + utils.hexDataLength(outboundData), + outboundData, + ], + ) + return msgData + } + const createInboxAccsEntry = function (msgDataHash: string) { + // The real bridge would emit the InboxAccs entry that came before this one, but our mock + // emits this, making it easier for us to validate here that all the parameters we sent are correct + const expectedInboxAccsEntry = utils.keccak256( + utils.solidityPack( + ['address', 'uint8', 'address', 'bytes32'], + [inboxMock.address, 9, l1GraphTokenGateway.address, msgDataHash], + ), + ) + return expectedInboxAccsEntry + } + const testValidOutboundTransfer = async function (signer: Signer, data: string) { + const tx = l1GraphTokenGateway + .connect(signer) + .outboundTransfer(grt.address, l2Receiver.address, toGRT('10'), maxGas, gasPriceBid, data, { + value: defaultEthValue, + }) + // Our bridge mock returns an incrementing seqNum starting at 1 + const expectedSeqNum = 1 + await expect(tx) + .emit(l1GraphTokenGateway, 'DepositInitiated') + .withArgs(grt.address, tokenSender.address, l2Receiver.address, expectedSeqNum, toGRT('10')) + + const msgData = createMsgData() + const msgDataHash = utils.keccak256(msgData) + const expectedInboxAccsEntry = createInboxAccsEntry(msgDataHash) + + await expect(tx).emit(inboxMock, 'InboxMessageDelivered').withArgs(1, msgData) + await expect(tx) + .emit(bridgeMock, 'MessageDelivered') + .withArgs( + expectedSeqNum, + expectedInboxAccsEntry, + inboxMock.address, + 9, + l1GraphTokenGateway.address, + msgDataHash, + ) + const escrowBalance = await grt.balanceOf(bridgeEscrow.address) + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(escrowBalance).eq(toGRT('10')) + await expect(senderBalance).eq(toGRT('990')) + } + before(async function () { + // First configure the Arbitrum bridge mocks + await bridgeMock.connect(governor.signer).setInbox(inboxMock.address, true) + await bridgeMock.connect(governor.signer).setOutbox(outboxMock.address, true) + await inboxMock.connect(governor.signer).setBridge(bridgeMock.address) + await outboxMock.connect(governor.signer).setBridge(bridgeMock.address) + + // Configure the gateway + await l1GraphTokenGateway + .connect(governor.signer) + .setArbitrumAddresses(inboxMock.address, mockRouter.address) + await l1GraphTokenGateway.connect(governor.signer).setL2TokenAddress(mockL2GRT.address) + await l1GraphTokenGateway + .connect(governor.signer) + .setL2CounterpartAddress(mockL2Gateway.address) + await l1GraphTokenGateway.connect(governor.signer).setEscrowAddress(bridgeEscrow.address) + await bridgeEscrow.connect(governor.signer).approveAll(l1GraphTokenGateway.address) + await l1GraphTokenGateway.connect(governor.signer).setPaused(false) + }) + + describe('calculateL2TokenAddress', function () { + it('returns the L2 token address', async function () { + expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(mockL2GRT.address) + }) + it('returns the zero address if the input is any other address', async function () { + expect(await l1GraphTokenGateway.calculateL2TokenAddress(tokenSender.address)).eq( + AddressZero, + ) + }) + }) + + describe('outboundTransfer', function () { + it('reverts when called with the wrong token address', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + tokenSender.address, + l2Receiver.address, + toGRT('10'), + maxGas, + gasPriceBid, + defaultData, + { + value: defaultEthValue, + }, + ) + await expect(tx).revertedWith('TOKEN_NOT_GRT') + }) + it('puts tokens in escrow and creates a retryable ticket', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData) + }) + it('decodes the sender address from messages sent by the router', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + const routerEncodedData = utils.defaultAbiCoder.encode( + ['address', 'bytes'], + [tokenSender.address, defaultData], + ) + await testValidOutboundTransfer(mockRouter.signer, routerEncodedData) + }) + it('reverts when called with the wrong value', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + grt.address, + l2Receiver.address, + toGRT('10'), + maxGas, + gasPriceBid, + defaultData, + { + value: defaultEthValue.add(1), + }, + ) + await expect(tx).revertedWith('WRONG_ETH_VALUE') + }) + it('reverts when called with nonempty calldata', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + grt.address, + l2Receiver.address, + toGRT('10'), + maxGas, + gasPriceBid, + defaultDataWithNotEmptyCallHookData, + { + value: defaultEthValue, + }, + ) + await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED') + }) + it('reverts when the sender does not have enough GRT', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('1001')) + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + grt.address, + l2Receiver.address, + toGRT('1001'), + maxGas, + gasPriceBid, + defaultData, + { + value: defaultEthValue, + }, + ) + await expect(tx).revertedWith('ERC20: transfer amount exceeds balance') + }) + }) + + describe('finalizeInboundTransfer', function () { + it('reverts when called by an account that is not the bridge', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .finalizeInboundTransfer( + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('NOT_FROM_BRIDGE') + }) + it('reverts when called by the bridge, but the tx was not started by the L2 gateway', async function () { + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('10'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock.connect(tokenSender.signer).executeTransaction( + toBN('0'), + [], + toBN('0'), + l2Receiver.address, // Note this is not mockL2Gateway + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') + }) + it('reverts if the gateway does not have tokens', async function () { + // This scenario should never really happen, but we still + // test that the gateway reverts in this case + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('10'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx).revertedWith('BRIDGE_OUT_OF_FUNDS') + }) + it('reverts if the gateway is revoked from escrow', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData) + // At this point, the gateway holds 10 GRT in escrow + // But we revoke the gateway's permission to move the funds: + await bridgeEscrow.connect(governor.signer).revokeAll(l1GraphTokenGateway.address) + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('8'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') + }) + it('sends tokens out of escrow', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData) + // At this point, the gateway holds 10 GRT in escrow + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('8'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'WithdrawalFinalized') + .withArgs(grt.address, l2Receiver.address, tokenSender.address, toBN('0'), toGRT('8')) + const escrowBalance = await grt.balanceOf(bridgeEscrow.address) + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(escrowBalance).eq(toGRT('2')) + await expect(senderBalance).eq(toGRT('998')) + }) + }) + }) +}) diff --git a/test/graphToken.test.ts b/test/graphToken.test.ts index 5f53e6866..b42274ac4 100644 --- a/test/graphToken.test.ts +++ b/test/graphToken.test.ts @@ -1,275 +1,5 @@ -import { expect } from 'chai' -import { constants, utils, BytesLike, BigNumber, Signature } from 'ethers' -import { eip712 } from '@graphprotocol/common-ts/dist/attestations' - -import { GraphToken } from '../build/types/GraphToken' - -import * as deployment from './lib/deployment' -import { getAccounts, getChainID, toBN, toGRT, Account } from './lib/testHelpers' - -const { AddressZero, MaxUint256 } = constants -const { keccak256, SigningKey } = utils - -const PERMIT_TYPE_HASH = eip712.typeHash( - 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)', -) -const SALT = '0x51f3d585afe6dfeb2af01bba0889a36c1db03beec88c6a4d0c53817069026afa' - -interface Permit { - owner: string - spender: string - value: BigNumber - nonce: BigNumber - deadline: BigNumber -} - -function hashEncodePermit(permit: Permit) { - return eip712.hashStruct( - PERMIT_TYPE_HASH, - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [permit.owner, permit.spender, permit.value, permit.nonce, permit.deadline], - ) -} - -function signPermit( - signer: BytesLike, - chainId: number, - contractAddress: string, - permit: Permit, -): Signature { - const domainSeparator = eip712.domainSeparator({ - name: 'Graph Token', - version: '0', - chainId, - verifyingContract: contractAddress, - salt: SALT, - }) - const hashEncodedPermit = hashEncodePermit(permit) - const message = eip712.encode(domainSeparator, hashEncodedPermit) - const messageHash = keccak256(message) - const signingKey = new SigningKey(signer) - return signingKey.signDigest(messageHash) -} +import { grtTests } from './lib/graphTokenTests' describe('GraphToken', () => { - let me: Account - let other: Account - let governor: Account - - const mePrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' - const otherPrivateKey = '0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1' - - let grt: GraphToken - - async function permitMaxOK(): Promise { - return permitOK(MaxUint256) - } - - async function permitOK(value: BigNumber): Promise { - const nonce = await grt.nonces(me.address) - return { - owner: me.address, - spender: other.address, - value: value, - nonce: nonce, - deadline: toBN('0'), - } - } - - async function permitExpired(): Promise { - const permit = await permitMaxOK() - permit.deadline = toBN('1') - return permit - } - - async function permitDeny(): Promise { - const permit = await permitMaxOK() - permit.value = toBN('0') - return permit - } - - async function createPermitTransaction(permit: Permit, signer: string) { - const chainID = await getChainID() - const signature: Signature = signPermit(signer, chainID, grt.address, permit) - return grt.permit( - permit.owner, - permit.spender, - permit.value, - permit.deadline, - signature.v, - signature.r, - signature.s, - ) - } - - before(async function () { - ;[me, other, governor] = await getAccounts() - }) - - beforeEach(async function () { - // Deploy graph token - grt = await deployment.deployGRT(governor.signer) - - // Mint some tokens - const tokens = toGRT('10000') - await grt.connect(governor.signer).mint(me.address, tokens) - }) - - describe('permit', function () { - it('should permit max token allowance', async function () { - // Allow to transfer tokens - const tokensToApprove = toGRT('1000') - const permit = await permitOK(tokensToApprove) - const tx = createPermitTransaction(permit, mePrivateKey) - await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, tokensToApprove) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(tokensToApprove) - - // Transfer tokens should work - const tokens = toGRT('100') - await grt.connect(other.signer).transferFrom(me.address, other.address, tokens) - }) - - it('should permit max token allowance', async function () { - // Allow to transfer tokens - const permit = await permitMaxOK() - const tx = createPermitTransaction(permit, mePrivateKey) - await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, MaxUint256) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(MaxUint256) - - // Transfer tokens should work - const tokens = toGRT('100') - await grt.connect(other.signer).transferFrom(me.address, other.address, tokens) - }) - - it('reject to transfer more tokens than approved by permit', async function () { - // Allow to transfer tokens - const tokensToApprove = toGRT('1000') - const permit = await permitOK(tokensToApprove) - await createPermitTransaction(permit, mePrivateKey) - - // Should not transfer more than approved - const tooManyTokens = toGRT('1001') - const tx = grt.connect(other.signer).transferFrom(me.address, other.address, tooManyTokens) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - - // Should transfer up to the approved amount - await grt.connect(other.signer).transferFrom(me.address, other.address, tokensToApprove) - }) - - it('reject use two permits with same nonce', async function () { - // Allow to transfer tokens - const permit = await permitMaxOK() - await createPermitTransaction(permit, mePrivateKey) - - // Try to re-use the permit - const tx = createPermitTransaction(permit, mePrivateKey) - await expect(tx).revertedWith('GRT: invalid permit') - }) - - it('reject use expired permit', async function () { - const permit = await permitExpired() - const tx = createPermitTransaction(permit, mePrivateKey) - await expect(tx).revertedWith('GRT: expired permit') - }) - - it('reject permit if holder address does not match', async function () { - const permit = await permitMaxOK() - const tx = createPermitTransaction(permit, otherPrivateKey) - await expect(tx).revertedWith('GRT: invalid permit') - }) - - it('should deny transfer from if permit was denied', async function () { - // Allow to transfer tokens - const permit1 = await permitMaxOK() - await createPermitTransaction(permit1, mePrivateKey) - - // Deny transfer tokens - const permit2 = await permitDeny() - await createPermitTransaction(permit2, mePrivateKey) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(toBN('0')) - - // Try to transfer without permit should fail - const tokens = toGRT('100') - const tx = grt.connect(other.signer).transferFrom(me.address, other.address, tokens) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - }) - }) - - describe('mint', function () { - describe('addMinter', function () { - it('reject add a new minter if not allowed', async function () { - const tx = grt.connect(me.signer).addMinter(me.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('should add a new minter', async function () { - expect(await grt.isMinter(me.address)).eq(false) - const tx = grt.connect(governor.signer).addMinter(me.address) - await expect(tx).emit(grt, 'MinterAdded').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(true) - }) - }) - - describe('mint', async function () { - it('reject mint if not minter', async function () { - const tx = grt.connect(me.signer).mint(me.address, toGRT('100')) - await expect(tx).revertedWith('Only minter can call') - }) - }) - - context('> when is minter', function () { - beforeEach(async function () { - await grt.connect(governor.signer).addMinter(me.address) - expect(await grt.isMinter(me.address)).eq(true) - }) - - describe('mint', async function () { - it('should mint', async function () { - const beforeTokens = await grt.balanceOf(me.address) - - const tokensToMint = toGRT('100') - const tx = grt.connect(me.signer).mint(me.address, tokensToMint) - await expect(tx).emit(grt, 'Transfer').withArgs(AddressZero, me.address, tokensToMint) - - const afterTokens = await grt.balanceOf(me.address) - expect(afterTokens).eq(beforeTokens.add(tokensToMint)) - }) - - it('should mint if governor', async function () { - const tokensToMint = toGRT('100') - await grt.connect(governor.signer).mint(me.address, tokensToMint) - }) - }) - - describe('removeMinter', function () { - it('reject remove a minter if not allowed', async function () { - const tx = grt.connect(me.signer).removeMinter(me.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('should remove a minter', async function () { - const tx = grt.connect(governor.signer).removeMinter(me.address) - await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(false) - }) - }) - - describe('renounceMinter', function () { - it('should renounce to be a minter', async function () { - const tx = grt.connect(me.signer).renounceMinter() - await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(false) - }) - }) - }) - }) + grtTests.bind(this)(false) }) diff --git a/test/l2/l2GraphToken.test.ts b/test/l2/l2GraphToken.test.ts new file mode 100644 index 000000000..56d8e5ed6 --- /dev/null +++ b/test/l2/l2GraphToken.test.ts @@ -0,0 +1,107 @@ +import { expect } from 'chai' + +import { getAccounts, toGRT, Account, initNetwork } from '../lib/testHelpers' + +import { L2GraphToken } from '../../build/types/L2GraphToken' + +import { grtTests } from '../lib/graphTokenTests' +import { NetworkFixture } from '../lib/fixtures' + +describe('L2GraphToken', () => { + describe('Base GRT behavior', () => { + grtTests.bind(this)(true) + }) + describe('Extended L2 behavior', () => { + let mockL2Gateway: Account + let mockL1GRT: Account + let governor: Account + let user: Account + + let fixture: NetworkFixture + let grt: L2GraphToken + + before(async function () { + await initNetwork() + ;[mockL1GRT, mockL2Gateway, governor, user] = await getAccounts() + fixture = new NetworkFixture() + ;({ grt } = await fixture.loadL2(governor.signer)) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('setGateway', async function () { + it('cannot be called by someone who is not the governor', async function () { + const tx = grt.connect(mockL2Gateway.signer).setGateway(mockL2Gateway.address) + await expect(tx).revertedWith('Only Governor can call') + }) + it('sets the L2 Gateway address when called by the governor', async function () { + const tx = grt.connect(governor.signer).setGateway(mockL2Gateway.address) + await expect(tx).emit(grt, 'GatewaySet').withArgs(mockL2Gateway.address) + await expect(await grt.gateway()).eq(mockL2Gateway.address) + }) + }) + describe('setL1Address', async function () { + it('cannot be called by someone who is not the governor', async function () { + const tx = grt.connect(mockL2Gateway.signer).setL1Address(mockL1GRT.address) + await expect(tx).revertedWith('Only Governor can call') + }) + it('sets the L1 GRT address when called by the governor', async function () { + const tx = grt.connect(governor.signer).setL1Address(mockL1GRT.address) + await expect(tx).emit(grt, 'L1AddressSet').withArgs(mockL1GRT.address) + await expect(await grt.l1Address()).eq(mockL1GRT.address) + }) + }) + describe('bridge minting and burning', async function () { + beforeEach(async function () { + // Configure the l1Address and gateway + await grt.connect(governor.signer).setL1Address(mockL1GRT.address) + await grt.connect(governor.signer).setGateway(mockL2Gateway.address) + }) + describe('bridgeMint', async function () { + it('cannot be called by someone who is not the gateway', async function () { + const tx = grt.connect(governor.signer).bridgeMint(user.address, toGRT('100')) + await expect(tx).revertedWith('NOT_GATEWAY') + }) + it('mints GRT into a destination account', async function () { + const tx = grt.connect(mockL2Gateway.signer).bridgeMint(user.address, toGRT('100')) + await expect(tx).emit(grt, 'BridgeMinted').withArgs(user.address, toGRT('100')) + await expect(await grt.balanceOf(user.address)).eq(toGRT('100')) + }) + }) + describe('bridgeBurn', async function () { + it('cannot be called by someone who is not the gateway', async function () { + const tx = grt.connect(governor.signer).bridgeBurn(user.address, toGRT('100')) + await expect(tx).revertedWith('NOT_GATEWAY') + }) + it('requires approval for burning', async function () { + await grt.connect(mockL2Gateway.signer).bridgeMint(user.address, toGRT('100')) + const tx = grt.connect(mockL2Gateway.signer).bridgeBurn(user.address, toGRT('20')) + await expect(tx).revertedWith('ERC20: burn amount exceeds allowance') + }) + it('fails if the user does not have enough funds', async function () { + await grt.connect(mockL2Gateway.signer).bridgeMint(user.address, toGRT('10')) + await grt.connect(user.signer).approve(mockL2Gateway.address, toGRT('20')) + const tx = grt.connect(mockL2Gateway.signer).bridgeBurn(user.address, toGRT('20')) + await expect(tx).revertedWith('ERC20: burn amount exceeds balance') + }) + it('burns GRT from an account when approved', async function () { + await grt.connect(mockL2Gateway.signer).bridgeMint(user.address, toGRT('100')) + await grt.connect(user.signer).approve(mockL2Gateway.address, toGRT('20')) + const tx = grt.connect(mockL2Gateway.signer).bridgeBurn(user.address, toGRT('20')) + await expect(tx).emit(grt, 'BridgeBurned').withArgs(user.address, toGRT('20')) + await expect(await grt.balanceOf(user.address)).eq(toGRT('80')) + }) + }) + it('does not allow the bridge to mint as a regular minter', async function () { + const tx = grt.connect(mockL2Gateway.signer).mint(user.address, toGRT('100')) + await expect(tx).revertedWith('Only minter can call') + }) + }) + }) +}) diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts new file mode 100644 index 000000000..a2b0e04b7 --- /dev/null +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -0,0 +1,379 @@ +import { expect, use } from 'chai' +import { constants, Signer, utils } from 'ethers' +import hre from 'hardhat' + +import { L2GraphToken } from '../../build/types/L2GraphToken' +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' + +import { NetworkFixture } from '../lib/fixtures' + +import { FakeContract, smock } from '@defi-wonderland/smock' + +use(smock.matchers) + +import { getAccounts, toGRT, Account, provider, applyL1ToL2Alias } from '../lib/testHelpers' + +const { AddressZero } = constants + +// Adapted from: +// https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/test/utils/messaging.ts#L5 +async function getL2SignerFromL1(l1Address: string): Promise { + const l2Address = applyL1ToL2Alias(l1Address) + await provider().send('hardhat_impersonateAccount', [l2Address]) + const l2Signer = await hre.ethers.getSigner(l2Address) + + return l2Signer +} + +describe('L2GraphTokenGateway', () => { + let me: Account + let governor: Account + let tokenSender: Account + let l1Receiver: Account + let l2Receiver: Account + let mockRouter: Account + let mockL1GRT: Account + let mockL1Gateway: Account + let pauseGuardian: Account + let fixture: NetworkFixture + let arbSysMock: FakeContract + + let grt: L2GraphToken + let l2GraphTokenGateway: L2GraphTokenGateway + + const senderTokens = toGRT('1000') + const defaultData = '0x' + const notEmptyCallHookData = '0x12' + const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( + ['bytes'], + [notEmptyCallHookData], + ) + + before(async function () { + ;[ + me, + governor, + tokenSender, + l1Receiver, + mockRouter, + mockL1GRT, + mockL1Gateway, + l2Receiver, + pauseGuardian, + ] = await getAccounts() + + fixture = new NetworkFixture() + ;({ grt, l2GraphTokenGateway } = await fixture.loadL2(governor.signer)) + + // Give some funds to the token sender + await grt.connect(governor.signer).mint(tokenSender.address, senderTokens) + }) + + beforeEach(async function () { + await fixture.setUp() + // Thanks to Livepeer: https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/test/unit/L2/l2LPTGateway.test.ts#L86 + arbSysMock = await smock.fake('ArbSys', { + address: '0x0000000000000000000000000000000000000064', + }) + arbSysMock.sendTxToL1.returns(1) + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + context('> immediately after deploy', function () { + describe('calculateL2TokenAddress', function () { + it('should return the zero address', async function () { + expect(await l2GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(AddressZero) + }) + }) + + describe('outboundTransfer', function () { + it('reverts because it is paused', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + ['outboundTransfer(address,address,uint256,bytes)']( + grt.address, + l1Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('Paused (contract)') + }) + }) + + describe('finalizeInboundTransfer', function () { + it('revert because it is paused', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .finalizeInboundTransfer( + grt.address, + tokenSender.address, + l1Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('Paused (contract)') + }) + }) + + describe('setL2Router', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l2GraphTokenGateway.connect(tokenSender.signer).setL2Router(mockRouter.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets router address', async function () { + const tx = l2GraphTokenGateway.connect(governor.signer).setL2Router(mockRouter.address) + await expect(tx).emit(l2GraphTokenGateway, 'L2RouterSet').withArgs(mockRouter.address) + expect(await l2GraphTokenGateway.l2Router()).eq(mockRouter.address) + }) + }) + + describe('setL1TokenAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .setL1TokenAddress(mockL1GRT.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets l2GRT', async function () { + const tx = l2GraphTokenGateway.connect(governor.signer).setL1TokenAddress(mockL1GRT.address) + await expect(tx).emit(l2GraphTokenGateway, 'L1TokenAddressSet').withArgs(mockL1GRT.address) + expect(await l2GraphTokenGateway.l1GRT()).eq(mockL1GRT.address) + }) + }) + + describe('setL1CounterpartAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .setL1CounterpartAddress(mockL1Gateway.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets L1Counterpart', async function () { + const tx = l2GraphTokenGateway + .connect(governor.signer) + .setL1CounterpartAddress(mockL1Gateway.address) + await expect(tx) + .emit(l2GraphTokenGateway, 'L1CounterpartAddressSet') + .withArgs(mockL1Gateway.address) + expect(await l2GraphTokenGateway.l1Counterpart()).eq(mockL1Gateway.address) + }) + }) + describe('Pausable behavior', () => { + it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { + let tx = l2GraphTokenGateway.connect(tokenSender.signer).setPaused(false) + await expect(tx).revertedWith('Only Governor or Guardian can call') + tx = l2GraphTokenGateway.connect(tokenSender.signer).setPaused(true) + await expect(tx).revertedWith('Only Governor or Guardian can call') + }) + it('can be paused and unpaused by the governor', async function () { + let tx = l2GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l2GraphTokenGateway.paused()).eq(false) + tx = l2GraphTokenGateway.connect(governor.signer).setPaused(true) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) + await expect(await l2GraphTokenGateway.paused()).eq(true) + }) + describe('setPauseGuardian', function () { + it('cannot be called by someone other than governor', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .setPauseGuardian(pauseGuardian.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets a new pause guardian', async function () { + const tx = l2GraphTokenGateway + .connect(governor.signer) + .setPauseGuardian(pauseGuardian.address) + await expect(tx) + .emit(l2GraphTokenGateway, 'NewPauseGuardian') + .withArgs(AddressZero, pauseGuardian.address) + }) + it('allows a pause guardian to pause and unpause', async function () { + await l2GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) + let tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(false) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l2GraphTokenGateway.paused()).eq(false) + tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) + await expect(await l2GraphTokenGateway.paused()).eq(true) + }) + }) + }) + }) + + context('> after configuring and unpausing', function () { + const testValidOutboundTransfer = async function (signer: Signer, data: string) { + const tx = l2GraphTokenGateway + .connect(signer) + ['outboundTransfer(address,address,uint256,bytes)']( + mockL1GRT.address, + l1Receiver.address, + toGRT('10'), + data, + ) + const expectedId = 1 + await expect(tx) + .emit(l2GraphTokenGateway, 'WithdrawalInitiated') + .withArgs( + mockL1GRT.address, + tokenSender.address, + l1Receiver.address, + expectedId, + 0, + toGRT('10'), + ) + + // Should use the L1 Gateway's interface, but both come from ITokenGateway + const calldata = l2GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ + mockL1GRT.address, + tokenSender.address, + l1Receiver.address, + toGRT('10'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ]) + await expect(tx) + .emit(l2GraphTokenGateway, 'TxToL1') + .withArgs(tokenSender.address, mockL1Gateway.address, 1, calldata) + + // For some reason the call count doesn't work properly, + // and each function call is counted 12 times. + // Possibly related to https://github.com/defi-wonderland/smock/issues/85 ? + //expect(arbSysMock.sendTxToL1).to.have.been.calledOnce + expect(arbSysMock.sendTxToL1).to.have.been.calledWith(mockL1Gateway.address, calldata) + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(senderBalance).eq(toGRT('990')) + } + before(async function () { + // Configure the L2 GRT + // Configure the gateway + await grt.connect(governor.signer).setGateway(l2GraphTokenGateway.address) + await grt.connect(governor.signer).setL1Address(mockL1GRT.address) + // Configure the gateway + await l2GraphTokenGateway.connect(governor.signer).setL2Router(mockRouter.address) + await l2GraphTokenGateway.connect(governor.signer).setL1TokenAddress(mockL1GRT.address) + await l2GraphTokenGateway + .connect(governor.signer) + .setL1CounterpartAddress(mockL1Gateway.address) + await l2GraphTokenGateway.connect(governor.signer).setPaused(false) + }) + + describe('calculateL2TokenAddress', function () { + it('returns the L2 token address', async function () { + expect(await l2GraphTokenGateway.calculateL2TokenAddress(mockL1GRT.address)).eq(grt.address) + }) + it('returns the zero address if the input is any other address', async function () { + expect(await l2GraphTokenGateway.calculateL2TokenAddress(tokenSender.address)).eq( + AddressZero, + ) + }) + }) + + describe('outboundTransfer', function () { + it('reverts when called with the wrong token address', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + ['outboundTransfer(address,address,uint256,bytes)']( + tokenSender.address, + l1Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('TOKEN_NOT_GRT') + }) + it('burns tokens and triggers an L1 call', async function () { + await grt.connect(tokenSender.signer).approve(l2GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData) + }) + it('decodes the sender address from messages sent by the router', async function () { + await grt.connect(tokenSender.signer).approve(l2GraphTokenGateway.address, toGRT('10')) + const routerEncodedData = utils.defaultAbiCoder.encode( + ['address', 'bytes'], + [tokenSender.address, defaultData], + ) + await testValidOutboundTransfer(mockRouter.signer, routerEncodedData) + }) + it('reverts when called with nonempty calldata', async function () { + await grt.connect(tokenSender.signer).approve(l2GraphTokenGateway.address, toGRT('10')) + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + ['outboundTransfer(address,address,uint256,bytes)']( + mockL1GRT.address, + l1Receiver.address, + toGRT('10'), + defaultDataWithNotEmptyCallHookData, + ) + await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED') + }) + it('reverts when the sender does not have enough GRT', async function () { + await grt.connect(tokenSender.signer).approve(l2GraphTokenGateway.address, toGRT('1001')) + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + ['outboundTransfer(address,address,uint256,bytes)']( + mockL1GRT.address, + l1Receiver.address, + toGRT('1001'), + defaultData, + ) + await expect(tx).revertedWith('ERC20: burn amount exceeds balance') + }) + }) + + describe('finalizeInboundTransfer', function () { + it('reverts when called by an account that is not the gateway', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .finalizeInboundTransfer( + mockL1GRT.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') + }) + it('reverts when called by an account that is the gateway but without the L2 alias', async function () { + const tx = l2GraphTokenGateway + .connect(mockL1Gateway.signer) + .finalizeInboundTransfer( + mockL1GRT.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') + }) + it('mints and sends tokens when called by the aliased gateway', async function () { + const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) + await me.signer.sendTransaction({ + to: await mockL1GatewayL2Alias.getAddress(), + value: utils.parseUnits('1', 'ether'), + }) + const tx = l2GraphTokenGateway + .connect(mockL1GatewayL2Alias) + .finalizeInboundTransfer( + mockL1GRT.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, tokenSender.address, l2Receiver.address, toGRT('10')) + + await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Receiver.address, toGRT('10')) + + // Unchanged + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(senderBalance).eq(toGRT('1000')) + // 10 newly minted GRT + const receiverBalance = await grt.balanceOf(l2Receiver.address) + await expect(receiverBalance).eq(toGRT('10')) + }) + }) + }) +}) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index 6194ef656..fe5eed768 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -19,6 +19,10 @@ import { RewardsManager } from '../../build/types/RewardsManager' import { EthereumDIDRegistry } from '../../build/types/EthereumDIDRegistry' import { GraphGovernance } from '../../build/types/GraphGovernance' import { SubgraphNFT } from '../../build/types/SubgraphNFT' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { L2GraphToken } from '../../build/types/L2GraphToken' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' // Disable logging for tests logger.pause() @@ -248,3 +252,54 @@ export async function deployGraphGovernance( deployer, ) as unknown as GraphGovernance } + +export async function deployL1GraphTokenGateway( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L1GraphTokenGateway', + [controller], + deployer, + ) as unknown as L1GraphTokenGateway +} + +export async function deployBridgeEscrow( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'BridgeEscrow', + [controller], + deployer, + ) as unknown as BridgeEscrow +} + +export async function deployL2GraphTokenGateway( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L2GraphTokenGateway', + [controller], + deployer, + ) as unknown as L2GraphTokenGateway +} + +export async function deployL2GRT( + deployer: Signer, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L2GraphToken', + [await deployer.getAddress(), toBN('0')], + deployer, + ) as unknown as L2GraphToken +} diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 96fd68665..3a829c181 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -2,7 +2,7 @@ import { utils, Wallet, Signer } from 'ethers' import * as deployment from './deployment' -import { evmSnapshot, evmRevert, provider } from './testHelpers' +import { evmSnapshot, evmRevert, initNetwork } from './testHelpers' export class NetworkFixture { lastSnapshotId: number @@ -16,11 +16,7 @@ export class NetworkFixture { slasher: Signer = Wallet.createRandom() as Signer, arbitrator: Signer = Wallet.createRandom() as Signer, ): Promise { - // Enable automining with each transaction, and disable - // the mining interval. Individual tests may modify this - // behavior as needed. - provider().send('evm_setIntervalMining', [0]) - provider().send('evm_setAutomine', [true]) + await initNetwork() // Roles const arbitratorAddress = await arbitrator.getAddress() @@ -55,6 +51,18 @@ export class NetworkFixture { proxyAdmin, ) + const l1GraphTokenGateway = await deployment.deployL1GraphTokenGateway( + deployer, + controller.address, + proxyAdmin, + ) + + const bridgeEscrow = await deployment.deployBridgeEscrow( + deployer, + controller.address, + proxyAdmin, + ) + // Setup controller await controller.setContractProxy(utils.id('EpochManager'), epochManager.address) await controller.setContractProxy(utils.id('GraphToken'), grt.address) @@ -63,6 +71,7 @@ export class NetworkFixture { await controller.setContractProxy(utils.id('DisputeManager'), staking.address) await controller.setContractProxy(utils.id('RewardsManager'), rewardsManager.address) await controller.setContractProxy(utils.id('ServiceRegistry'), serviceRegistry.address) + await controller.setContractProxy(utils.id('GraphTokenGateway'), l1GraphTokenGateway.address) // Setup contracts await curation.connect(deployer).syncAllContracts() @@ -71,6 +80,8 @@ export class NetworkFixture { await disputeManager.connect(deployer).syncAllContracts() await rewardsManager.connect(deployer).syncAllContracts() await staking.connect(deployer).syncAllContracts() + await l1GraphTokenGateway.connect(deployer).syncAllContracts() + await bridgeEscrow.connect(deployer).syncAllContracts() await staking.connect(deployer).setSlasher(slasherAddress, true) await grt.connect(deployer).addMinter(rewardsManager.address) @@ -90,6 +101,42 @@ export class NetworkFixture { rewardsManager, serviceRegistry, proxyAdmin, + l1GraphTokenGateway, + bridgeEscrow, + } + } + + async loadL2(deployer: Signer): Promise { + await initNetwork() + + // Deploy contracts + const proxyAdmin = await deployment.deployProxyAdmin(deployer) + const controller = await deployment.deployController(deployer) + + const grt = await deployment.deployL2GRT(deployer, proxyAdmin) + + const l2GraphTokenGateway = await deployment.deployL2GraphTokenGateway( + deployer, + controller.address, + proxyAdmin, + ) + + // Setup controller + await controller.setContractProxy(utils.id('GraphToken'), grt.address) + await controller.setContractProxy(utils.id('GraphTokenGateway'), l2GraphTokenGateway.address) + + // Setup contracts + await l2GraphTokenGateway.connect(deployer).syncAllContracts() + await grt.connect(deployer).addMinter(l2GraphTokenGateway.address) + + // Unpause the protocol + await controller.connect(deployer).setPaused(false) + + return { + controller, + grt, + proxyAdmin, + l2GraphTokenGateway, } } diff --git a/test/lib/graphTokenTests.ts b/test/lib/graphTokenTests.ts new file mode 100644 index 000000000..b0a1b7c7f --- /dev/null +++ b/test/lib/graphTokenTests.ts @@ -0,0 +1,287 @@ +import { expect } from 'chai' +import { constants, utils, BytesLike, BigNumber, Signature } from 'ethers' +import { eip712 } from '@graphprotocol/common-ts/dist/attestations' + +import * as deployment from './deployment' +import { getAccounts, getChainID, toBN, toGRT, Account, initNetwork } from './testHelpers' + +import { L2GraphToken } from '../../build/types/L2GraphToken' +import { GraphToken } from '../../build/types/GraphToken' + +const { AddressZero, MaxUint256 } = constants +const { keccak256, SigningKey } = utils + +const PERMIT_TYPE_HASH = eip712.typeHash( + 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)', +) +const L1SALT = '0x51f3d585afe6dfeb2af01bba0889a36c1db03beec88c6a4d0c53817069026afa' +const L2SALT = '0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591' + +interface Permit { + owner: string + spender: string + value: BigNumber + nonce: BigNumber + deadline: BigNumber +} + +function hashEncodePermit(permit: Permit) { + return eip712.hashStruct( + PERMIT_TYPE_HASH, + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [permit.owner, permit.spender, permit.value, permit.nonce, permit.deadline], + ) +} + +function signPermit( + signer: BytesLike, + chainId: number, + contractAddress: string, + permit: Permit, + salt: string, +): Signature { + const domainSeparator = eip712.domainSeparator({ + name: 'Graph Token', + version: '0', + chainId, + verifyingContract: contractAddress, + salt: salt, + }) + const hashEncodedPermit = hashEncodePermit(permit) + const message = eip712.encode(domainSeparator, hashEncodedPermit) + const messageHash = keccak256(message) + const signingKey = new SigningKey(signer) + return signingKey.signDigest(messageHash) +} + +export function grtTests(isL2: boolean): void { + let me: Account + let other: Account + let governor: Account + let salt: string + + const mePrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' + const otherPrivateKey = '0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1' + + let grt: GraphToken | L2GraphToken + + async function permitMaxOK(): Promise { + return permitOK(MaxUint256) + } + + async function permitOK(value: BigNumber): Promise { + const nonce = await grt.nonces(me.address) + return { + owner: me.address, + spender: other.address, + value: value, + nonce: nonce, + deadline: toBN('0'), + } + } + + async function permitExpired(): Promise { + const permit = await permitMaxOK() + permit.deadline = toBN('1') + return permit + } + + async function permitDeny(): Promise { + const permit = await permitMaxOK() + permit.value = toBN('0') + return permit + } + + async function createPermitTransaction(permit: Permit, signer: string, salt: string) { + const chainID = await getChainID() + const signature: Signature = signPermit(signer, chainID, grt.address, permit, salt) + return grt.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + signature.v, + signature.r, + signature.s, + ) + } + + before(async function () { + await initNetwork() + ;[me, other, governor] = await getAccounts() + }) + + beforeEach(async function () { + // Deploy graph token + if (isL2) { + const proxyAdmin = await deployment.deployProxyAdmin(governor.signer) + grt = await deployment.deployL2GRT(governor.signer, proxyAdmin) + salt = L2SALT + } else { + grt = await deployment.deployGRT(governor.signer) + salt = L1SALT + } + + // Mint some tokens + const tokens = toGRT('10000') + await grt.connect(governor.signer).mint(me.address, tokens) + }) + + describe('permit', function () { + it('should permit max token allowance', async function () { + // Allow to transfer tokens + const tokensToApprove = toGRT('1000') + const permit = await permitOK(tokensToApprove) + const tx = createPermitTransaction(permit, mePrivateKey, salt) + await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, tokensToApprove) + + // Allowance updated + const allowance = await grt.allowance(me.address, other.address) + expect(allowance).eq(tokensToApprove) + + // Transfer tokens should work + const tokens = toGRT('100') + await grt.connect(other.signer).transferFrom(me.address, other.address, tokens) + }) + + it('should permit max token allowance', async function () { + // Allow to transfer tokens + const permit = await permitMaxOK() + const tx = createPermitTransaction(permit, mePrivateKey, salt) + await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, MaxUint256) + + // Allowance updated + const allowance = await grt.allowance(me.address, other.address) + expect(allowance).eq(MaxUint256) + + // Transfer tokens should work + const tokens = toGRT('100') + await grt.connect(other.signer).transferFrom(me.address, other.address, tokens) + }) + + it('reject to transfer more tokens than approved by permit', async function () { + // Allow to transfer tokens + const tokensToApprove = toGRT('1000') + const permit = await permitOK(tokensToApprove) + await createPermitTransaction(permit, mePrivateKey, salt) + + // Should not transfer more than approved + const tooManyTokens = toGRT('1001') + const tx = grt.connect(other.signer).transferFrom(me.address, other.address, tooManyTokens) + await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') + + // Should transfer up to the approved amount + await grt.connect(other.signer).transferFrom(me.address, other.address, tokensToApprove) + }) + + it('reject use two permits with same nonce', async function () { + // Allow to transfer tokens + const permit = await permitMaxOK() + await createPermitTransaction(permit, mePrivateKey, salt) + + // Try to re-use the permit + const tx = createPermitTransaction(permit, mePrivateKey, salt) + await expect(tx).revertedWith('GRT: invalid permit') + }) + + it('reject use expired permit', async function () { + const permit = await permitExpired() + const tx = createPermitTransaction(permit, mePrivateKey, salt) + await expect(tx).revertedWith('GRT: expired permit') + }) + + it('reject permit if holder address does not match', async function () { + const permit = await permitMaxOK() + const tx = createPermitTransaction(permit, otherPrivateKey, salt) + await expect(tx).revertedWith('GRT: invalid permit') + }) + + it('should deny transfer from if permit was denied', async function () { + // Allow to transfer tokens + const permit1 = await permitMaxOK() + await createPermitTransaction(permit1, mePrivateKey, salt) + + // Deny transfer tokens + const permit2 = await permitDeny() + await createPermitTransaction(permit2, mePrivateKey, salt) + + // Allowance updated + const allowance = await grt.allowance(me.address, other.address) + expect(allowance).eq(toBN('0')) + + // Try to transfer without permit should fail + const tokens = toGRT('100') + const tx = grt.connect(other.signer).transferFrom(me.address, other.address, tokens) + await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') + }) + }) + + describe('mint', function () { + describe('addMinter', function () { + it('reject add a new minter if not allowed', async function () { + const tx = grt.connect(me.signer).addMinter(me.address) + await expect(tx).revertedWith('Only Governor can call') + }) + + it('should add a new minter', async function () { + expect(await grt.isMinter(me.address)).eq(false) + const tx = grt.connect(governor.signer).addMinter(me.address) + await expect(tx).emit(grt, 'MinterAdded').withArgs(me.address) + expect(await grt.isMinter(me.address)).eq(true) + }) + }) + + describe('mint', async function () { + it('reject mint if not minter', async function () { + const tx = grt.connect(me.signer).mint(me.address, toGRT('100')) + await expect(tx).revertedWith('Only minter can call') + }) + }) + + context('> when is minter', function () { + beforeEach(async function () { + await grt.connect(governor.signer).addMinter(me.address) + expect(await grt.isMinter(me.address)).eq(true) + }) + + describe('mint', async function () { + it('should mint', async function () { + const beforeTokens = await grt.balanceOf(me.address) + + const tokensToMint = toGRT('100') + const tx = grt.connect(me.signer).mint(me.address, tokensToMint) + await expect(tx).emit(grt, 'Transfer').withArgs(AddressZero, me.address, tokensToMint) + + const afterTokens = await grt.balanceOf(me.address) + expect(afterTokens).eq(beforeTokens.add(tokensToMint)) + }) + + it('should mint if governor', async function () { + const tokensToMint = toGRT('100') + await grt.connect(governor.signer).mint(me.address, tokensToMint) + }) + }) + + describe('removeMinter', function () { + it('reject remove a minter if not allowed', async function () { + const tx = grt.connect(me.signer).removeMinter(me.address) + await expect(tx).revertedWith('Only Governor can call') + }) + + it('should remove a minter', async function () { + const tx = grt.connect(governor.signer).removeMinter(me.address) + await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) + expect(await grt.isMinter(me.address)).eq(false) + }) + }) + + describe('renounceMinter', function () { + it('should renounce to be a minter', async function () { + const tx = grt.connect(me.signer).renounceMinter() + await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) + expect(await grt.isMinter(me.address)).eq(false) + }) + }) + }) + }) +} diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index ea07ba449..59372dd68 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -25,6 +25,14 @@ export interface Account { export const provider = (): providers.JsonRpcProvider => hre.waffle.provider +// Enable automining with each transaction, and disable +// the mining interval. Individual tests may modify this +// behavior as needed. +export async function initNetwork(): Promise { + await provider().send('evm_setIntervalMining', [0]) + await provider().send('evm_setAutomine', [true]) +} + export const getAccounts = async (): Promise => { const accounts = [] const signers: Signer[] = await hre.ethers.getSigners() @@ -113,3 +121,14 @@ export const deriveChannelKey = (): ChannelKey => { }, } } + +// Adapted from: +// https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/utils/arbitrum/messaging.ts#L118 +export const applyL1ToL2Alias = (l1Address: string): string => { + const offset = toBN('0x1111000000000000000000000000000000001111') + const l1AddressAsNumber = toBN(l1Address) + const l2AddressAsNumber = l1AddressAsNumber.add(offset) + + const mask = toBN(2).pow(160) + return l2AddressAsNumber.mod(mask).toHexString() +} diff --git a/test/staking/allocation.test.ts b/test/staking/allocation.test.ts index 96b6ca332..b20f55fc5 100644 --- a/test/staking/allocation.test.ts +++ b/test/staking/allocation.test.ts @@ -17,7 +17,7 @@ import { Account, } from '../lib/testHelpers' -const { AddressZero, HashZero } = constants +const { AddressZero } = constants const MAX_PPM = toBN('1000000') diff --git a/test/staking/rebate.test.ts b/test/staking/rebate.test.ts index 416992716..a4f152514 100644 --- a/test/staking/rebate.test.ts +++ b/test/staking/rebate.test.ts @@ -4,7 +4,7 @@ import { BigNumber } from 'ethers' import { deployContract } from '../lib/deployment' import { RebatePoolMock } from '../../build/types/RebatePoolMock' -import { getAccounts, toBN, toGRT, formatGRT, Account } from '../lib/testHelpers' +import { getAccounts, toBN, toGRT, formatGRT, Account, initNetwork } from '../lib/testHelpers' const toFloat = (n: BigNumber) => parseFloat(formatGRT(n)) const toFixed = (n: number | BigNumber, precision = 12) => { @@ -186,6 +186,7 @@ describe('Staking:Rebate', () => { } beforeEach(async function () { + await initNetwork() ;[deployer] = await getAccounts() rebatePoolMock = (await deployContract( 'RebatePoolMock', diff --git a/yarn.lock b/yarn.lock index 592a8a491..c2bc98ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -184,6 +184,18 @@ enabled "2.0.x" kuler "^2.0.0" +"@defi-wonderland/smock@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@defi-wonderland/smock/-/smock-2.0.7.tgz#59d5fc93e175ad120c5dcdd8294e07525606c855" + integrity sha512-RVpODLKZ/Cr0C1bCbhJ2aXbAr2Ll/K2WO7hDL96tqhMzCsA7ToWdDIgiNpV5Vtqqvpftu5ddO7v3TAurQNSU0w== + dependencies: + "@nomiclabs/ethereumjs-vm" "^4.2.2" + diff "^5.0.0" + lodash.isequal "^4.5.0" + lodash.isequalwith "^4.4.0" + rxjs "^7.2.0" + semver "^7.3.5" + "@endemolshinegroup/cosmiconfig-typescript-loader@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz#eea4635828dde372838b0909693ebd9aafeec22d" @@ -278,7 +290,7 @@ patch-package "^6.2.2" postinstall-postinstall "^2.1.0" -"@ethereumjs/block@^3.5.0", "@ethereumjs/block@^3.6.2": +"@ethereumjs/block@^3.5.0", "@ethereumjs/block@^3.6.0", "@ethereumjs/block@^3.6.2": version "3.6.2" resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-3.6.2.tgz#63d1e26d0b7a7a3684fce920de6ebabec1e5b674" integrity sha512-mOqYWwMlAZpYUEOEqt7EfMFuVL2eyLqWWIzcf4odn6QgXY8jBI2NhVuJncrMCKeMZrsJAe7/auaRRB6YcdH+Qw== @@ -288,7 +300,7 @@ ethereumjs-util "^7.1.4" merkle-patricia-tree "^4.2.4" -"@ethereumjs/blockchain@^5.5.2": +"@ethereumjs/blockchain@^5.5.0", "@ethereumjs/blockchain@^5.5.2": version "5.5.2" resolved "https://registry.yarnpkg.com/@ethereumjs/blockchain/-/blockchain-5.5.2.tgz#1848abd9dc1ee56acf8cec4c84304d7f4667d027" integrity sha512-Jz26iJmmsQtngerW6r5BDFaew/f2mObLrRZo3rskLOx1lmtMZ8+TX/vJexmivrnWgmAsTdNWhlKUYY4thPhPig== @@ -310,7 +322,15 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.0" -"@ethereumjs/common@^2.6.3", "@ethereumjs/common@^2.6.4": +"@ethereumjs/common@^2.6.0", "@ethereumjs/common@^2.6.3": + version "2.6.3" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.3.tgz#39ddece7300b336276bad6c02f6a9f1a082caa05" + integrity sha512-mQwPucDL7FDYIg9XQ8DL31CnIYZwGhU5hyOO5E+BMmT71G0+RHvIT5rIkLBirJEKxV6+Rcf9aEIY0kXInxUWpQ== + dependencies: + crc-32 "^1.2.0" + ethereumjs-util "^7.1.4" + +"@ethereumjs/common@^2.6.4": version "2.6.4" resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.4.tgz#1b3cdd3aa4ee3b0ca366756fc35e4a03022a01cc" integrity sha512-RDJh/R/EAr+B7ZRg5LfJ0BIpf/1LydFgYdvZEuTraojCbVypO2sQ+QnpP5u2wJf9DASyooKqu8O4FJEWUV6NXw== @@ -337,7 +357,7 @@ "@ethereumjs/common" "^2.4.0" ethereumjs-util "^7.1.0" -"@ethereumjs/tx@^3.5.1": +"@ethereumjs/tx@^3.4.0", "@ethereumjs/tx@^3.5.1": version "3.5.1" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.1.tgz#8d941b83a602b4a89949c879615f7ea9a90e6671" integrity sha512-xzDrTiu4sqZXUcaBxJ4n4W5FrppwxLxZB4ZDGVLtxSQR4lVuOnFR6RcUHdg1mpUhAPVrmnzLJpxaeXnPxIyhWA== @@ -345,6 +365,24 @@ "@ethereumjs/common" "^2.6.3" ethereumjs-util "^7.1.4" +"@ethereumjs/vm@^5.6.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.8.0.tgz#c9055f96afc13dd7b72893b57fa20027effea6fe" + integrity sha512-mn2G2SX79QY4ckVvZUfxlNUpzwT2AEIkvgJI8aHoQaNYEHhH8rmdVDIaVVgz6//PjK52BZsK23afz+WvSR0Qqw== + dependencies: + "@ethereumjs/block" "^3.6.2" + "@ethereumjs/blockchain" "^5.5.2" + "@ethereumjs/common" "^2.6.3" + "@ethereumjs/tx" "^3.5.1" + async-eventemitter "^0.2.4" + core-js-pure "^3.0.1" + debug "^4.3.3" + ethereumjs-util "^7.1.4" + functional-red-black-tree "^1.0.1" + mcl-wasm "^0.7.1" + merkle-patricia-tree "^4.2.4" + rustbn.js "~0.2.0" + "@ethereumjs/vm@^5.9.0": version "5.9.0" resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.9.0.tgz#54e485097c6dbb42554d541ef8d84d06b7ddf12f" @@ -869,9 +907,9 @@ integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== "@metamask/eth-sig-util@^4.0.0": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088" - integrity sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ== + version "4.0.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.0.tgz#11553ba06de0d1352332c1bde28c8edd00e0dcf6" + integrity sha512-LczOjjxY4A7XYloxzyxJIHONELmUxVZncpOLoClpEcTiebiVdM46KRPYXGuULro9oNNR2xdVx3yoKiQjdfWmoA== dependencies: ethereumjs-abi "^0.6.8" ethereumjs-util "^6.2.1" @@ -905,6 +943,27 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomiclabs/ethereumjs-vm@^4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc" + integrity sha512-8WmX94mMcJaZ7/m7yBbyuS6B+wuOul+eF+RY9fBpGhNaUpyMR/vFIcDojqcWQ4Yafe1tMKY5LDu2yfT4NZgV4Q== + dependencies: + async "^2.1.2" + async-eventemitter "^0.2.2" + core-js-pure "^3.0.1" + ethereumjs-account "^3.0.0" + ethereumjs-block "^2.2.2" + ethereumjs-blockchain "^4.0.3" + ethereumjs-common "^1.5.0" + ethereumjs-tx "^2.1.2" + ethereumjs-util "^6.2.0" + fake-merkle-patricia-tree "^1.0.1" + functional-red-black-tree "^1.0.1" + merkle-patricia-tree "3.0.0" + rustbn.js "~0.2.0" + safe-buffer "^5.1.1" + util.promisify "^1.0.0" + "@nomiclabs/hardhat-ethers@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.2.tgz#c472abcba0c5185aaa4ad4070146e95213c68511" @@ -1761,6 +1820,13 @@ anymatch@~3.1.1, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arbos-precompiles@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arbos-precompiles/-/arbos-precompiles-1.0.2.tgz#7bebd5963aef972cd259eb41f3116ea065013ea6" + integrity sha512-1dOFYFJUN0kKoofh6buZJ8qCqTs+oLGSsGzHI0trA/Pka/TCERflCRsNVxez2lihOvK7MT/a2RA8AepKtBXdPQ== + dependencies: + hardhat "^2.6.4" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -3857,7 +3923,7 @@ diff@3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -diff@5.0.0: +diff@5.0.0, diff@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== @@ -4755,7 +4821,7 @@ ethereumjs-util@^7.0.10, ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereu ethjs-util "0.1.6" rlp "^2.2.4" -ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.4: +ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.3, ethereumjs-util@^7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.4.tgz#a6885bcdd92045b06f596c7626c3e89ab3312458" integrity sha512-p6KmuPCX4mZIqsQzXfmSx9Y0l2hqf+VkAiwSisW3UKUFdk8ZkAt+AYaor83z2nSi6CU2zSsXMlD80hAbNEGM0A== @@ -5938,6 +6004,60 @@ hardhat-tracer@^1.0.0-alpha.6: dependencies: ethers "^5.0.24" +hardhat@^2.6.4: + version "2.9.2" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.9.2.tgz#123f3fed6810ef8637b127b73ca44bb9c9efc249" + integrity sha512-elTcUK1EdFverWinybQ+DoJzsM6sgiHUYs0ZYNNXMfESty6ESHiFSwkfJsC88/q09vmIz6YVaMh73BYnYd+feQ== + dependencies: + "@ethereumjs/block" "^3.6.0" + "@ethereumjs/blockchain" "^5.5.0" + "@ethereumjs/common" "^2.6.0" + "@ethereumjs/tx" "^3.4.0" + "@ethereumjs/vm" "^5.6.0" + "@ethersproject/abi" "^5.1.2" + "@metamask/eth-sig-util" "^4.0.0" + "@sentry/node" "^5.18.1" + "@solidity-parser/parser" "^0.14.1" + "@types/bn.js" "^5.1.0" + "@types/lru-cache" "^5.1.0" + abort-controller "^3.0.0" + adm-zip "^0.4.16" + aggregate-error "^3.0.0" + ansi-escapes "^4.3.0" + chalk "^2.4.2" + chokidar "^3.4.0" + ci-info "^2.0.0" + debug "^4.1.1" + enquirer "^2.3.0" + env-paths "^2.2.0" + ethereum-cryptography "^0.1.2" + ethereumjs-abi "^0.6.8" + ethereumjs-util "^7.1.3" + find-up "^2.1.0" + fp-ts "1.19.3" + fs-extra "^7.0.1" + glob "^7.1.3" + immutable "^4.0.0-rc.12" + io-ts "1.10.4" + lodash "^4.17.11" + merkle-patricia-tree "^4.2.2" + mnemonist "^0.38.0" + mocha "^9.2.0" + p-map "^4.0.0" + qs "^6.7.0" + raw-body "^2.4.1" + resolve "1.17.0" + semver "^6.3.0" + slash "^3.0.0" + solc "0.7.3" + source-map-support "^0.5.13" + stacktrace-parser "^0.1.10" + "true-case-path" "^2.2.1" + tsort "0.0.1" + undici "^4.14.1" + uuid "^8.3.2" + ws "^7.4.6" + hardhat@^2.9.5: version "2.9.5" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.9.5.tgz#6814a9f3afd5630ffe6bcb05a4367eb259c9222a" @@ -7610,6 +7730,16 @@ lodash.get@^4: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + +lodash.isequalwith@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz#266726ddd528f854f21f4ea98a065606e0fbc6b0" + integrity sha1-Jmcm3dUo+FTyH06pigZWBuD7xrA= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -7910,7 +8040,7 @@ merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.3.2: rlp "^2.0.0" semaphore ">=1.0.1" -merkle-patricia-tree@^4.2.4: +merkle-patricia-tree@^4.2.2, merkle-patricia-tree@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/merkle-patricia-tree/-/merkle-patricia-tree-4.2.4.tgz#ff988d045e2bf3dfa2239f7fabe2d59618d57413" integrity sha512-eHbf/BG6eGNsqqfbLED9rIqbsF4+sykEaBn6OLNs71tjclbMcMOk1tEPmJKcNcNCLkvbpY/lwyOlizWsqPNo8w== From 0495b9ca6a99969a4a38d0f5cbab38de6ba8b5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 12 May 2022 18:26:19 -0300 Subject: [PATCH 02/78] feat: allow callhooks from whitelisted addresses in the Arbitrum GRT bridge --- contracts/gateway/L1GraphTokenGateway.sol | 31 ++++- contracts/l2/gateway/L2GraphTokenGateway.sol | 49 ++++++- test/gateway/l1GraphTokenGateway.test.ts | 75 +++++++++-- test/l2/l2GraphTokenGateway.test.ts | 130 ++++++++++++++++--- 4 files changed, 256 insertions(+), 29 deletions(-) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index a1503eb8d..5e1304b08 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -30,6 +30,8 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { address public l2Counterpart; // Address of the BridgeEscrow contract that holds the GRT in the bridge address public escrow; + // Address of the L1 Reservoir that is the only sender allowed to send extra data + mapping(address => bool) public callhookWhitelist; // Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 event DepositInitiated( @@ -57,6 +59,10 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { event L2CounterpartAddressSet(address _l2Counterpart); // Emitted when the escrow address has been updated event EscrowAddressSet(address _escrow); + // Emitted when an address is added to the callhook whitelist + event AddedToCallhookWhitelist(address newWhitelisted); + // Emitted when an address is removed from the callhook whitelist + event RemovedFromCallhookWhitelist(address notWhitelisted); /** * @dev Allows a function to be called only by the gateway's L2 counterpart. @@ -122,6 +128,26 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { emit EscrowAddressSet(_escrow); } + /** + * @dev Adds an address to the callhook whitelist. + * This address will be allowed to include callhooks when transferring tokens. + * @param newWhitelisted Address to add to the whitelist + */ + function addToCallhookWhitelist(address newWhitelisted) external onlyGovernor { + callhookWhitelist[newWhitelisted] = true; + emit AddedToCallhookWhitelist(newWhitelisted); + } + + /** + * @dev Removes an address from the callhook whitelist. + * This address will no longer be allowed to include callhooks when transferring tokens. + * @param notWhitelisted Address to remove from the whitelist + */ + function removeFromCallhookWhitelist(address notWhitelisted) external onlyGovernor { + callhookWhitelist[notWhitelisted] = false; + emit RemovedFromCallhookWhitelist(notWhitelisted); + } + /** * @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox. * The tokens are escrowed by the gateway until they are withdrawn back to L1. @@ -156,7 +182,10 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { { bytes memory extraData; (from, maxSubmissionCost, extraData) = parseOutboundData(_data); - require(extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); + require( + extraData.length == 0 || callhookWhitelist[msg.sender] == true, + "CALL_HOOK_DATA_NOT_ALLOWED" + ); require(maxSubmissionCost > 0, "NO_SUBMISSION_COST"); { diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 94d87f279..262669234 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -30,7 +30,8 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { address public l1Counterpart; // Address of the Arbitrum Gateway Router on L2 address public l2Router; - + // Addresses in L1 that are whitelisted to have callhooks on transfers + mapping(address => bool) public callhookWhitelist; // Calldata included in an outbound transfer, stored as a structure for convenience and stack depth struct OutboundCalldata { address from; @@ -61,6 +62,12 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { event L1TokenAddressSet(address _l1GRT); // Emitted when the address of the counterpart gateway on L1 has been updated event L1CounterpartAddressSet(address _l1Counterpart); + // Emitted when an address is added to the callhook whitelist + event AddedToCallhookWhitelist(address newWhitelisted); + // Emitted when an address is removed from the callhook whitelist + event RemovedFromCallhookWhitelist(address notWhitelisted); + // Emitted when a callhook call failed + event CallhookFailed(address destination); /** * @dev Checks that the sender is the L2 alias of the counterpart @@ -108,6 +115,26 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { emit L1CounterpartAddressSet(_l1Counterpart); } + /** + * @dev Adds an L1 address to the callhook whitelist. + * This address will be allowed to include callhooks when transferring tokens. + * @param newWhitelisted Address to add to the whitelist + */ + function addToCallhookWhitelist(address newWhitelisted) external onlyGovernor { + callhookWhitelist[newWhitelisted] = true; + emit AddedToCallhookWhitelist(newWhitelisted); + } + + /** + * @dev Removes an L1 address from the callhook whitelist. + * This address will no longer be allowed to include callhooks when transferring tokens. + * @param notWhitelisted Address to remove from the whitelist + */ + function removeFromCallhookWhitelist(address notWhitelisted) external onlyGovernor { + callhookWhitelist[notWhitelisted] = false; + emit RemovedFromCallhookWhitelist(notWhitelisted); + } + /** * @notice Burns L2 tokens and initiates a transfer to L1. * The tokens will be available on L1 only after the wait period (7 days) is over, @@ -196,17 +223,35 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @param _from Address of the sender on L1 * @param _to Recipient address on L2 * @param _amount Amount of tokens transferred + * @param _data Extra callhook data, only used when the sender is whitelisted */ function finalizeInboundTransfer( address _l1Token, address _from, address _to, uint256 _amount, - bytes calldata // _data unused in L2 + bytes calldata _data ) external payable override notPaused onlyL1Counterpart { require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); require(msg.value == 0, "INVALID_NONZERO_VALUE"); + if (_data.length > 0 && callhookWhitelist[_from] == true) { + bytes memory callhookData; + { + bytes memory gatewayData; + (gatewayData, callhookData) = abi.decode(_data, (bytes, bytes)); + } + bool success; + // solhint-disable-next-line avoid-low-level-calls + (success, ) = _to.call(callhookData); + // Callhooks shouldn't revert, but if they do: + // we revert, so that the retryable ticket can be re-attempted + // later. + if (!success) { + revert("CALLHOOK_FAILED"); + } + } + L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount); emit DepositFinalized(_l1Token, _from, _to, _amount); diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index 61ee8ce02..6e69acf7a 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -186,6 +186,48 @@ describe('L1GraphTokenGateway', () => { expect(await l1GraphTokenGateway.escrow()).eq(bridgeEscrow.address) }) }) + describe('addToCallhookWhitelist', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .addToCallhookWhitelist(tokenSender.address) + await expect(tx).revertedWith('Caller must be Controller governor') + expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) + }) + it('adds an address to the callhook whitelist', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'AddedToCallhookWhitelist') + .withArgs(tokenSender.address) + expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) + }) + }) + describe('removeFromCallhookWhitelist', function () { + it('is not callable by addreses that are not the governor', async function () { + await l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .removeFromCallhookWhitelist(tokenSender.address) + await expect(tx).revertedWith('Caller must be Controller governor') + expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) + }) + it('removes an address from the callhook whitelist', async function () { + await l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + const tx = l1GraphTokenGateway + .connect(governor.signer) + .removeFromCallhookWhitelist(tokenSender.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'RemovedFromCallhookWhitelist') + .withArgs(tokenSender.address) + expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) + }) + }) describe('Pausable behavior', () => { it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { let tx = l1GraphTokenGateway.connect(tokenSender.signer).setPaused(false) @@ -230,7 +272,7 @@ describe('L1GraphTokenGateway', () => { }) context('> after configuring and unpausing', function () { - const createMsgData = function () { + const createMsgData = function (callHookData: string) { const selector = l1GraphTokenGateway.interface.getSighash('finalizeInboundTransfer') const params = utils.defaultAbiCoder.encode( ['address', 'address', 'address', 'uint256', 'bytes'], @@ -239,7 +281,7 @@ describe('L1GraphTokenGateway', () => { tokenSender.address, l2Receiver.address, toGRT('10'), - utils.defaultAbiCoder.encode(['bytes', 'bytes'], [emptyCallHookData, emptyCallHookData]), + utils.defaultAbiCoder.encode(['bytes', 'bytes'], [emptyCallHookData, callHookData]), ], ) const outboundData = utils.hexlify(utils.concat([selector, params])) @@ -283,7 +325,11 @@ describe('L1GraphTokenGateway', () => { ) return expectedInboxAccsEntry } - const testValidOutboundTransfer = async function (signer: Signer, data: string) { + const testValidOutboundTransfer = async function ( + signer: Signer, + data: string, + callHookData: string, + ) { const tx = l1GraphTokenGateway .connect(signer) .outboundTransfer(grt.address, l2Receiver.address, toGRT('10'), maxGas, gasPriceBid, data, { @@ -295,7 +341,7 @@ describe('L1GraphTokenGateway', () => { .emit(l1GraphTokenGateway, 'DepositInitiated') .withArgs(grt.address, tokenSender.address, l2Receiver.address, expectedSeqNum, toGRT('10')) - const msgData = createMsgData() + const msgData = createMsgData(callHookData) const msgDataHash = utils.keccak256(msgData) const expectedInboxAccsEntry = createInboxAccsEntry(msgDataHash) @@ -365,7 +411,7 @@ describe('L1GraphTokenGateway', () => { }) it('puts tokens in escrow and creates a retryable ticket', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender.signer, defaultData) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) }) it('decodes the sender address from messages sent by the router', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) @@ -373,7 +419,7 @@ describe('L1GraphTokenGateway', () => { ['address', 'bytes'], [tokenSender.address, defaultData], ) - await testValidOutboundTransfer(mockRouter.signer, routerEncodedData) + await testValidOutboundTransfer(mockRouter.signer, routerEncodedData, emptyCallHookData) }) it('reverts when called with the wrong value', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) @@ -392,7 +438,7 @@ describe('L1GraphTokenGateway', () => { ) await expect(tx).revertedWith('WRONG_ETH_VALUE') }) - it('reverts when called with nonempty calldata', async function () { + it('reverts when called with nonempty calldata, if the sender is not whitelisted', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) const tx = l1GraphTokenGateway .connect(tokenSender.signer) @@ -409,6 +455,17 @@ describe('L1GraphTokenGateway', () => { ) await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED') }) + it('allows sending nonempty calldata, if the sender is whitelisted', async function () { + await l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer( + tokenSender.signer, + defaultDataWithNotEmptyCallHookData, + notEmptyCallHookData, + ) + }) it('reverts when the sender does not have enough GRT', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('1001')) const tx = l1GraphTokenGateway @@ -503,7 +560,7 @@ describe('L1GraphTokenGateway', () => { }) it('reverts if the gateway is revoked from escrow', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender.signer, defaultData) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) // At this point, the gateway holds 10 GRT in escrow // But we revoke the gateway's permission to move the funds: await bridgeEscrow.connect(governor.signer).revokeAll(l1GraphTokenGateway.address) @@ -538,7 +595,7 @@ describe('L1GraphTokenGateway', () => { }) it('sends tokens out of escrow', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) - await testValidOutboundTransfer(tokenSender.signer, defaultData) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) // At this point, the gateway holds 10 GRT in escrow const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( 'finalizeInboundTransfer', diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index a2b0e04b7..36b63a33a 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -1,5 +1,5 @@ import { expect, use } from 'chai' -import { constants, Signer, utils } from 'ethers' +import { constants, ContractTransaction, Signer, utils } from 'ethers' import hre from 'hardhat' import { L2GraphToken } from '../../build/types/L2GraphToken' @@ -9,9 +9,12 @@ import { NetworkFixture } from '../lib/fixtures' import { FakeContract, smock } from '@defi-wonderland/smock' +import rewardsManagerMockAbi from '../../build/abis/RewardsManagerMock.json' + use(smock.matchers) -import { getAccounts, toGRT, Account, provider, applyL1ToL2Alias } from '../lib/testHelpers' +import { getAccounts, toGRT, Account, provider, applyL1ToL2Alias, toBN } from '../lib/testHelpers' +import { Interface } from 'ethers/lib/utils' const { AddressZero } = constants @@ -43,10 +46,11 @@ describe('L2GraphTokenGateway', () => { const senderTokens = toGRT('1000') const defaultData = '0x' - const notEmptyCallHookData = '0x12' + const rmmIface = new Interface(rewardsManagerMockAbi) + const notEmptyCallHookData = rmmIface.encodeFunctionData('pow', [toBN(1), toBN(2), toBN(3)]) const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( - ['bytes'], - [notEmptyCallHookData], + ['bytes', 'bytes'], + ['0x', notEmptyCallHookData], ) before(async function () { @@ -161,6 +165,48 @@ describe('L2GraphTokenGateway', () => { expect(await l2GraphTokenGateway.l1Counterpart()).eq(mockL1Gateway.address) }) }) + describe('addToCallhookWhitelist', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .addToCallhookWhitelist(tokenSender.address) + await expect(tx).revertedWith('Caller must be Controller governor') + expect(await l2GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) + }) + it('adds an address to the callhook whitelist', async function () { + const tx = l2GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + await expect(tx) + .emit(l2GraphTokenGateway, 'AddedToCallhookWhitelist') + .withArgs(tokenSender.address) + expect(await l2GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) + }) + }) + describe('removeFromCallhookWhitelist', function () { + it('is not callable by addreses that are not the governor', async function () { + await l2GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .removeFromCallhookWhitelist(tokenSender.address) + await expect(tx).revertedWith('Caller must be Controller governor') + expect(await l2GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) + }) + it('removes an address from the callhook whitelist', async function () { + await l2GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + const tx = l2GraphTokenGateway + .connect(governor.signer) + .removeFromCallhookWhitelist(tokenSender.address) + await expect(tx) + .emit(l2GraphTokenGateway, 'RemovedFromCallhookWhitelist') + .withArgs(tokenSender.address) + expect(await l2GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) + }) + }) describe('Pausable behavior', () => { it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { let tx = l2GraphTokenGateway.connect(tokenSender.signer).setPaused(false) @@ -322,6 +368,37 @@ describe('L2GraphTokenGateway', () => { }) describe('finalizeInboundTransfer', function () { + const testValidFinalizeTransfer = async function ( + data: string, + ): Promise { + const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) + await me.signer.sendTransaction({ + to: await mockL1GatewayL2Alias.getAddress(), + value: utils.parseUnits('1', 'ether'), + }) + const tx = l2GraphTokenGateway + .connect(mockL1GatewayL2Alias) + .finalizeInboundTransfer( + mockL1GRT.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + data, + ) + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, tokenSender.address, l2Receiver.address, toGRT('10')) + + await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Receiver.address, toGRT('10')) + + // Unchanged + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(senderBalance).eq(toGRT('1000')) + // 10 newly minted GRT + const receiverBalance = await grt.balanceOf(l2Receiver.address) + await expect(receiverBalance).eq(toGRT('10')) + return tx + } it('reverts when called by an account that is not the gateway', async function () { const tx = l2GraphTokenGateway .connect(tokenSender.signer) @@ -347,6 +424,35 @@ describe('L2GraphTokenGateway', () => { await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') }) it('mints and sends tokens when called by the aliased gateway', async function () { + await testValidFinalizeTransfer(defaultData) + }) + it('does not call any callhooks if the sender is not whitelisted', async function () { + const rewardsManagerMock = await smock.fake('RewardsManagerMock', { + address: l2Receiver.address, + }) + rewardsManagerMock.pow.returns(1) + await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData) + expect(rewardsManagerMock.pow).to.not.have.been.called + }) + it('calls a callhook if the sender is whitelisted', async function () { + const rewardsManagerMock = await smock.fake('RewardsManagerMock', { + address: l2Receiver.address, + }) + rewardsManagerMock.pow.returns(1) + await l2GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData) + expect(rewardsManagerMock.pow).to.have.been.calledWith(toBN(1), toBN(2), toBN(3)) + }) + it('reverts if a callhook reverts', async function () { + const rewardsManagerMock = await smock.fake('RewardsManagerMock', { + address: l2Receiver.address, + }) + rewardsManagerMock.pow.reverts() + await l2GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) await me.signer.sendTransaction({ to: await mockL1GatewayL2Alias.getAddress(), @@ -359,20 +465,10 @@ describe('L2GraphTokenGateway', () => { tokenSender.address, l2Receiver.address, toGRT('10'), - defaultData, + defaultDataWithNotEmptyCallHookData, ) await expect(tx) - .emit(l2GraphTokenGateway, 'DepositFinalized') - .withArgs(mockL1GRT.address, tokenSender.address, l2Receiver.address, toGRT('10')) - - await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Receiver.address, toGRT('10')) - - // Unchanged - const senderBalance = await grt.balanceOf(tokenSender.address) - await expect(senderBalance).eq(toGRT('1000')) - // 10 newly minted GRT - const receiverBalance = await grt.balanceOf(l2Receiver.address) - await expect(receiverBalance).eq(toGRT('10')) + .revertedWith('CALLHOOK_FAILED') }) }) }) From 359d2547deb1b2f62064da3d77a104a115b5531f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 6 Jun 2022 13:54:55 -0700 Subject: [PATCH 03/78] fix: call bridge callhooks after minting tokens --- contracts/l2/gateway/L2GraphTokenGateway.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 262669234..b639e2374 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -235,6 +235,8 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); require(msg.value == 0, "INVALID_NONZERO_VALUE"); + L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount); + if (_data.length > 0 && callhookWhitelist[_from] == true) { bytes memory callhookData; { @@ -252,8 +254,6 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { } } - L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount); - emit DepositFinalized(_l1Token, _from, _to, _amount); } From 00e7d3a25e08b18e5251f3b22106e2c71da9b2c4 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Tue, 5 Apr 2022 10:55:39 -0300 Subject: [PATCH 04/78] feat: add commands to configure and use the Arbitrum bridge on the CLI We also add Goerli and Arbitrum Nitro Devnet configurations (though the CLI commands don't support Nitro yet). --- arbitrum-addresses.json | 42 + cli/address-book.ts | 2 +- cli/cli.ts | 3 + cli/commands/bridge/index.ts | 22 + cli/commands/bridge/to-l1.ts | 217 + cli/commands/bridge/to-l2.ts | 179 + cli/commands/migrate.ts | 5 +- cli/commands/protocol/configure-bridge.ts | 89 + cli/commands/protocol/get.ts | 12 + cli/commands/protocol/index.ts | 8 +- cli/commands/protocol/list.ts | 9 +- cli/commands/protocol/set.ts | 25 + cli/contracts.ts | 34 +- cli/defaults.ts | 16 + cli/env.ts | 2 +- cli/network.ts | 3 +- cli/utils.ts | 18 + contracts/arbitrum/NodeInterface.sol | 76 + .../tests/arbitrum/ArbRetryableTxStub.sol | 9 + hardhat.config.ts | 2 + package.json | 5 +- tasks/gre.ts | 3 +- test/l2/l2GraphTokenGateway.test.ts | 9 +- yarn.lock | 4505 ++++++++++------- 24 files changed, 3306 insertions(+), 1989 deletions(-) create mode 100644 arbitrum-addresses.json create mode 100644 cli/commands/bridge/index.ts create mode 100644 cli/commands/bridge/to-l1.ts create mode 100644 cli/commands/bridge/to-l2.ts create mode 100644 cli/commands/protocol/configure-bridge.ts create mode 100644 contracts/arbitrum/NodeInterface.sol create mode 100644 contracts/tests/arbitrum/ArbRetryableTxStub.sol diff --git a/arbitrum-addresses.json b/arbitrum-addresses.json new file mode 100644 index 000000000..5a4655cf2 --- /dev/null +++ b/arbitrum-addresses.json @@ -0,0 +1,42 @@ +{ + "source": "https://github.com/OffchainLabs/arbitrum/tree/f54baf10871ee86aedca4880796342ef9bd0b0ab/packages/", + "1": { + "L1GatewayRouter": { + "address": "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef" + }, + "IInbox": { + "address": "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f" + } + }, + "4": { + "L1GatewayRouter": { + "address": "0x70C143928eCfFaf9F5b406f7f4fC28Dc43d68380" + }, + "IInbox": { + "address": "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e" + } + }, + "5": { + "L1GatewayRouter": { + "address": "0x8BDFa67ace22cE2BFb2fFebe72f0c91CDA694d4b" + }, + "IInbox": { + "address": "0x1FdBBcC914e84aF593884bf8e8Dd6877c29035A2" + } + }, + "42161": { + "L2GatewayRouter": { + "address": "0x5288c571Fd7aD117beA99bF60FE0846C4E84F933" + } + }, + "421611": { + "L2GatewayRouter": { + "address": "0x9413AD42910c1eA60c737dB5f58d1C504498a3cD" + } + }, + "421612": { + "L2GatewayRouter": { + "address": "0xC502Ded1EE1d616B43F7f20Ebde83Be1A275ca3c" + } + } +} diff --git a/cli/address-book.ts b/cli/address-book.ts index 39818c82a..5c938fe2a 100644 --- a/cli/address-book.ts +++ b/cli/address-book.ts @@ -28,7 +28,7 @@ export interface AddressBook { } export const getAddressBook = (path: string, chainId: string): AddressBook => { - if (!path) throw new Error(`A path the the address book file is required.`) + if (!path) throw new Error(`A path to the address book file is required.`) if (!chainId) throw new Error(`A chainId is required.`) const addressBook = JSON.parse(fs.readFileSync(path, 'utf8') || '{}') as AddressBookJson diff --git a/cli/cli.ts b/cli/cli.ts index 097846c7f..c94a95b40 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -8,6 +8,7 @@ import { proxyCommand } from './commands/proxy' import { protocolCommand } from './commands/protocol' import { contractsCommand } from './commands/contracts' import { airdropCommand } from './commands/airdrop' +import { bridgeCommand } from './commands/bridge' import { cliOpts } from './defaults' @@ -27,11 +28,13 @@ yargs .option('m', cliOpts.mnemonic) .option('p', cliOpts.providerUrl) .option('n', cliOpts.accountNumber) + .option('r', cliOpts.arbitrumAddressBook) .command(deployCommand) .command(migrateCommand) .command(proxyCommand) .command(protocolCommand) .command(contractsCommand) .command(airdropCommand) + .command(bridgeCommand) .demandCommand(1, 'Choose a command from the above list') .help().argv diff --git a/cli/commands/bridge/index.ts b/cli/commands/bridge/index.ts new file mode 100644 index 000000000..fcdf1bcc1 --- /dev/null +++ b/cli/commands/bridge/index.ts @@ -0,0 +1,22 @@ +import yargs, { Argv } from 'yargs' + +import { redeemSendToL2Command, sendToL2Command } from './to-l2' +import { startSendToL1Command, finishSendToL1Command, waitFinishSendToL1Command } from './to-l1' +import { cliOpts } from '../../defaults' + +export const bridgeCommand = { + command: 'bridge', + describe: 'Graph token bridge actions.', + builder: (yargs: Argv): yargs.Argv => { + return yargs + .option('-l', cliOpts.l2ProviderUrl) + .command(sendToL2Command) + .command(redeemSendToL2Command) + .command(startSendToL1Command) + .command(finishSendToL1Command) + .command(waitFinishSendToL1Command) + }, + handler: (): void => { + yargs.showHelp() + }, +} diff --git a/cli/commands/bridge/to-l1.ts b/cli/commands/bridge/to-l1.ts new file mode 100644 index 000000000..ffee9dbe8 --- /dev/null +++ b/cli/commands/bridge/to-l1.ts @@ -0,0 +1,217 @@ +import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' +import { logger } from '../../logging' +import { getAddressBook } from '../../address-book' +import { getProvider, sendTransaction, toGRT } from '../../network' +import { chainIdIsL2 } from '../../utils' +import { loadAddressBookContract } from '../../contracts' +import { + L2TransactionReceipt, + getL2Network, + L2ToL1MessageStatus, + L2ToL1MessageWriter, +} from '@arbitrum/sdk' +import { L2GraphTokenGateway } from '../../../build/types/L2GraphTokenGateway' +import { BigNumber } from 'ethers' +import { JsonRpcProvider } from '@ethersproject/providers' + +const FOURTEEN_DAYS_IN_SECONDS = 24 * 3600 * 14 + +const BLOCK_SEARCH_THRESHOLD = 6 * 3600 +const searchForArbBlockByTimestamp = async ( + l2Provider: JsonRpcProvider, + timestamp: number, +): Promise => { + let step = 131072 + let block = await l2Provider.getBlock('latest') + while (block.timestamp > timestamp) { + while (block.number - step < 0) { + step = Math.round(step / 2) + } + block = await l2Provider.getBlock(block.number - step) + } + while (step > 1 && Math.abs(block.timestamp - timestamp) > BLOCK_SEARCH_THRESHOLD) { + step = Math.round(step / 2) + if (block.timestamp - timestamp > 0) { + block = await l2Provider.getBlock(block.number - step) + } else { + block = await l2Provider.getBlock(block.number + step) + } + } + return block.number +} + +const wait = (ms: number): Promise => { + return new Promise((res) => setTimeout(res, ms)) +} + +const waitUntilOutboxEntryCreatedWithCb = async ( + msg: L2ToL1MessageWriter, + retryDelay: number, + callback: () => void, +) => { + let done = false + while (!done) { + const status = await msg.status(null) + if (status == L2ToL1MessageStatus.CONFIRMED) { + done = true + } else { + callback() + await wait(retryDelay) + } + } +} + +export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Sending tokens to L1 <<<\n`) + const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l2ChainId = (await l2Provider.getNetwork()).chainId + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + throw new Error( + 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', + ) + } + + const l1GRT = cli.contracts['GraphToken'] + const l1GRTAddress = l1GRT.address + const amount = toGRT(cliArgs.amount) + const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address + const l2Wallet = cli.wallet.connect(l2Provider) + const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString()) + + const gateway = loadAddressBookContract('L2GraphTokenGateway', l2AddressBook, l2Wallet) + const l2GRT = loadAddressBookContract('L2GraphToken', l2AddressBook, l2Wallet) + + const l1Gateway = cli.contracts['L1GraphTokenGateway'] + logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`) + logger.info(`Using L2 gateway ${gateway.address} and L1 gateway ${l1Gateway.address}`) + + const params = [l1GRTAddress, recipient, amount, '0x'] + logger.info('Approving token transfer') + await sendTransaction(l2Wallet, l2GRT, 'approve', [gateway.address, amount]) + logger.info('Sending outbound transfer transaction') + const receipt = await sendTransaction( + l2Wallet, + gateway, + 'outboundTransfer(address,address,uint256,bytes)', + params, + ) + const l2Receipt = new L2TransactionReceipt(receipt) + const l2ToL1Message = ( + await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider)) + )[0] + + logger.info( + `The transaction generated an outbox message with batch number ${l2ToL1Message.batchNumber}`, + ) + logger.info(`and index in batch ${l2ToL1Message.indexInBatch}.`) + logger.info( + `After the dispute period is finalized (in ~1 week), you can finalize this by calling`, + ) + logger.info(`finish-send-to-l1 with the following txhash:`) + logger.info(l2Receipt.transactionHash) +} + +export const finishSendToL1 = async ( + cli: CLIEnvironment, + cliArgs: CLIArgs, + wait: boolean, +): Promise => { + logger.info(`>>> Finishing transaction sending tokens to L1 <<<\n`) + const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l2ChainId = (await l2Provider.getNetwork()).chainId + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + throw new Error( + 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', + ) + } + + const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString()) + + const gateway = loadAddressBookContract( + 'L2GraphTokenGateway', + l2AddressBook, + l2Provider, + ) as L2GraphTokenGateway + let txHash: string + if (cliArgs.txHash) { + txHash = cliArgs.txHash + } else { + logger.info( + `Looking for withdrawals initiated by ${cli.wallet.address} in roughly the last 14 days`, + ) + const fromBlock = await searchForArbBlockByTimestamp( + l2Provider, + Math.round(Date.now() / 1000) - FOURTEEN_DAYS_IN_SECONDS, + ) + const filt = gateway.filters.WithdrawalInitiated(null, cli.wallet.address) + const allEvents = await gateway.queryFilter(filt, BigNumber.from(fromBlock).toHexString()) + if (allEvents.length == 0) { + throw new Error('No withdrawals found') + } + txHash = allEvents[allEvents.length - 1].transactionHash + } + logger.info(`Getting receipt from transaction ${txHash}`) + const receipt = await l2Provider.getTransactionReceipt(txHash) + + const l2Receipt = new L2TransactionReceipt(receipt) + logger.info(`Getting L2 to L1 message...`) + const l2ToL1Message = ( + await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider)) + )[0] + + if (wait) { + const retryDelayMs = cliArgs.retryDelaySeconds ? cliArgs.retryDelaySeconds * 1000 : 60000 + logger.info('Waiting for outbox entry to be created, this can take a full week...') + await waitUntilOutboxEntryCreatedWithCb(l2ToL1Message, retryDelayMs, () => { + logger.info('Still waiting...') + }) + } else { + logger.info('Checking if L2 to L1 message is confirmed...') + const status = await l2ToL1Message.status(null) + if (status != L2ToL1MessageStatus.CONFIRMED) { + throw new Error( + `Transaction is not confirmed, status is ${status} when it should be ${L2ToL1MessageStatus.CONFIRMED}. Has the dispute period passed?`, + ) + } + } + logger.info('Getting proof to execute message') + const proofInfo = await l2ToL1Message.tryGetProof(l2Provider) + + if (await l2ToL1Message.hasExecuted(proofInfo)) { + throw new Error('Message already executed!') + } + + logger.info('Executing outbox transaction') + const tx = await l2ToL1Message.execute(proofInfo) + const outboxExecuteReceipt = await tx.wait() + logger.info('Transaction succeeded! tx hash:') + logger.info(outboxExecuteReceipt.transactionHash) +} + +export const startSendToL1Command = { + command: 'start-send-to-l1 [recipient]', + describe: 'Start an L2-to-L1 Graph Token transaction', + handler: async (argv: CLIArgs): Promise => { + return startSendToL1(await loadEnv(argv), argv) + }, +} + +export const finishSendToL1Command = { + command: 'finish-send-to-l1 [txHash]', + describe: + 'Finish an L2-to-L1 Graph Token transaction. L2 dispute period must have completed. ' + + 'If txHash is not specified, the last withdrawal from the main account in the past 14 days will be redeemed.', + handler: async (argv: CLIArgs): Promise => { + return finishSendToL1(await loadEnv(argv), argv, false) + }, +} + +export const waitFinishSendToL1Command = { + command: 'wait-finish-send-to-l1 [txHash] [retryDelaySeconds]', + describe: + "Wait for an L2-to-L1 Graph Token transaction's dispute period to complete (which takes about a week), and then finalize it. " + + 'If txHash is not specified, the last withdrawal from the main account in the past 14 days will be redeemed.', + handler: async (argv: CLIArgs): Promise => { + return finishSendToL1(await loadEnv(argv), argv, true) + }, +} diff --git a/cli/commands/bridge/to-l2.ts b/cli/commands/bridge/to-l2.ts new file mode 100644 index 000000000..fd9ac947d --- /dev/null +++ b/cli/commands/bridge/to-l2.ts @@ -0,0 +1,179 @@ +import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' +import { logger } from '../../logging' +import { getContractAt, getProvider, sendTransaction, toGRT } from '../../network' +import { BigNumber, utils } from 'ethers' +import { parseEther } from '@ethersproject/units' +import { + L1TransactionReceipt, + L1ToL2MessageStatus, + getRawArbTransactionReceipt, + L1ToL2MessageWriter, +} from '@arbitrum/sdk' +import { nodeInterfaceAddress, arbRetryableTxAddress, chainIdIsL2 } from '../../utils' +import { JsonRpcProvider } from '@ethersproject/providers' + +const maxSubmissionPriceIncreasePct = 400 +const maxGasIncreasePct = 50 + +const percentIncrease = (val: BigNumber, increase: number): BigNumber => { + return val.add(val.mul(increase).div(100)) +} + +const logAutoRedeemReason = (autoRedeemRec) => { + if (autoRedeemRec == null) { + logger.info(`Auto redeem was not attempted.`) + return + } + switch (autoRedeemRec.returnCode) { + case 1: + logger.info(`Auto redeem reverted.`) + case 2: + logger.info(`Auto redeem failed: hit congestion in the chain.`) + case 8: + logger.info(`Auto redeem failed: exceeded the tx gas limit.`) + case 10: + logger.info(`Auto redeem failed: gas provided is below minimum tx gas.`) + case 11: + logger.info(`Auto redeem failed: L2 gas price is set too low.`) + case 12: + logger.info(`Auto redeem failed: no L2 gas is provided for auto redeem.`) + default: + logger.info(`Auto redeem reverted, unknown code ${autoRedeemRec.returnCode}`) + } +} + +const checkAndRedeemMessage = async ( + l1ToL2Message: L1ToL2MessageWriter, + l2Provider: JsonRpcProvider, +) => { + const res = await l1ToL2Message.waitForStatus() + if (res.status === L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2) { + /** Message wasn't auto-redeemed! */ + logger.warn('Funds were deposited on L2 but the retryable ticket was not redeemed') + const autoRedeemRec = await getRawArbTransactionReceipt(l2Provider, l1ToL2Message.autoRedeemId) + logAutoRedeemReason(autoRedeemRec) + logger.info('Attempting to redeem...') + await l1ToL2Message.redeem() + } else if (res.status != L1ToL2MessageStatus.REDEEMED) { + throw new Error(`Unexpected L1ToL2MessageStatus ${res.status}`) + } + logger.info(`Transfer successful: ${l1ToL2Message.l2TxHash}`) +} + +export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Sending tokens to L2 <<<\n`) + const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l2ChainId = (await l2Provider.getNetwork()).chainId + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + throw new Error( + 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', + ) + } + const gateway = cli.contracts['L1GraphTokenGateway'] + const l1GRT = cli.contracts['GraphToken'] + const l1GRTAddress = l1GRT.address + const amount = toGRT(cliArgs.amount) + const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address + const l2Dest = await gateway.l2Counterpart() + + logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`) + logger.info(`Using L1 gateway ${gateway.address} and L2 gateway ${l2Dest}`) + // See https://github.com/OffchainLabs/arbitrum/blob/master/packages/arb-ts/src/lib/bridge.ts + const depositCalldata = await gateway.getOutboundCalldata( + l1GRTAddress, + cli.wallet.address, + recipient, + amount, + '0x', + ) + + const arbRetryableTx = getContractAt('ArbRetryableTx', arbRetryableTxAddress, l2Provider) + const nodeInterface = getContractAt('NodeInterface', nodeInterfaceAddress, l2Provider) + + logger.info('Estimating retryable ticket submission cost:') + let maxSubmissionPrice = (await arbRetryableTx.getSubmissionPrice(depositCalldata.length - 2))[0] + logger.info( + `maxSubmissionPrice: ${maxSubmissionPrice}, but will accept an increase of up to ${maxSubmissionPriceIncreasePct}%`, + ) + maxSubmissionPrice = percentIncrease(maxSubmissionPrice, maxSubmissionPriceIncreasePct) + + const gasPriceBid = await l2Provider.getGasPrice() + // Comment from Offchain Labs' implementation: + // we add a 0.05 ether "deposit" buffer to pay for execution in the gas estimation + logger.info('Estimating retryable ticket gas:') + let maxGas = ( + await nodeInterface.estimateRetryableTicket( + gateway.address, + parseEther('0.05'), + l2Dest, + parseEther('0'), + maxSubmissionPrice, + cli.wallet.address, + cli.wallet.address, + 0, + gasPriceBid, + depositCalldata, + ) + )[0] + logger.info(`maxGas: ${maxGas}, but will accept an increase of up to ${maxGasIncreasePct}%`) + maxGas = percentIncrease(maxGas, maxGasIncreasePct) + + const ethValue = maxSubmissionPrice.add(gasPriceBid.mul(maxGas)) + logger.info(`tx value: ${ethValue}`) + const data = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionPrice, '0x']) + + const params = [l1GRTAddress, recipient, amount, maxGas, gasPriceBid, data] + logger.info('Approving token transfer') + await sendTransaction(cli.wallet, l1GRT, 'approve', [gateway.address, amount]) + logger.info('Sending outbound transfer transaction') + const receipt = await sendTransaction(cli.wallet, gateway, 'outboundTransfer', params, { + value: ethValue, + }) + const l1Receipt = new L1TransactionReceipt(receipt) + const l1ToL2Message = await l1Receipt.getL1ToL2Message(cli.wallet.connect(l2Provider)) + + logger.info('Waiting for message to propagate to L2...') + try { + await checkAndRedeemMessage(l1ToL2Message, l2Provider) + } catch (e) { + logger.error('Auto redeem failed') + logger.error(e) + logger.error('You can re-attempt using redeem-send-to-l2 with the following txHash:') + logger.error(receipt.transactionHash) + } +} + +export const redeemSendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Redeeming pending tokens on L2 <<<\n`) + const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l2ChainId = (await l2Provider.getNetwork()).chainId + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + throw new Error( + 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', + ) + } + const l1Provider = cli.wallet.provider + + const receipt = await l1Provider.getTransactionReceipt(cliArgs.txHash) + const l1Receipt = new L1TransactionReceipt(receipt) + const l1ToL2Message = await l1Receipt.getL1ToL2Message(cli.wallet.connect(l2Provider)) + + logger.info('Checking message status in L2...') + await checkAndRedeemMessage(l1ToL2Message, l2Provider) +} + +export const sendToL2Command = { + command: 'send-to-l2 [recipient]', + describe: 'Perform an L1-to-L2 Graph Token transaction', + handler: async (argv: CLIArgs): Promise => { + return sendToL2(await loadEnv(argv), argv) + }, +} + +export const redeemSendToL2Command = { + command: 'redeem-send-to-l2 ', + describe: 'Finish an L1-to-L2 Graph Token transaction if it failed to auto-redeem', + handler: async (argv: CLIArgs): Promise => { + return redeemSendToL2(await loadEnv(argv), argv) + }, +} diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 0e538ed77..9a88f0b57 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -11,6 +11,7 @@ import { sendTransaction, } from '../network' import { loadEnv, CLIArgs, CLIEnvironment } from '../env' +import { chainIdIsL2 } from '../utils' const { EtherSymbol } = constants const { formatEther } = utils @@ -59,8 +60,6 @@ let allContracts = [ // But for now we'll only include a subset: const l2Contracts = ['GraphProxyAdmin', 'Controller', 'L2GraphToken', 'L2GraphTokenGateway'] -const l2ChainIds = [42161, 421611] - export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { const graphConfigPath = cliArgs.graphConfig const force = cliArgs.force @@ -69,7 +68,7 @@ export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Setting L1 Bridge Configuration <<<\n`) + + if (chainIdIsL2(cli.chainId)) { + throw new Error('Cannot set L1 configuration on an L2 network!') + } + const l2ChainId = cliArgs.l2ChainId ? cliArgs.l2ChainId : l1ToL2ChainIdMap[cli.chainId] + logger.info('Connecting with the contracts on L2 chainId ' + l2ChainId) + const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId) + const arbAddressBook = getAddressBook(cliArgs.arbAddressBook, cli.chainId.toString()) + + const gateway = cli.contracts['L1GraphTokenGateway'] + + const l2GRT = l2AddressBook.getEntry('L2GraphToken') + logger.info('L2 GRT address: ' + l2GRT.address) + await sendTransaction(cli.wallet, gateway, 'setL2TokenAddress', [l2GRT.address]) + + const l2Counterpart = l2AddressBook.getEntry('L2GraphTokenGateway') + logger.info('L2 Gateway address: ' + l2Counterpart.address) + await sendTransaction(cli.wallet, gateway, 'setL2CounterpartAddress', [l2Counterpart.address]) + + const bridgeEscrow = cli.contracts.BridgeEscrow + logger.info('Escrow address: ' + bridgeEscrow.address) + await sendTransaction(cli.wallet, gateway, 'setEscrowAddress', [bridgeEscrow.address]) + await sendTransaction(cli.wallet, bridgeEscrow, 'approveAll', [gateway.address]) + + const l1Inbox = arbAddressBook.getEntry('IInbox') + const l1Router = arbAddressBook.getEntry('L1GatewayRouter') + logger.info( + 'L1 Inbox address: ' + l1Inbox.address + ' and L1 Router address: ' + l1Router.address, + ) + await sendTransaction(cli.wallet, gateway, 'setArbitrumAddresses', [ + l1Inbox.address, + l1Router.address, + ]) +} + +export const configureL2Bridge = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Setting L2 Bridge Configuration <<<\n`) + + if (!chainIdIsL2(cli.chainId)) { + throw new Error('Cannot set L2 configuration on an L1 network!') + } + const l1ChainId = cliArgs.l1ChainId ? cliArgs.l1ChainId : l2ToL1ChainIdMap[cli.chainId] + logger.info('Connecting with the contracts on L1 chainId ' + l1ChainId) + const l1AddressBook = getAddressBook(cliArgs.addressBook, l1ChainId) + const arbAddressBook = getAddressBook(cliArgs.arbAddressBook, cli.chainId.toString()) + + const gateway = cli.contracts['L2GraphTokenGateway'] + const token = cli.contracts['L2GraphToken'] + + const l1GRT = l1AddressBook.getEntry('GraphToken') + logger.info('L1 GRT address: ' + l1GRT.address) + await sendTransaction(cli.wallet, gateway, 'setL1TokenAddress', [l1GRT.address]) + await sendTransaction(cli.wallet, token, 'setL1Address', [l1GRT.address]) + + const l1Counterpart = l1AddressBook.getEntry('L1GraphTokenGateway') + logger.info('L1 Gateway address: ' + l1Counterpart.address) + await sendTransaction(cli.wallet, gateway, 'setL1CounterpartAddress', [l1Counterpart.address]) + + const l2Router = arbAddressBook.getEntry('L2GatewayRouter') + logger.info('L2 Router address: ' + l2Router.address) + await sendTransaction(cli.wallet, gateway, 'setL2Router', [l2Router.address]) + + logger.info('L2 Gateway address: ' + gateway.address) + await sendTransaction(cli.wallet, token, 'setGateway', [gateway.address]) +} + +export const configureL1BridgeCommand = { + command: 'configure-l1-bridge [l2ChainId]', + describe: 'Configure L1/L2 bridge parameters (L1 side) using the address book', + handler: async (argv: CLIArgs): Promise => { + return configureL1Bridge(await loadEnv(argv), argv) + }, +} + +export const configureL2BridgeCommand = { + command: 'configure-l2-bridge [l1ChainId]', + describe: 'Configure L1/L2 bridge parameters (L2 side) using the address book', + handler: async (argv: CLIArgs): Promise => { + return configureL2Bridge(await loadEnv(argv), argv) + }, +} diff --git a/cli/commands/protocol/get.ts b/cli/commands/protocol/get.ts index 27932d209..439b539b4 100644 --- a/cli/commands/protocol/get.ts +++ b/cli/commands/protocol/get.ts @@ -51,6 +51,18 @@ export const gettersList = { 'controller-get-paused': { contract: 'Controller', name: 'paused' }, 'controller-get-partial-paused': { contract: 'Controller', name: 'partialPaused' }, 'controller-get-pause-guardian': { contract: 'Controller', name: 'pauseGuardian' }, + 'l1-gateway-l2-grt': { contract: 'L1GraphTokenGateway', name: 'l2GRT' }, + 'l1-gateway-inbox': { contract: 'L1GraphTokenGateway', name: 'inbox' }, + 'l1-gateway-escrow': { contract: 'L1GraphTokenGateway', name: 'escrow' }, + 'l1-gateway-l1-router': { contract: 'L1GraphTokenGateway', name: 'l1Router' }, + 'l1-gateway-l2-counterpart': { contract: 'L1GraphTokenGateway', name: 'l2Counterpart' }, + 'l1-gateway-paused': { contract: 'L1GraphTokenGateway', name: 'paused' }, + 'l2-gateway-l1-grt': { contract: 'L2GraphTokenGateway', name: 'l1GRT' }, + 'l2-gateway-l2-router': { contract: 'L2GraphTokenGateway', name: 'l2Router' }, + 'l2-gateway-l1-counterpart': { contract: 'L2GraphTokenGateway', name: 'l1Counterpart' }, + 'l2-gateway-paused': { contract: 'L2GraphTokenGateway', name: 'paused' }, + 'l2-token-gateway': { contract: 'L2GraphToken', name: 'gateway' }, + 'l2-token-l1-address': { contract: 'L2GraphToken', name: 'l1Address' }, } const buildHelp = () => { diff --git a/cli/commands/protocol/index.ts b/cli/commands/protocol/index.ts index 69ca4e4c2..acab2be90 100644 --- a/cli/commands/protocol/index.ts +++ b/cli/commands/protocol/index.ts @@ -3,6 +3,7 @@ import yargs, { Argv } from 'yargs' import { listCommand } from './list' import { getCommand } from './get' import { setCommand } from './set' +import { configureL1BridgeCommand, configureL2BridgeCommand } from './configure-bridge' export interface ProtocolFunction { contract: string @@ -17,7 +18,12 @@ export const protocolCommand = { command: 'protocol', describe: 'Graph protocol configuration', builder: (yargs: Argv): yargs.Argv => { - return yargs.command(getCommand).command(setCommand).command(listCommand) + return yargs + .command(getCommand) + .command(setCommand) + .command(listCommand) + .command(configureL1BridgeCommand) + .command(configureL2BridgeCommand) }, handler: (): void => { yargs.showHelp() diff --git a/cli/commands/protocol/list.ts b/cli/commands/protocol/list.ts index 228151e04..c82ca1d81 100644 --- a/cli/commands/protocol/list.ts +++ b/cli/commands/protocol/list.ts @@ -15,6 +15,9 @@ const contractNames = [ 'DisputeManager', 'RewardsManager', 'GNS', + 'L1GraphTokenGateway', + 'L2GraphToken', + 'L2GraphTokenGateway', ] export const listProtocolParams = async (cli: CLIEnvironment): Promise => { @@ -26,13 +29,15 @@ export const listProtocolParams = async (cli: CLIEnvironment): Promise => colWidths: [30, 50], }) + if (!(contractName in cli.contracts)) { + continue + } const contract = cli.contracts[contractName] table.push(['* address', contract.address]) const req = [] for (const fn of Object.values(gettersList)) { if (fn.contract != contractName) continue - const contract = cli.contracts[fn.contract] if (contract.interface.getFunction(fn.name).inputs.length == 0) { const contractFn: ContractFunction = contract.functions[fn.name] @@ -56,7 +61,7 @@ export const listProtocolParams = async (cli: CLIEnvironment): Promise => const controller = cli.contracts['Controller'] for (const contractName of contractNames) { - if (contractName === 'Controller') continue + if (contractName === 'Controller' || !(contractName in cli.contracts)) continue const contract = cli.contracts[contractName] const contractFn = contract.functions['controller'] diff --git a/cli/commands/protocol/set.ts b/cli/commands/protocol/set.ts index 4fc05f877..966d07897 100644 --- a/cli/commands/protocol/set.ts +++ b/cli/commands/protocol/set.ts @@ -59,6 +59,31 @@ export const settersList = { 'controller-set-paused': { contract: 'Controller', name: 'setPaused' }, 'controller-set-partial-paused': { contract: 'Controller', name: 'setPartialPaused' }, 'controller-set-pause-guardian': { contract: 'Controller', name: 'setPauseGuardian' }, + 'l1-gateway-set-l2-grt': { contract: 'L1GraphTokenGateway', name: 'setL2TokenAddress' }, + 'l1-gateway-set-arbitrum-addresses': { + contract: 'L1GraphTokenGateway', + name: 'setArbitrumAddresses', + }, + 'l1-gateway-set-l2-counterpart': { + contract: 'L1GraphTokenGateway', + name: 'setL2CounterpartAddress', + }, + 'l1-gateway-set-escrow-address': { + contract: 'L1GraphTokenGateway', + name: 'setEscrowAddress', + }, + 'l1-gateway-set-paused': { contract: 'L1GraphTokenGateway', name: 'setPaused' }, + 'bridge-escrow-approve-all': { contract: 'BridgeEscrow', name: 'approveAll' }, + 'bridge-escrow-revoke-all': { contract: 'BridgeEscrow', name: 'revokeAll' }, + 'l2-gateway-set-l1-grt': { contract: 'L2GraphTokenGateway', name: 'setL1TokenAddress' }, + 'l2-gateway-set-l2-router': { contract: 'L2GraphTokenGateway', name: 'setL2Router' }, + 'l2-gateway-set-l1-counterpart': { + contract: 'L2GraphTokenGateway', + name: 'setL1CounterpartAddress', + }, + 'l2-gateway-set-paused': { contract: 'L2GraphTokenGateway', name: 'setPaused' }, + 'l2-token-set-gateway': { contract: 'L2GraphToken', name: 'setGateway' }, + 'l2-token-set-l1-address': { contract: 'L2GraphToken', name: 'setL1Address' }, } const buildHelp = () => { diff --git a/cli/contracts.ts b/cli/contracts.ts index fbe79062f..6dace372d 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -1,4 +1,4 @@ -import { providers, Signer } from 'ethers' +import { BaseContract, providers, Signer } from 'ethers' import { AddressBook } from './address-book' import { logger } from './logging' @@ -19,6 +19,11 @@ import { IENS } from '../build/types/IENS' import { IEthereumDIDRegistry } from '../build/types/IEthereumDIDRegistry' import { GraphGovernance } from '../build/types/GraphGovernance' import { AllocationExchange } from '../build/types/AllocationExchange' +import { L1GraphTokenGateway } from '../build/types/L1GraphTokenGateway' +import { L2GraphToken } from '../build/types/L2GraphToken' +import { L2GraphTokenGateway } from '../build/types/L2GraphTokenGateway' +import { BridgeEscrow } from '../build/types/BridgeEscrow' +import { chainIdIsL2 } from './utils' export interface NetworkContracts { EpochManager: EpochManager @@ -36,20 +41,37 @@ export interface NetworkContracts { IEthereumDIDRegistry: IEthereumDIDRegistry GraphGovernance: GraphGovernance AllocationExchange: AllocationExchange + L1GraphTokenGateway: L1GraphTokenGateway + BridgeEscrow: BridgeEscrow + L2GraphToken: L2GraphToken + L2GraphTokenGateway: L2GraphTokenGateway +} + +export const loadAddressBookContract = ( + contractName: string, + addressBook: AddressBook, + signerOrProvider?: Signer | providers.Provider, +): BaseContract => { + const contractEntry = addressBook.getEntry(contractName) + let contract = getContractAt(contractName, contractEntry.address) + if (signerOrProvider) { + contract = contract.connect(signerOrProvider) + } + return contract } export const loadContracts = ( addressBook: AddressBook, + chainId: number | string, signerOrProvider?: Signer | providers.Provider, ): NetworkContracts => { const contracts = {} for (const contractName of addressBook.listEntries()) { - const contractEntry = addressBook.getEntry(contractName) try { - const contract = getContractAt(contractName, contractEntry.address) - contracts[contractName] = contract - if (signerOrProvider) { - contracts[contractName] = contracts[contractName].connect(signerOrProvider) + contracts[contractName] = loadAddressBookContract(contractName, addressBook, signerOrProvider) + // On L2 networks, we alias L2GraphToken as GraphToken + if (signerOrProvider && chainIdIsL2(chainId) && contractName == 'L2GraphToken') { + contracts['GraphToken'] = contracts[contractName] } } catch (err) { logger.warn(`Could not load contract ${contractName} - ${err.message}`) diff --git a/cli/defaults.ts b/cli/defaults.ts index c1be71eb2..b6a76ddb2 100644 --- a/cli/defaults.ts +++ b/cli/defaults.ts @@ -7,6 +7,8 @@ export const local = { addressBookPath: './addresses.json', graphConfigPath: './config/graph.mainnet.yml', accountNumber: '0', + arbitrumAddressBookPath: './arbitrum-addresses.json', + arbProviderUrl: 'https://rinkeby.arbitrum.io/rpc', } export const defaultOverrides: Overrides = { @@ -56,4 +58,18 @@ export const cliOpts = { type: 'boolean', default: false, }, + arbitrumAddressBook: { + alias: 'arb-address-book', + description: 'The path to the address book file for Arbitrum deployments', + type: 'string', + group: 'Config', + default: local.arbitrumAddressBookPath, + }, + l2ProviderUrl: { + alias: 'l2-provider-url', + description: 'The URL of an Arbitrum provider (only for bridge commands)', + type: 'string', + group: 'Arbitrum', + default: local.arbProviderUrl, + }, } as { [key: string]: Options } diff --git a/cli/env.ts b/cli/env.ts index d889fcdc6..ab89c9d85 100644 --- a/cli/env.ts +++ b/cli/env.ts @@ -43,7 +43,7 @@ export const loadEnv = async (argv: CLIArgs, wallet?: Wallet): Promise, - overrides?: Overrides, + overrides?: PayableOverrides, ): Promise => { // Setup overrides if (overrides) { diff --git a/cli/utils.ts b/cli/utils.ts index a11e40f4e..035c5dba0 100644 --- a/cli/utils.ts +++ b/cli/utils.ts @@ -2,6 +2,20 @@ import { Contract, Wallet, providers } from 'ethers' import { loadArtifact } from './artifacts' +export const l1ToL2ChainIdMap = { + '1': '42161', + '4': '421611', + '5': '421612', +} + +export const l2ChainIds = Object.values(l1ToL2ChainIdMap).map(Number) +export const l2ToL1ChainIdMap = Object.fromEntries( + Object.entries(l1ToL2ChainIdMap).map(([k, v]) => [v, k]), +) + +export const nodeInterfaceAddress = '0x00000000000000000000000000000000000000C8' +export const arbRetryableTxAddress = '0x000000000000000000000000000000000000006E' + export const contractAt = ( contractName: string, contractAddress: string, @@ -12,3 +26,7 @@ export const contractAt = ( export const getProvider = (providerUrl: string, network?: number): providers.JsonRpcProvider => new providers.JsonRpcProvider(providerUrl, network) + +export const chainIdIsL2 = (chainId: number | string): boolean => { + return l2ChainIds.includes(Number(chainId)) +} diff --git a/contracts/arbitrum/NodeInterface.sol b/contracts/arbitrum/NodeInterface.sol new file mode 100644 index 000000000..ba8a6acaa --- /dev/null +++ b/contracts/arbitrum/NodeInterface.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/89e1f6234fe133253f445db44ec1612d57389c45/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed max solidity version to <0.8.0 (pablo@edgeandnode.com) + * + */ + +pragma solidity >=0.4.21 <0.8.0; + +/** @title Interface for providing Outbox proof data + * @notice This contract doesn't exist on-chain. Instead it is a virtual interface accessible at 0x00000000000000000000000000000000000000C8 + * This is a cute trick to allow an Arbitrum node to provide data without us having to implement an additional RPC ) + */ + +interface NodeInterface { + /** + * @notice Returns the proof necessary to redeem a message + * @param batchNum index of outbox entry (i.e., outgoing messages Merkle root) in array of outbox entries + * @param index index of outgoing message in outbox entry + * @return proof Merkle proof of message inclusion in outbox entry + * @return path Merkle path to message + * @return l2Sender sender if original message (i.e., caller of ArbSys.sendTxToL1) + * @return l1Dest destination address for L1 contract call + * @return l2Block l2 block number at which sendTxToL1 call was made + * @return l1Block l1 block number at which sendTxToL1 call was made + * @return timestamp l2 Timestamp at which sendTxToL1 call was made + * @return amount value in L1 message in wei + * @return calldataForL1 abi-encoded L1 message data + */ + function lookupMessageBatchProof(uint256 batchNum, uint64 index) + external + view + returns ( + bytes32[] memory proof, + uint256 path, + address l2Sender, + address l1Dest, + uint256 l2Block, + uint256 l1Block, + uint256 timestamp, + uint256 amount, + bytes memory calldataForL1 + ); + + /** + * @notice Estimate the cost of putting a message in the L2 inbox that is reexecuted + * @param sender sender of the L1 and L2 transaction + * @param deposit amount to deposit to sender in L2 + * @param destAddr destination L2 contract address + * @param l2CallValue call value for retryable L2 message + * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee + * @param excessFeeRefundAddress maxgas x gasprice - execution cost gets credited here on L2 balance + * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled + * @param maxGas Max gas deducted from user's L2 balance to cover L2 execution + * @param gasPriceBid price bid for L2 execution + * @param data ABI encoded data of L2 message + * @return gas used, and gas price to execute this transaction + */ + function estimateRetryableTicket( + address sender, + uint256 deposit, + address destAddr, + uint256 l2CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external pure returns (uint256, uint256); +} diff --git a/contracts/tests/arbitrum/ArbRetryableTxStub.sol b/contracts/tests/arbitrum/ArbRetryableTxStub.sol new file mode 100644 index 000000000..3b2a32702 --- /dev/null +++ b/contracts/tests/arbitrum/ArbRetryableTxStub.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +// This only exists so that our hardhat build gives us an ABI artifact for ArbRetryableTx + +pragma solidity ^0.7.6; + +import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; + +abstract contract ArbRetryableTxStub is ArbRetryableTx {} diff --git a/hardhat.config.ts b/hardhat.config.ts index 80475e136..11557e15b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -58,9 +58,11 @@ interface NetworkConfig { const networkConfigs: NetworkConfig[] = [ { network: 'mainnet', chainId: 1 }, { network: 'rinkeby', chainId: 4 }, + { network: 'goerli', chainId: 5 }, { network: 'kovan', chainId: 42 }, { network: 'arbitrum-rinkeby', chainId: 421611, url: 'https://rinkeby.arbitrum.io/rpc' }, { network: 'arbitrum-one', chainId: 42161, url: 'https://arb1.arbitrum.io/rpc' }, + { network: 'arbitrum-nitro-devnet', chainId: 421612, url: 'https://nitro-devnet.arbitrum.io/rpc' }, ] function getAccountMnemonic() { diff --git a/package.json b/package.json index da7e6acb7..01bc45677 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,14 @@ "addresses.json" ], "dependencies": { - "ethers": "^5.4.4" + "ethers": "^5.6.0" }, "devDependencies": { + "@arbitrum/sdk": "^1.1.2", "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", "@defi-wonderland/smock": "^2.0.7", - "@ethersproject/experimental": "^5.4.0", + "@ethersproject/experimental": "^5.6.0", "@graphprotocol/common-ts": "^1.6.0", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-etherscan": "^2.1.1", diff --git a/tasks/gre.ts b/tasks/gre.ts index ebeda417c..e3fed59a7 100644 --- a/tasks/gre.ts +++ b/tasks/gre.ts @@ -25,8 +25,7 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { const chainId = hre.network.config.chainId.toString() const provider = hre.ethers.provider const addressBook = getAddressBook(cliOpts.addressBook.default, chainId) - const contracts = loadContracts(addressBook, provider) as ConsoleNetworkContracts - + const contracts = loadContracts(addressBook, chainId, provider) as ConsoleNetworkContracts // Connect contracts to a signing account contracts.connect = async function (n = 0) { const accounts = await hre.ethers.getSigners() diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index 36b63a33a..71909d587 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -9,7 +9,11 @@ import { NetworkFixture } from '../lib/fixtures' import { FakeContract, smock } from '@defi-wonderland/smock' -import rewardsManagerMockAbi from '../../build/abis/RewardsManagerMock.json' +import path from 'path' +import { Artifacts } from 'hardhat/internal/artifacts' +const ARTIFACTS_PATH = path.resolve('build/contracts') +const artifacts = new Artifacts(ARTIFACTS_PATH) +const rewardsManagerMockAbi = artifacts.readArtifactSync('RewardsManagerMock').abi use(smock.matchers) @@ -467,8 +471,7 @@ describe('L2GraphTokenGateway', () => { toGRT('10'), defaultDataWithNotEmptyCallHookData, ) - await expect(tx) - .revertedWith('CALLHOOK_FAILED') + await expect(tx).revertedWith('CALLHOOK_FAILED') }) }) }) diff --git a/yarn.lock b/yarn.lock index c2bc98ea5..fbe22cbd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,25 @@ # yarn lockfile v1 +"@arbitrum/sdk@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-1.1.2.tgz#3c29a4d333791781dbd61410fe635a57b595aa71" + integrity sha512-ILgdiOqezBs2loa703YtG4W10arxgrfWbaL17e7dnjp8q2yhjdWF0iTSx+Ep/mTVPhVE/9cOT/S/XuUmxdqiMQ== + dependencies: + "@ethersproject/address" "^5.0.8" + "@ethersproject/bignumber" "^5.1.1" + "@ethersproject/bytes" "^5.0.8" + "@typechain/ethers-v5" "9.0.0" + "@types/prompts" "^2.0.14" + "@types/yargs" "^17.0.9" + arb-bridge-eth "0.7.5" + arb-bridge-peripherals "1.0.6" + arbos-precompiles "1.0.2" + dotenv "^10.0.0" + ethers "^5.1.0" + ts-node "^10.2.1" + typechain "7.0.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -10,26 +29,31 @@ "@babel/highlight" "^7.10.4" "@babel/code-frame@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" - integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== dependencies: - "@babel/highlight" "^7.14.5" + "@babel/highlight" "^7.16.7" -"@babel/helper-validator-identifier@^7.14.5": - version "7.14.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" - integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== -"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" - integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== +"@babel/highlight@^7.10.4", "@babel/highlight@^7.16.7": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" + integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg== dependencies: - "@babel/helper-validator-identifier" "^7.14.5" + "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@commitlint/cli@^13.2.1": version "13.2.1" resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-13.2.1.tgz#80ebd46beef6ceed3bb0c0842bcda8d02a3c91de" @@ -170,15 +194,22 @@ dependencies: chalk "^4.0.0" -"@cto.af/textdecoder@^0.0.0": - version "0.0.0" - resolved "https://registry.yarnpkg.com/@cto.af/textdecoder/-/textdecoder-0.0.0.tgz#e1e8d84c936c30a0f4619971f19ca41941af9fdc" - integrity sha512-sJpx3F5xcVV/9jNYJQtvimo4Vfld/nD3ph+ZWtQzZ03Zo8rJC7QKQTRcIGS13Rcz80DwFNthCWMrd58vpY4ZAQ== +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" "@dabh/diagnostics@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" - integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== dependencies: colorspace "1.1.x" enabled "2.0.x" @@ -237,18 +268,18 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@ethereum-waffle/chai@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/chai/-/chai-3.4.0.tgz#2477877410a96bf370edd64df905b04fb9aba9d5" - integrity sha512-GVaFKuFbFUclMkhHtQTDnWBnBQMJc/pAbfbFj/nnIK237WPLsO3KDDslA7m+MNEyTAOFrcc0CyfruAGGXAQw3g== +"@ethereum-waffle/chai@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/chai/-/chai-3.4.4.tgz#16c4cc877df31b035d6d92486dfdf983df9138ff" + integrity sha512-/K8czydBtXXkcM9X6q29EqEkc5dN3oYenyH2a9hF7rGAApAJUpH8QBtojxOY/xQ2up5W332jqgxwp0yPiYug1g== dependencies: - "@ethereum-waffle/provider" "^3.4.0" - ethers "^5.0.0" + "@ethereum-waffle/provider" "^3.4.4" + ethers "^5.5.2" -"@ethereum-waffle/compiler@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/compiler/-/compiler-3.4.0.tgz#68917321212563544913de33e408327745cb1284" - integrity sha512-a2wxGOoB9F1QFRE+Om7Cz2wn+pxM/o7a0a6cbwhaS2lECJgFzeN9xEkVrKahRkF4gEfXGcuORg4msP0Asxezlw== +"@ethereum-waffle/compiler@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/compiler/-/compiler-3.4.4.tgz#d568ee0f6029e68b5c645506079fbf67d0dfcf19" + integrity sha512-RUK3axJ8IkD5xpWjWoJgyHclOeEzDLQFga6gKpeGxiS/zBu+HB0W2FvsrrLalTFIaPw/CGYACRBSIxqiCqwqTQ== dependencies: "@resolver-engine/imports" "^0.3.3" "@resolver-engine/imports-fs" "^0.3.3" @@ -262,30 +293,30 @@ ts-generator "^0.1.1" typechain "^3.0.0" -"@ethereum-waffle/ens@^3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/ens/-/ens-3.3.0.tgz#d54f4c8e6b7bcafdc13ab294433f45416b2b2791" - integrity sha512-zVIH/5cQnIEgJPg1aV8+ehYicpcfuAisfrtzYh1pN3UbfeqPylFBeBaIZ7xj/xYzlJjkrek/h9VfULl6EX9Aqw== +"@ethereum-waffle/ens@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/ens/-/ens-3.4.4.tgz#db97ea2c9decbb70b9205d53de2ccbd6f3182ba1" + integrity sha512-0m4NdwWxliy3heBYva1Wr4WbJKLnwXizmy5FfSSr5PMbjI7SIGCdCB59U7/ZzY773/hY3bLnzLwvG5mggVjJWg== dependencies: "@ensdomains/ens" "^0.4.4" "@ensdomains/resolver" "^0.2.4" - ethers "^5.0.1" + ethers "^5.5.2" -"@ethereum-waffle/mock-contract@^3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/mock-contract/-/mock-contract-3.3.0.tgz#7b331f1c95c5d46ee9478f7a6be2869f707d307a" - integrity sha512-apwq0d+2nQxaNwsyLkE+BNMBhZ1MKGV28BtI9WjD3QD2Ztdt1q9II4sKA4VrLTUneYSmkYbJZJxw89f+OpJGyw== +"@ethereum-waffle/mock-contract@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/mock-contract/-/mock-contract-3.4.4.tgz#fc6ffa18813546f4950a69f5892d4dd54b2c685a" + integrity sha512-Mp0iB2YNWYGUV+VMl5tjPsaXKbKo8MDH9wSJ702l9EBjdxFf/vBvnMBAC1Fub1lLtmD0JHtp1pq+mWzg/xlLnA== dependencies: - "@ethersproject/abi" "^5.0.1" - ethers "^5.0.1" + "@ethersproject/abi" "^5.5.0" + ethers "^5.5.2" -"@ethereum-waffle/provider@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/provider/-/provider-3.4.0.tgz#a36a0890d4fbc230e807870c8d3b683594efef00" - integrity sha512-QgseGzpwlzmaHXhqfdzthCGu5a6P1SBF955jQHf/rBkK1Y7gGo2ukt3rXgxgfg/O5eHqRU+r8xw5MzVyVaBscQ== +"@ethereum-waffle/provider@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/provider/-/provider-3.4.4.tgz#398fc1f7eb91cc2df7d011272eacba8af0c7fffb" + integrity sha512-GK8oKJAM8+PKy2nK08yDgl4A80mFuI8zBkE0C9GqTRYQqvuxIyXoLmJ5NZU9lIwyWVv5/KsoA11BgAv2jXE82g== dependencies: - "@ethereum-waffle/ens" "^3.3.0" - ethers "^5.0.1" + "@ethereum-waffle/ens" "^3.4.4" + ethers "^5.5.2" ganache-core "^2.13.2" patch-package "^6.2.2" postinstall-postinstall "^2.1.0" @@ -314,15 +345,7 @@ lru-cache "^5.1.1" semaphore-async-await "^1.5.1" -"@ethereumjs/common@^2.3.0", "@ethereumjs/common@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.4.0.tgz#2d67f6e6ba22246c5c89104e6b9a119fb3039766" - integrity sha512-UdkhFWzWcJCZVsj1O/H8/oqj/0RVYjLc1OhPjBrQdALAkQHpCp8xXI4WLnuGTADqTdJZww0NtgwG+TRPkXt27w== - dependencies: - crc-32 "^1.2.0" - ethereumjs-util "^7.1.0" - -"@ethereumjs/common@^2.6.0", "@ethereumjs/common@^2.6.3": +"@ethereumjs/common@^2.3.0", "@ethereumjs/common@^2.4.0", "@ethereumjs/common@^2.6.0", "@ethereumjs/common@^2.6.3": version "2.6.3" resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.3.tgz#39ddece7300b336276bad6c02f6a9f1a082caa05" integrity sha512-mQwPucDL7FDYIg9XQ8DL31CnIYZwGhU5hyOO5E+BMmT71G0+RHvIT5rIkLBirJEKxV6+Rcf9aEIY0kXInxUWpQ== @@ -349,15 +372,7 @@ ethereumjs-util "^7.1.1" miller-rabin "^4.0.0" -"@ethereumjs/tx@^3.2.1": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.0.tgz#14ed1b7fa0f28e1cd61e3ecbdab824205f6a4378" - integrity sha512-yTwEj2lVzSMgE6Hjw9Oa1DZks/nKTWM8Wn4ykDNapBPua2f4nXO3qKnni86O6lgDj5fVNRqbDsD0yy7/XNGDEA== - dependencies: - "@ethereumjs/common" "^2.4.0" - ethereumjs-util "^7.1.0" - -"@ethereumjs/tx@^3.4.0", "@ethereumjs/tx@^3.5.1": +"@ethereumjs/tx@^3.2.1", "@ethereumjs/tx@^3.4.0", "@ethereumjs/tx@^3.5.1": version "3.5.1" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.1.tgz#8d941b83a602b4a89949c879615f7ea9a90e6671" integrity sha512-xzDrTiu4sqZXUcaBxJ4n4W5FrppwxLxZB4ZDGVLtxSQR4lVuOnFR6RcUHdg1mpUhAPVrmnzLJpxaeXnPxIyhWA== @@ -431,451 +446,728 @@ "@ethersproject/properties" "^5.0.3" "@ethersproject/strings" "^5.0.4" -"@ethersproject/abi@5.4.0", "@ethersproject/abi@^5.0.0-beta.146", "@ethersproject/abi@^5.0.1", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.4.0.tgz#a6d63bdb3672f738398846d4279fa6b6c9818242" - integrity sha512-9gU2H+/yK1j2eVMdzm6xvHSnMxk8waIHQGYCZg5uvAyH0rsAzxkModzBSpbAkAuhKFEovC2S9hM4nPuLym8IZw== - dependencies: - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/abi@5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.4.1.tgz#6ac28fafc9ef6f5a7a37e30356a2eb31fa05d39b" - integrity sha512-9mhbjUk76BiSluiiW4BaYyI58KSbDMMQpCLdsAR+RsT2GyATiNYxVv+pGWRrekmsIdY3I+hOqsYQSTkc8L/mcg== - dependencies: - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/abstract-provider@5.4.1", "@ethersproject/abstract-provider@^5.4.0": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.4.1.tgz#e404309a29f771bd4d28dbafadcaa184668c2a6e" - integrity sha512-3EedfKI3LVpjSKgAxoUaI+gB27frKsxzm+r21w9G60Ugk+3wVLQwhi1LsEJAKNV7WoZc8CIpNrATlL1QFABjtQ== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/networks" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/web" "^5.4.0" - -"@ethersproject/abstract-signer@5.4.1", "@ethersproject/abstract-signer@^5.4.0": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.4.1.tgz#e4e9abcf4dd4f1ba0db7dff9746a5f78f355ea81" - integrity sha512-SkkFL5HVq1k4/25dM+NWP9MILgohJCgGv5xT5AcRruGz4ILpfHeBtO/y6j+Z3UN/PAjDeb4P7E51Yh8wcGNLGA== - dependencies: - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - -"@ethersproject/address@5.4.0", "@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.0.4", "@ethersproject/address@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.4.0.tgz#ba2d00a0f8c4c0854933b963b9a3a9f6eb4a37a3" - integrity sha512-SD0VgOEkcACEG/C6xavlU1Hy3m5DGSXW3CUHkaaEHbAPPsgi0coP5oNPsxau8eTlZOk/bpa/hKeCNoK5IzVI2Q== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/rlp" "^5.4.0" - -"@ethersproject/base64@5.4.0", "@ethersproject/base64@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.4.0.tgz#7252bf65295954c9048c7ca5f43e5c86441b2a9a" - integrity sha512-CjQw6E17QDSSC5jiM9YpF7N1aSCHmYGMt9bWD8PWv6YPMxjsys2/Q8xLrROKI3IWJ7sFfZ8B3flKDTM5wlWuZQ== - dependencies: - "@ethersproject/bytes" "^5.4.0" - -"@ethersproject/basex@5.4.0", "@ethersproject/basex@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.4.0.tgz#0a2da0f4e76c504a94f2b21d3161ed9438c7f8a6" - integrity sha512-J07+QCVJ7np2bcpxydFVf/CuYo9mZ7T73Pe7KQY4c1lRlrixMeblauMxHXD0MPwFmUHZIILDNViVkykFBZylbg== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - -"@ethersproject/bignumber@5.4.1", "@ethersproject/bignumber@>=5.0.0-beta.130", "@ethersproject/bignumber@^5.0.7", "@ethersproject/bignumber@^5.1.1", "@ethersproject/bignumber@^5.4.0": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.4.1.tgz#64399d3b9ae80aa83d483e550ba57ea062c1042d" - integrity sha512-fJhdxqoQNuDOk6epfM7yD6J8Pol4NUCy1vkaGAkuujZm0+lNow//MKu1hLhRiYV4BsOHyBv5/lsTjF+7hWwhJg== +"@ethersproject/abi@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613" + integrity sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w== + dependencies: + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/hash" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + +"@ethersproject/abi@5.6.0", "@ethersproject/abi@^5.0.0-beta.146", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.6.0.tgz#ea07cbc1eec2374d32485679c12408005895e9f3" + integrity sha512-AhVByTwdXCc2YQ20v300w6KVHle9g2OFc28ZAFCPnJyEpkv1xKXjZcSTgWOlv1i+0dqlgF8RCF2Rn2KC1t+1Vg== + dependencies: + "@ethersproject/address" "^5.6.0" + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/constants" "^5.6.0" + "@ethersproject/hash" "^5.6.0" + "@ethersproject/keccak256" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.0" + +"@ethersproject/abstract-provider@5.5.1": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz#2f1f6e8a3ab7d378d8ad0b5718460f85649710c5" + integrity sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/networks" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/transactions" "^5.5.0" + "@ethersproject/web" "^5.5.0" + +"@ethersproject/abstract-provider@5.6.0", "@ethersproject/abstract-provider@^5.5.0", "@ethersproject/abstract-provider@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.6.0.tgz#0c4ac7054650dbd9c476cf5907f588bbb6ef3061" + integrity sha512-oPMFlKLN+g+y7a79cLK3WiLcjWFnZQtXWgnLAbHZcN3s7L4v90UHpTOrLk+m3yr0gt+/h9STTM6zrr7PM8uoRw== + dependencies: + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/networks" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/transactions" "^5.6.0" + "@ethersproject/web" "^5.6.0" + +"@ethersproject/abstract-signer@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz#590ff6693370c60ae376bf1c7ada59eb2a8dd08d" + integrity sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA== + dependencies: + "@ethersproject/abstract-provider" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + +"@ethersproject/abstract-signer@5.6.0", "@ethersproject/abstract-signer@^5.5.0", "@ethersproject/abstract-signer@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.0.tgz#9cd7ae9211c2b123a3b29bf47aab17d4d016e3e7" + integrity sha512-WOqnG0NJKtI8n0wWZPReHtaLkDByPL67tn4nBaDAhmVq8sjHTPbCdz4DRhVu/cfTOvfy9w3iq5QZ7BX7zw56BQ== + dependencies: + "@ethersproject/abstract-provider" "^5.6.0" + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + +"@ethersproject/address@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.5.0.tgz#bcc6f576a553f21f3dd7ba17248f81b473c9c78f" + integrity sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/rlp" "^5.5.0" + +"@ethersproject/address@5.6.0", "@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.0.4", "@ethersproject/address@^5.0.8", "@ethersproject/address@^5.5.0", "@ethersproject/address@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.0.tgz#13c49836d73e7885fc148ad633afad729da25012" + integrity sha512-6nvhYXjbXsHPS+30sHZ+U4VMagFC/9zAk6Gd/h3S21YW4+yfb0WfRtaAIZ4kfM4rrVwqiy284LP0GtL5HXGLxQ== + dependencies: + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/keccak256" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/rlp" "^5.6.0" + +"@ethersproject/base64@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090" + integrity sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA== + dependencies: + "@ethersproject/bytes" "^5.5.0" + +"@ethersproject/base64@5.6.0", "@ethersproject/base64@^5.5.0", "@ethersproject/base64@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.0.tgz#a12c4da2a6fb86d88563216b0282308fc15907c9" + integrity sha512-2Neq8wxJ9xHxCF9TUgmKeSh9BXJ6OAxWfeGWvbauPh8FuHEjamgHilllx8KkSd5ErxyHIX7Xv3Fkcud2kY9ezw== + dependencies: + "@ethersproject/bytes" "^5.6.0" + +"@ethersproject/basex@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.5.0.tgz#e40a53ae6d6b09ab4d977bd037010d4bed21b4d3" + integrity sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + +"@ethersproject/basex@5.6.0", "@ethersproject/basex@^5.5.0", "@ethersproject/basex@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.6.0.tgz#9ea7209bf0a1c3ddc2a90f180c3a7f0d7d2e8a69" + integrity sha512-qN4T+hQd/Md32MoJpc69rOwLYRUXwjTlhHDIeUkUmiN/JyWkkLLMoG0TqvSQKNqZOMgN5stbUYN6ILC+eD7MEQ== + dependencies: + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + +"@ethersproject/bignumber@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.5.0.tgz#875b143f04a216f4f8b96245bde942d42d279527" + integrity sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg== dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" bn.js "^4.11.9" -"@ethersproject/bignumber@5.4.2": - version "5.4.2" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.4.2.tgz#44232e015ae4ce82ac034de549eb3583c71283d8" - integrity sha512-oIBDhsKy5bs7j36JlaTzFgNPaZjiNDOXsdSgSpXRucUl+UA6L/1YLlFeI3cPAoodcenzF4nxNPV13pcy7XbWjA== +"@ethersproject/bignumber@5.6.0", "@ethersproject/bignumber@>=5.0.0-beta.130", "@ethersproject/bignumber@^5.0.7", "@ethersproject/bignumber@^5.1.1", "@ethersproject/bignumber@^5.5.0", "@ethersproject/bignumber@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.0.tgz#116c81b075c57fa765a8f3822648cf718a8a0e26" + integrity sha512-VziMaXIUHQlHJmkv1dlcd6GY2PmT0khtAqaMctCIDogxkrarMzA9L94KN1NeXqqOfFD6r0sJT3vCTOFSmZ07DA== dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" bn.js "^4.11.9" -"@ethersproject/bytes@5.4.0", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.4.0.tgz#56fa32ce3bf67153756dbaefda921d1d4774404e" - integrity sha512-H60ceqgTHbhzOj4uRc/83SCN9d+BSUnOkrr2intevqdtEMO1JFVZ1XL84OEZV+QjV36OaZYxtnt4lGmxcGsPfA== +"@ethersproject/bytes@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c" + integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog== dependencies: - "@ethersproject/logger" "^5.4.0" + "@ethersproject/logger" "^5.5.0" -"@ethersproject/constants@5.4.0", "@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.4", "@ethersproject/constants@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.4.0.tgz#ee0bdcb30bf1b532d2353c977bf2ef1ee117958a" - integrity sha512-tzjn6S7sj9+DIIeKTJLjK9WGN2Tj0P++Z8ONEIlZjyoTkBuODN+0VfhAyYksKi43l1Sx9tX2VlFfzjfmr5Wl3Q== +"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.0.8", "@ethersproject/bytes@^5.5.0", "@ethersproject/bytes@^5.6.0": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7" + integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g== dependencies: - "@ethersproject/bignumber" "^5.4.0" + "@ethersproject/logger" "^5.6.0" -"@ethersproject/contracts@5.4.1", "@ethersproject/contracts@^5.1.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.4.1.tgz#3eb4f35b7fe60a962a75804ada2746494df3e470" - integrity sha512-m+z2ZgPy4pyR15Je//dUaymRUZq5MtDajF6GwFbGAVmKz/RF+DNIPwF0k5qEcL3wPGVqUjFg2/krlCRVTU4T5w== - dependencies: - "@ethersproject/abi" "^5.4.0" - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - -"@ethersproject/experimental@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/experimental/-/experimental-5.4.0.tgz#72c280dc4c981749f7ea0a9ecffd663c633a145e" - integrity sha512-dy+AgxqRUwJ+8J6bIz062NL7a+Vv+zGP4geeN09nvNEMPzY/kPYQBV5RECRbFFCH0U5mASf5ZnuF1N2DIIaZ1g== - dependencies: - "@ethersproject/web" "^5.4.0" - ethers "^5.4.0" +"@ethersproject/constants@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.5.0.tgz#d2a2cd7d94bd1d58377d1d66c4f53c9be4d0a45e" + integrity sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + +"@ethersproject/constants@5.6.0", "@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.4", "@ethersproject/constants@^5.5.0", "@ethersproject/constants@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.0.tgz#55e3eb0918584d3acc0688e9958b0cedef297088" + integrity sha512-SrdaJx2bK0WQl23nSpV/b1aq293Lh0sUaZT/yYKPDKn4tlAbkH96SPJwIhwSwTsoQQZxuh1jnqsKwyymoiBdWA== + dependencies: + "@ethersproject/bignumber" "^5.6.0" + +"@ethersproject/contracts@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.5.0.tgz#b735260d4bd61283a670a82d5275e2a38892c197" + integrity sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@ethersproject/abstract-provider" "^5.5.0" + "@ethersproject/abstract-signer" "^5.5.0" + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/transactions" "^5.5.0" + +"@ethersproject/contracts@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.6.0.tgz#60f2cfc7addd99a865c6c8cfbbcec76297386067" + integrity sha512-74Ge7iqTDom0NX+mux8KbRUeJgu1eHZ3iv6utv++sLJG80FVuU9HnHeKVPfjd9s3woFhaFoQGf3B3iH/FrQmgw== + dependencies: + "@ethersproject/abi" "^5.6.0" + "@ethersproject/abstract-provider" "^5.6.0" + "@ethersproject/abstract-signer" "^5.6.0" + "@ethersproject/address" "^5.6.0" + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/constants" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/transactions" "^5.6.0" + +"@ethersproject/experimental@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/experimental/-/experimental-5.6.0.tgz#c72ef00a79b746c522eb79736712169d71c55f64" + integrity sha512-lSEM/6t+BicbeyRxat5meoQhXZLoBEziVrxZqeCIhsPntvq4DlMobPBKXF0Iz3m0dMvl9uga7fHEO4YD9SgCgw== + dependencies: + "@ethersproject/web" "^5.6.0" + ethers "^5.6.0" scrypt-js "3.0.1" -"@ethersproject/hash@5.4.0", "@ethersproject/hash@>=5.0.0-beta.128", "@ethersproject/hash@^5.0.4", "@ethersproject/hash@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.4.0.tgz#d18a8e927e828e22860a011f39e429d388344ae0" - integrity sha512-xymAM9tmikKgbktOCjW60Z5sdouiIIurkZUr9oW5NOex5uwxrbsYG09kb5bMcNjlVeJD3yPivTNzViIs1GCbqA== - dependencies: - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/hdnode@5.4.0", "@ethersproject/hdnode@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.4.0.tgz#4bc9999b9a12eb5ce80c5faa83114a57e4107cac" - integrity sha512-pKxdS0KAaeVGfZPp1KOiDLB0jba11tG6OP1u11QnYfb7pXn6IZx0xceqWRr6ygke8+Kw74IpOoSi7/DwANhy8Q== - dependencies: - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/basex" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/pbkdf2" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" - "@ethersproject/signing-key" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/wordlists" "^5.4.0" - -"@ethersproject/json-wallets@5.4.0", "@ethersproject/json-wallets@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.4.0.tgz#2583341cfe313fc9856642e8ace3080154145e95" - integrity sha512-igWcu3fx4aiczrzEHwG1xJZo9l1cFfQOWzTqwRw/xcvxTk58q4f9M7cjh51EKphMHvrJtcezJ1gf1q1AUOfEQQ== - dependencies: - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/hdnode" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/pbkdf2" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/random" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" +"@ethersproject/hash@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.5.0.tgz#7cee76d08f88d1873574c849e0207dcb32380cc9" + integrity sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg== + dependencies: + "@ethersproject/abstract-signer" "^5.5.0" + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + +"@ethersproject/hash@5.6.0", "@ethersproject/hash@>=5.0.0-beta.128", "@ethersproject/hash@^5.0.4", "@ethersproject/hash@^5.5.0", "@ethersproject/hash@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.0.tgz#d24446a5263e02492f9808baa99b6e2b4c3429a2" + integrity sha512-fFd+k9gtczqlr0/BruWLAu7UAOas1uRRJvOR84uDf4lNZ+bTkGl366qvniUZHKtlqxBRU65MkOobkmvmpHU+jA== + dependencies: + "@ethersproject/abstract-signer" "^5.6.0" + "@ethersproject/address" "^5.6.0" + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/keccak256" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.0" + +"@ethersproject/hdnode@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.5.0.tgz#4a04e28f41c546f7c978528ea1575206a200ddf6" + integrity sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q== + dependencies: + "@ethersproject/abstract-signer" "^5.5.0" + "@ethersproject/basex" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/pbkdf2" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/sha2" "^5.5.0" + "@ethersproject/signing-key" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@ethersproject/transactions" "^5.5.0" + "@ethersproject/wordlists" "^5.5.0" + +"@ethersproject/hdnode@5.6.0", "@ethersproject/hdnode@^5.5.0", "@ethersproject/hdnode@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.6.0.tgz#9dcbe8d629bbbcf144f2cae476337fe92d320998" + integrity sha512-61g3Jp3nwDqJcL/p4nugSyLrpl/+ChXIOtCEM8UDmWeB3JCAt5FoLdOMXQc3WWkc0oM2C0aAn6GFqqMcS/mHTw== + dependencies: + "@ethersproject/abstract-signer" "^5.6.0" + "@ethersproject/basex" "^5.6.0" + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/pbkdf2" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/sha2" "^5.6.0" + "@ethersproject/signing-key" "^5.6.0" + "@ethersproject/strings" "^5.6.0" + "@ethersproject/transactions" "^5.6.0" + "@ethersproject/wordlists" "^5.6.0" + +"@ethersproject/json-wallets@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz#dd522d4297e15bccc8e1427d247ec8376b60e325" + integrity sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ== + dependencies: + "@ethersproject/abstract-signer" "^5.5.0" + "@ethersproject/address" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/hdnode" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/pbkdf2" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/random" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@ethersproject/transactions" "^5.5.0" + aes-js "3.0.0" + scrypt-js "3.0.1" + +"@ethersproject/json-wallets@5.6.0", "@ethersproject/json-wallets@^5.5.0", "@ethersproject/json-wallets@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.6.0.tgz#4c2fc27f17e36c583e7a252fb938bc46f98891e5" + integrity sha512-fmh86jViB9r0ibWXTQipxpAGMiuxoqUf78oqJDlCAJXgnJF024hOOX7qVgqsjtbeoxmcLwpPsXNU0WEe/16qPQ== + dependencies: + "@ethersproject/abstract-signer" "^5.6.0" + "@ethersproject/address" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/hdnode" "^5.6.0" + "@ethersproject/keccak256" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/pbkdf2" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/random" "^5.6.0" + "@ethersproject/strings" "^5.6.0" + "@ethersproject/transactions" "^5.6.0" aes-js "3.0.0" scrypt-js "3.0.1" -"@ethersproject/keccak256@5.4.0", "@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.0.3", "@ethersproject/keccak256@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.4.0.tgz#7143b8eea4976080241d2bd92e3b1f1bf7025318" - integrity sha512-FBI1plWet+dPUvAzPAeHzRKiPpETQzqSUWR1wXJGHVWi4i8bOSrpC3NwpkPjgeXG7MnugVc1B42VbfnQikyC/A== +"@ethersproject/keccak256@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.5.0.tgz#e4b1f9d7701da87c564ffe336f86dcee82983492" + integrity sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg== dependencies: - "@ethersproject/bytes" "^5.4.0" - js-sha3 "0.5.7" + "@ethersproject/bytes" "^5.5.0" + js-sha3 "0.8.0" -"@ethersproject/logger@5.4.0", "@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@^5.0.5", "@ethersproject/logger@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.4.0.tgz#f39adadf62ad610c420bcd156fd41270e91b3ca9" - integrity sha512-xYdWGGQ9P2cxBayt64d8LC8aPFJk6yWCawQi/4eJ4+oJdMMjEBMrIcIMZ9AxhwpPVmnBPrsB10PcXGmGAqgUEQ== +"@ethersproject/keccak256@5.6.0", "@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.0.3", "@ethersproject/keccak256@^5.5.0", "@ethersproject/keccak256@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.6.0.tgz#fea4bb47dbf8f131c2e1774a1cecbfeb9d606459" + integrity sha512-tk56BJ96mdj/ksi7HWZVWGjCq0WVl/QvfhFQNeL8fxhBlGoP+L80uDCiQcpJPd+2XxkivS3lwRm3E0CXTfol0w== + dependencies: + "@ethersproject/bytes" "^5.6.0" + js-sha3 "0.8.0" -"@ethersproject/logger@5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.4.1.tgz#503bd33683538b923c578c07d1c2c0dd18672054" - integrity sha512-DZ+bRinnYLPw1yAC64oRl0QyVZj43QeHIhVKfD/+YwSz4wsv1pfwb5SOFjz+r710YEWzU6LrhuSjpSO+6PeE4A== +"@ethersproject/logger@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d" + integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg== + +"@ethersproject/logger@5.6.0", "@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@^5.0.5", "@ethersproject/logger@^5.5.0", "@ethersproject/logger@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a" + integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg== -"@ethersproject/networks@5.4.2", "@ethersproject/networks@^5.4.0": - version "5.4.2" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.4.2.tgz#2247d977626e97e2c3b8ee73cd2457babde0ce35" - integrity sha512-eekOhvJyBnuibfJnhtK46b8HimBc5+4gqpvd1/H9LEl7Q7/qhsIhM81dI9Fcnjpk3jB1aTy6bj0hz3cifhNeYw== +"@ethersproject/networks@5.5.1": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.1.tgz#b7f7b9fb88dec1ea48f739b7fb9621311aa8ce6c" + integrity sha512-tYRDM4zZtSUcKnD4UMuAlj7SeXH/k5WC4SP2u1Pn57++JdXHkRu2zwNkgNogZoxHzhm9Q6qqurDBVptHOsW49Q== dependencies: - "@ethersproject/logger" "^5.4.0" + "@ethersproject/logger" "^5.5.0" -"@ethersproject/pbkdf2@5.4.0", "@ethersproject/pbkdf2@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.4.0.tgz#ed88782a67fda1594c22d60d0ca911a9d669641c" - integrity sha512-x94aIv6tiA04g6BnazZSLoRXqyusawRyZWlUhKip2jvoLpzJuLb//KtMM6PEovE47pMbW+Qe1uw+68ameJjB7g== +"@ethersproject/networks@5.6.1", "@ethersproject/networks@^5.5.0", "@ethersproject/networks@^5.6.0": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.1.tgz#7a21ed1f83e86121737b16841961ec99ccf5c9c7" + integrity sha512-b2rrupf3kCTcc3jr9xOWBuHylSFtbpJf79Ga7QR98ienU2UqGimPGEsYMgbI29KHJfA5Us89XwGVmxrlxmSrMg== dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" + "@ethersproject/logger" "^5.6.0" -"@ethersproject/properties@5.4.0", "@ethersproject/properties@>=5.0.0-beta.131", "@ethersproject/properties@^5.0.3", "@ethersproject/properties@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.4.0.tgz#38ba20539b44dcc5d5f80c45ad902017dcdbefe7" - integrity sha512-7jczalGVRAJ+XSRvNA6D5sAwT4gavLq3OXPuV/74o3Rd2wuzSL035IMpIMgei4CYyBdialJMrTqkOnzccLHn4A== +"@ethersproject/pbkdf2@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz#e25032cdf02f31505d47afbf9c3e000d95c4a050" + integrity sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg== dependencies: - "@ethersproject/logger" "^5.4.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/sha2" "^5.5.0" -"@ethersproject/properties@5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.4.1.tgz#9f051f976ce790142c6261ccb7b826eaae1f2f36" - integrity sha512-cyCGlF8wWlIZyizsj2PpbJ9I7rIlUAfnHYwy/T90pdkSn/NFTa5YWZx2wTJBe9V7dD65dcrrEMisCRUJiq6n3w== - dependencies: - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/providers@5.4.3": - version "5.4.3" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.3.tgz#4cd7ccd9e12bc3875b33df8b24abf735663958a5" - integrity sha512-VURwkaWPoUj7jq9NheNDT5Iyy64Qcyf6BOFDwVdHsmLmX/5prNjFrgSX3GHPE4z1BRrVerDxe2yayvXKFm/NNg== - dependencies: - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/basex" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/networks" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/random" "^5.4.0" - "@ethersproject/rlp" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/web" "^5.4.0" +"@ethersproject/pbkdf2@5.6.0", "@ethersproject/pbkdf2@^5.5.0", "@ethersproject/pbkdf2@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.6.0.tgz#04fcc2d7c6bff88393f5b4237d906a192426685a" + integrity sha512-Wu1AxTgJo3T3H6MIu/eejLFok9TYoSdgwRr5oGY1LTLfmGesDoSx05pemsbrPT2gG4cQME+baTSCp5sEo2erZQ== + dependencies: + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/sha2" "^5.6.0" + +"@ethersproject/properties@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995" + integrity sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA== + dependencies: + "@ethersproject/logger" "^5.5.0" + +"@ethersproject/properties@5.6.0", "@ethersproject/properties@>=5.0.0-beta.131", "@ethersproject/properties@^5.0.3", "@ethersproject/properties@^5.5.0", "@ethersproject/properties@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.6.0.tgz#38904651713bc6bdd5bdd1b0a4287ecda920fa04" + integrity sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg== + dependencies: + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/providers@5.5.1": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.1.tgz#ba87e3c93219bbd2e2edf8b369873aee774abf04" + integrity sha512-2zdD5sltACDWhjUE12Kucg2PcgM6V2q9JMyVvObtVGnzJu+QSmibbP+BHQyLWZUBfLApx2942+7DC5D+n4wBQQ== + dependencies: + "@ethersproject/abstract-provider" "^5.5.0" + "@ethersproject/abstract-signer" "^5.5.0" + "@ethersproject/address" "^5.5.0" + "@ethersproject/basex" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/hash" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/networks" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/random" "^5.5.0" + "@ethersproject/rlp" "^5.5.0" + "@ethersproject/sha2" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@ethersproject/transactions" "^5.5.0" + "@ethersproject/web" "^5.5.0" bech32 "1.1.4" ws "7.4.6" -"@ethersproject/providers@5.4.5": - version "5.4.5" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.5.tgz#eb2ea2a743a8115f79604a8157233a3a2c832928" - integrity sha512-1GkrvkiAw3Fj28cwi1Sqm8ED1RtERtpdXmRfwIBGmqBSN5MoeRUHuwHPppMtbPayPgpFcvD7/Gdc9doO5fGYgw== - dependencies: - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/basex" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/networks" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/random" "^5.4.0" - "@ethersproject/rlp" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/web" "^5.4.0" +"@ethersproject/providers@5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.6.2.tgz#b9807b1c8c6f59fa2ee4b3cf6519724d07a9f422" + integrity sha512-6/EaFW/hNWz+224FXwl8+HdMRzVHt8DpPmu5MZaIQqx/K/ELnC9eY236SMV7mleCM3NnEArFwcAAxH5kUUgaRg== + dependencies: + "@ethersproject/abstract-provider" "^5.6.0" + "@ethersproject/abstract-signer" "^5.6.0" + "@ethersproject/address" "^5.6.0" + "@ethersproject/basex" "^5.6.0" + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/constants" "^5.6.0" + "@ethersproject/hash" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/networks" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/random" "^5.6.0" + "@ethersproject/rlp" "^5.6.0" + "@ethersproject/sha2" "^5.6.0" + "@ethersproject/strings" "^5.6.0" + "@ethersproject/transactions" "^5.6.0" + "@ethersproject/web" "^5.6.0" bech32 "1.1.4" ws "7.4.6" -"@ethersproject/random@5.4.0", "@ethersproject/random@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.4.0.tgz#9cdde60e160d024be39cc16f8de3b9ce39191e16" - integrity sha512-pnpWNQlf0VAZDEOVp1rsYQosmv2o0ITS/PecNw+mS2/btF8eYdspkN0vIXrCMtkX09EAh9bdk8GoXmFXM1eAKw== +"@ethersproject/random@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.5.0.tgz#305ed9e033ca537735365ac12eed88580b0f81f9" + integrity sha512-egGYZwZ/YIFKMHcoBUo8t3a8Hb/TKYX8BCBoLjudVCZh892welR3jOxgOmb48xznc9bTcMm7Tpwc1gHC1PFNFQ== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + +"@ethersproject/random@5.6.0", "@ethersproject/random@^5.5.0", "@ethersproject/random@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.6.0.tgz#1505d1ab6a250e0ee92f436850fa3314b2cb5ae6" + integrity sha512-si0PLcLjq+NG/XHSZz90asNf+YfKEqJGVdxoEkSukzbnBgC8rydbgbUgBbBGLeHN4kAJwUFEKsu3sCXT93YMsw== dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" -"@ethersproject/rlp@5.4.0", "@ethersproject/rlp@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.4.0.tgz#de61afda5ff979454e76d3b3310a6c32ad060931" - integrity sha512-0I7MZKfi+T5+G8atId9QaQKHRvvasM/kqLyAH4XxBCBchAooH2EX5rL9kYZWwcm3awYV+XC7VF6nLhfeQFKVPg== +"@ethersproject/rlp@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.5.0.tgz#530f4f608f9ca9d4f89c24ab95db58ab56ab99a0" + integrity sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + +"@ethersproject/rlp@5.6.0", "@ethersproject/rlp@^5.5.0", "@ethersproject/rlp@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.0.tgz#55a7be01c6f5e64d6e6e7edb6061aa120962a717" + integrity sha512-dz9WR1xpcTL+9DtOT/aDO+YyxSSdO8YIS0jyZwHHSlAmnxA6cKU3TrTd4Xc/bHayctxTgGLYNuVVoiXE4tTq1g== dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" -"@ethersproject/sha2@5.4.0", "@ethersproject/sha2@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.4.0.tgz#c9a8db1037014cbc4e9482bd662f86c090440371" - integrity sha512-siheo36r1WD7Cy+bDdE1BJ8y0bDtqXCOxRMzPa4bV1TGt/eTUUt03BHoJNB6reWJD8A30E/pdJ8WFkq+/uz4Gg== +"@ethersproject/sha2@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7" + integrity sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + hash.js "1.1.7" + +"@ethersproject/sha2@5.6.0", "@ethersproject/sha2@^5.5.0", "@ethersproject/sha2@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.0.tgz#364c4c11cc753bda36f31f001628706ebadb64d9" + integrity sha512-1tNWCPFLu1n3JM9t4/kytz35DkuF9MxqkGGEHNauEbaARdm2fafnOyw1s0tIQDPKF/7bkP1u3dbrmjpn5CelyA== + dependencies: + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + hash.js "1.1.7" + +"@ethersproject/signing-key@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.5.0.tgz#2aa37169ce7e01e3e80f2c14325f624c29cedbe0" + integrity sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng== dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + bn.js "^4.11.9" + elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/signing-key@5.4.0", "@ethersproject/signing-key@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.4.0.tgz#2f05120984e81cf89a3d5f6dec5c68ee0894fbec" - integrity sha512-q8POUeywx6AKg2/jX9qBYZIAmKSB4ubGXdQ88l40hmATj29JnG5pp331nAWwwxPn2Qao4JpWHNZsQN+bPiSW9A== +"@ethersproject/signing-key@5.6.0", "@ethersproject/signing-key@^5.5.0", "@ethersproject/signing-key@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.0.tgz#4f02e3fb09e22b71e2e1d6dc4bcb5dafa69ce042" + integrity sha512-S+njkhowmLeUu/r7ir8n78OUKx63kBdMCPssePS89So1TH4hZqnWFsThEd/GiXYp9qMxVrydf7KdM9MTGPFukA== dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" bn.js "^4.11.9" elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/solidity@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.4.0.tgz#1305e058ea02dc4891df18b33232b11a14ece9ec" - integrity sha512-XFQTZ7wFSHOhHcV1DpcWj7VXECEiSrBuv7JErJvB9Uo+KfCdc3QtUZV+Vjh/AAaYgezUEKbCtE6Khjm44seevQ== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/strings@5.4.0", "@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.0.4", "@ethersproject/strings@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.4.0.tgz#fb12270132dd84b02906a8d895ae7e7fa3d07d9a" - integrity sha512-k/9DkH5UGDhv7aReXLluFG5ExurwtIpUfnDNhQA29w896Dw3i4uDTz01Quaptbks1Uj9kI8wo9tmW73wcIEaWA== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/transactions@5.4.0", "@ethersproject/transactions@^5.0.0-beta.135", "@ethersproject/transactions@^5.1.1", "@ethersproject/transactions@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.4.0.tgz#a159d035179334bd92f340ce0f77e83e9e1522e0" - integrity sha512-s3EjZZt7xa4BkLknJZ98QGoIza94rVjaEed0rzZ/jB9WrIuu/1+tjvYCWzVrystXtDswy7TPBeIepyXwSYa4WQ== - dependencies: - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/rlp" "^5.4.0" - "@ethersproject/signing-key" "^5.4.0" - -"@ethersproject/units@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.4.0.tgz#d57477a4498b14b88b10396062c8cbbaf20c79fe" - integrity sha512-Z88krX40KCp+JqPCP5oPv5p750g+uU6gopDYRTBGcDvOASh6qhiEYCRatuM/suC4S2XW9Zz90QI35MfSrTIaFg== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/wallet@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.4.0.tgz#fa5b59830b42e9be56eadd45a16a2e0933ad9353" - integrity sha512-wU29majLjM6AjCjpat21mPPviG+EpK7wY1+jzKD0fg3ui5fgedf2zEu1RDgpfIMsfn8fJHJuzM4zXZ2+hSHaSQ== - dependencies: - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/hdnode" "^5.4.0" - "@ethersproject/json-wallets" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/random" "^5.4.0" - "@ethersproject/signing-key" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/wordlists" "^5.4.0" - -"@ethersproject/web@5.4.0", "@ethersproject/web@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.4.0.tgz#49fac173b96992334ed36a175538ba07a7413d1f" - integrity sha512-1bUusGmcoRLYgMn6c1BLk1tOKUIFuTg8j+6N8lYlbMpDesnle+i3pGSagGNvwjaiLo4Y5gBibwctpPRmjrh4Og== - dependencies: - "@ethersproject/base64" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/wordlists@5.4.0", "@ethersproject/wordlists@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.4.0.tgz#f34205ec3bbc9e2c49cadaee774cf0b07e7573d7" - integrity sha512-FemEkf6a+EBKEPxlzeVgUaVSodU7G0Na89jqKjmWMlDB0tomoU8RlEMgUvXyqtrg8N4cwpLh8nyRnm1Nay1isA== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" +"@ethersproject/solidity@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.5.0.tgz#2662eb3e5da471b85a20531e420054278362f93f" + integrity sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/sha2" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + +"@ethersproject/solidity@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.6.0.tgz#64657362a596bf7f5630bdc921c07dd78df06dc3" + integrity sha512-YwF52vTNd50kjDzqKaoNNbC/r9kMDPq3YzDWmsjFTRBcIF1y4JCQJ8gB30wsTfHbaxgxelI5BfxQSxD/PbJOww== + dependencies: + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/keccak256" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/sha2" "^5.6.0" + "@ethersproject/strings" "^5.6.0" + +"@ethersproject/strings@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.5.0.tgz#e6784d00ec6c57710755699003bc747e98c5d549" + integrity sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + +"@ethersproject/strings@5.6.0", "@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.0.4", "@ethersproject/strings@^5.5.0", "@ethersproject/strings@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.0.tgz#9891b26709153d996bf1303d39a7f4bc047878fd" + integrity sha512-uv10vTtLTZqrJuqBZR862ZQjTIa724wGPWQqZrofaPI/kUsf53TBG0I0D+hQ1qyNtllbNzaW+PDPHHUI6/65Mg== + dependencies: + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/constants" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/transactions@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.5.0.tgz#7e9bf72e97bcdf69db34fe0d59e2f4203c7a2908" + integrity sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA== + dependencies: + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/rlp" "^5.5.0" + "@ethersproject/signing-key" "^5.5.0" + +"@ethersproject/transactions@5.6.0", "@ethersproject/transactions@^5.0.0-beta.135", "@ethersproject/transactions@^5.5.0", "@ethersproject/transactions@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.0.tgz#4b594d73a868ef6e1529a2f8f94a785e6791ae4e" + integrity sha512-4HX+VOhNjXHZyGzER6E/LVI2i6lf9ejYeWD6l4g50AdmimyuStKc39kvKf1bXWQMg7QNVh+uC7dYwtaZ02IXeg== + dependencies: + "@ethersproject/address" "^5.6.0" + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/constants" "^5.6.0" + "@ethersproject/keccak256" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/rlp" "^5.6.0" + "@ethersproject/signing-key" "^5.6.0" + +"@ethersproject/units@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.5.0.tgz#104d02db5b5dc42cc672cc4587bafb87a95ee45e" + integrity sha512-7+DpjiZk4v6wrikj+TCyWWa9dXLNU73tSTa7n0TSJDxkYbV3Yf1eRh9ToMLlZtuctNYu9RDNNy2USq3AdqSbag== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + +"@ethersproject/units@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.6.0.tgz#e5cbb1906988f5740254a21b9ded6bd51e826d9c" + integrity sha512-tig9x0Qmh8qbo1w8/6tmtyrm/QQRviBh389EQ+d8fP4wDsBrJBf08oZfoiz1/uenKK9M78yAP4PoR7SsVoTjsw== + dependencies: + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/constants" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/wallet@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.5.0.tgz#322a10527a440ece593980dca6182f17d54eae75" + integrity sha512-Mlu13hIctSYaZmUOo7r2PhNSd8eaMPVXe1wxrz4w4FCE4tDYBywDH+bAR1Xz2ADyXGwqYMwstzTrtUVIsKDO0Q== + dependencies: + "@ethersproject/abstract-provider" "^5.5.0" + "@ethersproject/abstract-signer" "^5.5.0" + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/hash" "^5.5.0" + "@ethersproject/hdnode" "^5.5.0" + "@ethersproject/json-wallets" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/random" "^5.5.0" + "@ethersproject/signing-key" "^5.5.0" + "@ethersproject/transactions" "^5.5.0" + "@ethersproject/wordlists" "^5.5.0" + +"@ethersproject/wallet@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.6.0.tgz#33d11a806d783864208f348709a5a3badac8e22a" + integrity sha512-qMlSdOSTyp0MBeE+r7SUhr1jjDlC1zAXB8VD84hCnpijPQiSNbxr6GdiLXxpUs8UKzkDiNYYC5DRI3MZr+n+tg== + dependencies: + "@ethersproject/abstract-provider" "^5.6.0" + "@ethersproject/abstract-signer" "^5.6.0" + "@ethersproject/address" "^5.6.0" + "@ethersproject/bignumber" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/hash" "^5.6.0" + "@ethersproject/hdnode" "^5.6.0" + "@ethersproject/json-wallets" "^5.6.0" + "@ethersproject/keccak256" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/random" "^5.6.0" + "@ethersproject/signing-key" "^5.6.0" + "@ethersproject/transactions" "^5.6.0" + "@ethersproject/wordlists" "^5.6.0" + +"@ethersproject/web@5.5.1": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.1.tgz#cfcc4a074a6936c657878ac58917a61341681316" + integrity sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg== + dependencies: + "@ethersproject/base64" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + +"@ethersproject/web@5.6.0", "@ethersproject/web@^5.5.0", "@ethersproject/web@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.6.0.tgz#4bf8b3cbc17055027e1a5dd3c357e37474eaaeb8" + integrity sha512-G/XHj0hV1FxI2teHRfCGvfBUHFmU+YOSbCxlAMqJklxSa7QMiHFQfAxvwY2PFqgvdkxEKwRNr/eCjfAPEm2Ctg== + dependencies: + "@ethersproject/base64" "^5.6.0" + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.0" + +"@ethersproject/wordlists@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.5.0.tgz#aac74963aa43e643638e5172353d931b347d584f" + integrity sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/hash" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + +"@ethersproject/wordlists@5.6.0", "@ethersproject/wordlists@^5.5.0", "@ethersproject/wordlists@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.6.0.tgz#79e62c5276e091d8575f6930ba01a29218ded032" + integrity sha512-q0bxNBfIX3fUuAo9OmjlEYxP40IB8ABgb7HjEZCL5IKubzV3j30CWi2rqQbjTS2HfoyQbfINoKcTVWP4ejwR7Q== + dependencies: + "@ethersproject/bytes" "^5.6.0" + "@ethersproject/hash" "^5.6.0" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.0" "@graphprotocol/common-ts@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@graphprotocol/common-ts/-/common-ts-1.6.0.tgz#b8ee56ae8abad2c836423d5d43bc250f0f2d69e7" - integrity sha512-3O8yR+XpmZRbb6TFGLoKCwEWftRrUpSKSMZ7d3p+AdqN7xaFBSZvBk8XbzeoMLvLCnYO7mid0VWGWE7FC3yDjA== + version "1.8.2" + resolved "https://registry.yarnpkg.com/@graphprotocol/common-ts/-/common-ts-1.8.2.tgz#f6d8dd2b27ea2f99804d5805e4eab022e41b0bb9" + integrity sha512-nqtOHNi5vHPiiBv0Dxq7/ZGa/Nch2UVu5hbbwo6BqVh4KaEq6Z9MeofyU5Ew538txNdTliXFZ135i0dsBhVRIA== dependencies: - "@ethersproject/bignumber" "^5.1.1" - "@ethersproject/contracts" "^5.1.1" - "@ethersproject/transactions" "^5.1.1" - "@graphprotocol/contracts" "1.4.0" + "@graphprotocol/contracts" "1.9.0" "@graphprotocol/pino-sentry-simple" "0.7.1" - "@urql/core" "1.13.1" - "@urql/exchange-execute" "1.0.1" - body-parser "1.19.0" + "@urql/core" "2.3.6" + "@urql/exchange-execute" "1.2.2" + body-parser "1.19.1" bs58 "4.0.1" cors "2.8.5" - cross-fetch "3.0.6" - ethers "^5.1.3" - express "4.17.1" - graphql "15.4.0" - graphql-tag "2.11.0" - helmet "4.1.1" + cross-fetch "3.1.4" + ethers "5.5.2" + express "4.17.2" + graphql "16.2.0" + graphql-tag "2.12.6" + helmet "4.6.0" morgan "1.10.0" ngeohash "0.6.3" - pg "8.4.2" - pg-hstore "2.3.3" - pino "6.7.0" - pino-multi-stream "5.1.1" - prom-client "12.0.0" - sequelize "6.3.5" - -"@graphprotocol/contracts@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@graphprotocol/contracts/-/contracts-1.4.0.tgz#7271e83e3444c08663216e91899ffc0d98fc1ff5" - integrity sha512-DvUtcOXlAREzcBW4tROpFdQNGTDqcCeNvhGuX2s+6C1znr6kDVDgIrXmKJ+ofGRzJxCNe3QAtlPdFg00IQL25w== + pg "8.7.1" + pg-hstore "2.3.4" + pino "7.6.0" + pino-multi-stream "6.0.0" + prom-client "14.0.1" + sequelize "6.12.0" + +"@graphprotocol/contracts@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@graphprotocol/contracts/-/contracts-1.9.0.tgz#1ebf3336e07263036092826e1297c31d4d266ba9" + integrity sha512-BonWS6P76RN4HPuq8T9qyJqp4ec8dJBI60MIEi10BD28Hq0WNfG6UYW+OfyIxw8g7LM9DRw8KK0e4f2IDg673A== dependencies: - ethers "^5.1.3" + ethers "^5.4.4" "@graphprotocol/pino-sentry-simple@0.7.1": version "0.7.1" @@ -887,10 +1179,10 @@ split2 "^3.1.1" through2 "^3.0.1" -"@graphql-typed-document-node/core@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950" - integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg== +"@graphql-typed-document-node/core@^3.1.0", "@graphql-typed-document-node/core@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052" + integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg== "@humanwhocodes/config-array@^0.5.0": version "0.5.0" @@ -902,9 +1194,9 @@ minimatch "^3.0.4" "@humanwhocodes/object-schema@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" - integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@metamask/eth-sig-util@^4.0.0": version "4.0.0" @@ -922,6 +1214,16 @@ resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw== +"@noble/hashes@1.0.0", "@noble/hashes@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.0.0.tgz#d5e38bfbdaba174805a4e649f13be9a9ed3351ae" + integrity sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg== + +"@noble/secp256k1@1.5.5", "@noble/secp256k1@~1.5.2": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.5.5.tgz#315ab5745509d1a8c8e90d0bdf59823ccf9bcfc3" + integrity sha512-sZ1W6gQzYnu45wPrWx8D3kwI2/U29VYTx9OjbDAd7jwRItJ0cSTMPRL/C8AWZFn9kWFLQGqEXVEE86w4Z8LpIQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -965,14 +1267,14 @@ util.promisify "^1.0.0" "@nomiclabs/hardhat-ethers@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.2.tgz#c472abcba0c5185aaa4ad4070146e95213c68511" - integrity sha512-6quxWe8wwS4X5v3Au8q1jOvXYEPkS1Fh+cME5u6AwNdnI4uERvPlVjlgRWzpnb+Rrt1l/cEqiNRH9GlsBMSDQg== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.5.tgz#131b0da1b71680d5a01569f916ae878229d326d3" + integrity sha512-A2gZAGB6kUvLx+kzM92HKuUF33F1FSe90L0TmkXkT2Hh0OKRpvWZURUSU2nghD2yC4DzfEZ3DftfeHGvZ2JTUw== "@nomiclabs/hardhat-etherscan@^2.1.1": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-2.1.4.tgz#510b6a669cc2dad095466b2ba9ae0b411c779081" - integrity sha512-KgFNTQv9gpioiTpQ9UlTysCAFfkcBonmEn9rVPTT22A7DRENFM1VTsVeGWF3AzRhd0mrASBF+o0gvbH30pSe0Q== + version "2.1.8" + resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-2.1.8.tgz#e206275e96962cd15e5ba9148b44388bc922d8c2" + integrity sha512-0+rj0SsZotVOcTLyDOxnOc3Gulo8upo0rsw/h+gBPcmtj91YqYJNhdARHoBxOhhE8z+5IUQPx+Dii04lXT14PA== dependencies: "@ethersproject/abi" "^5.1.2" "@ethersproject/address" "^5.0.2" @@ -983,39 +1285,46 @@ semver "^6.3.0" "@nomiclabs/hardhat-waffle@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.1.tgz#5d43654fba780720c5033dea240fe14f70ef4bd2" - integrity sha512-2YR2V5zTiztSH9n8BYWgtv3Q+EL0N5Ltm1PAr5z20uAY4SkkfylJ98CIqt18XFvxTD5x4K2wKBzddjV9ViDAZQ== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.3.tgz#9c538a09c5ed89f68f5fd2dc3f78f16ed1d6e0b1" + integrity sha512-049PHSnI1CZq6+XTbrMbMv5NaL7cednTfPenx02k3cEh8wBMLa6ys++dBETJa6JjfwgA9nBhhHQ173LJv6k2Pg== dependencies: "@types/sinon-chai" "^3.2.3" "@types/web3" "1.0.19" +"@openzeppelin/contracts-0.8@npm:@openzeppelin/contracts@^4.3.2": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.5.0.tgz#3fd75d57de172b3743cdfc1206883f56430409cc" + integrity sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA== + "@openzeppelin/contracts-upgradeable@3.4.2": version "3.4.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-3.4.2.tgz#2c2a1b0fa748235a1f495b6489349776365c51b3" integrity sha512-mDlBS17ymb2wpaLcrqRYdnBAmP1EwqhOXMvqWk2c5Q1N1pm5TkiCtXM9Xzznh4bYsQBq0aIWEkFFE2+iLSN1Tw== -"@openzeppelin/contracts@^3.4.1": +"@openzeppelin/contracts@3.4.2", "@openzeppelin/contracts@^3.4.1": version "3.4.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== "@openzeppelin/hardhat-upgrades@^1.6.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-1.9.0.tgz#68d9d5b5876809f49ae445973614ea9624474872" - integrity sha512-ND1sqm8dpTY6CZLdaC5IgtUo6zvlVgSeqadrWRbr/N7J2Bs2JsINWA2G+r4IeunzbcOJFB7GHTs/RkFR6hNLmA== + version "1.17.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-1.17.0.tgz#24ea0f366c3b2df985263cf8b1b796afd04d7e13" + integrity sha512-GNxR3/3fCKQsFpBi/r+5ib6U81UM9KCypmcOQxuCkVp9JKJ80/3hQdg1R+AQku/dlnhutPsfkCokH2LZFc5mNA== dependencies: - "@openzeppelin/upgrades-core" "^1.8.0" + "@openzeppelin/upgrades-core" "^1.14.1" + chalk "^4.1.0" + proper-lockfile "^4.1.1" -"@openzeppelin/upgrades-core@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.8.0.tgz#b2d6f747e3a4c3e37a18d1b13f45b7617dde2260" - integrity sha512-hO/SqUusFzD8mxrtDllShTE0eeWXsBkrR8zjjr8C8j+/gzvWQnd9HkNGTh7bpf8giUGzkERDuOOpFdSI1R3yvQ== +"@openzeppelin/upgrades-core@^1.14.1", "@openzeppelin/upgrades-core@^1.7.6": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.14.1.tgz#a0e1c83f9811186ac49d286e6b43ae129097422b" + integrity sha512-iKlh1mbUxyfdjdEiUFyhMkqirfas+DMUu7ED53nZbHEyhcYsm+5Fl/g0Bv6bZA+a7k8kO8+22DNEKsqaDUBc2Q== dependencies: bn.js "^5.1.2" - cbor "^7.0.0" + cbor "^8.0.0" chalk "^4.1.0" - compare-versions "^3.6.0" + compare-versions "^4.0.0" debug "^4.1.1" ethereumjs-util "^7.0.3" proper-lockfile "^4.1.1" @@ -1058,6 +1367,28 @@ path-browserify "^1.0.0" url "^0.11.0" +"@scure/base@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.0.0.tgz#109fb595021de285f05a7db6806f2f48296fcee7" + integrity sha512-gIVaYhUsy+9s58m/ETjSJVKHhKTBMmcRb9cEV5/5dwvfDlfORjKrFsDeDHWRrm6RjcPvCLZFwGJjAjLj1gg4HA== + +"@scure/bip32@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.0.1.tgz#1409bdf9f07f0aec99006bb0d5827693418d3aa5" + integrity sha512-AU88KKTpQ+YpTLoicZ/qhFhRRIo96/tlb+8YmDDHR9yiKVjSsFZiefJO4wjS2PMTkz5/oIcw84uAq/8pleQURA== + dependencies: + "@noble/hashes" "~1.0.0" + "@noble/secp256k1" "~1.5.2" + "@scure/base" "~1.0.0" + +"@scure/bip39@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.0.0.tgz#47504e58de9a56a4bbed95159d2d6829fa491bb0" + integrity sha512-HrtcikLbd58PWOkl02k9V6nXWQyoa7A0+Ek9VF7z17DDk9XZAFUcIdqfh0jJXLypmizc5/8P6OxoUeKliiWv4w== + dependencies: + "@noble/hashes" "~1.0.0" + "@scure/base" "~1.0.0" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -1131,33 +1462,7 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== -"@sinonjs/commons@^1.7.0": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" - integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== - dependencies: - type-detect "4.0.8" - -"@sinonjs/fake-timers@^7.1.0": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5" - integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg== - dependencies: - "@sinonjs/commons" "^1.7.0" - -"@solidity-parser/parser@^0.12.0": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.12.2.tgz#1afad367cb29a2ed8cdd4a3a62701c2821fb578f" - integrity sha512-d7VS7PxgMosm5NyaiyDJRNID5pK4AWj1l64Dbz0147hJgy5k2C0/ZiKK/9u5c5K+HRUVHmp+RMvGEjGh84oA5Q== - -"@solidity-parser/parser@^0.13.2": - version "0.13.2" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.13.2.tgz#b6c71d8ca0b382d90a7bbed241f9bc110af65cbe" - integrity sha512-RwHnpRnfrnD2MSPveYoPh8nhofEvX7fgjHk1Oq+NNvCcLx4r1js91CO9o+F/F3fBzOCyvm8kKRTriFICX/odWw== - dependencies: - antlr4ts "^0.5.0-alpha.4" - -"@solidity-parser/parser@^0.14.1": +"@solidity-parser/parser@^0.14.0", "@solidity-parser/parser@^0.14.1": version "0.14.1" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.1.tgz#179afb29f4e295a77cc141151f26b3848abc3c46" integrity sha512-eLjj2L6AuQjBB6s/ibwCAc0DwrR5Ge+ys+wgWo+bviU7fV2nTMQhU63CGaDKXg9iTmMxwhkyoggdIR7ZGRfMgw== @@ -1172,36 +1477,64 @@ defer-to-connect "^1.0.1" "@tenderly/hardhat-tenderly@^1.0.11": - version "1.0.12" - resolved "https://registry.yarnpkg.com/@tenderly/hardhat-tenderly/-/hardhat-tenderly-1.0.12.tgz#fa64da2bf2f6d35a1c131ac9ccaf2f7e8b189a50" - integrity sha512-zx2zVpbBxGWVp+aLgf59sZR5lxdqfq/PjqUhga6+iazukQNu/Y6pLfVnCcF1ggvLsf7gnMjwLe3YEx/GxCAykQ== + version "1.0.13" + resolved "https://registry.yarnpkg.com/@tenderly/hardhat-tenderly/-/hardhat-tenderly-1.0.13.tgz#6182a2d32bf12d110622f0b24263dc964ed7aa6d" + integrity sha512-XsrF2QIUh8YmzCcWHmPnSNQjZNBQkyrHER8bcrWsFIgL7ub49hmPbpGVB2Isb6gUV/IGBpBm7R69jpmoTvJ17g== dependencies: axios "^0.21.1" fs-extra "^9.0.1" js-yaml "^3.14.0" -"@truffle/error@^0.0.14": - version "0.0.14" - resolved "https://registry.yarnpkg.com/@truffle/error/-/error-0.0.14.tgz#59683b5407bede7bddf16d80dc5592f9c5e5fa05" - integrity sha512-utJx+SZYoMqk8wldQG4gCVKhV8GwMJbWY7sLXFT/D8wWZTnE2peX7URFJh/cxkjTRCO328z1s2qewkhyVsu2HA== +"@truffle/error@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@truffle/error/-/error-0.1.0.tgz#5e9fed79e6cda624c926d314b280a576f8b22a36" + integrity sha512-RbUfp5VreNhsa2Q4YbBjz18rOQI909pG32bghl1hulO7IpvcqTS+C3Ge5cNbiWQ1WGzy1wIeKLW0tmQtHFB7qg== -"@truffle/interface-adapter@^0.5.3": - version "0.5.3" - resolved "https://registry.yarnpkg.com/@truffle/interface-adapter/-/interface-adapter-0.5.3.tgz#9fa9b177d11ce57182797439ea2bc658bc706a49" - integrity sha512-LCwOKlIMZqRNbncJG3Q+BS+aFg2bXU95jjgLNwFK+K//eoj4QhdxQPq2bTejRg4ag0UOr8MqJQbYFzkJxJBujQ== +"@truffle/interface-adapter@^0.5.12": + version "0.5.12" + resolved "https://registry.yarnpkg.com/@truffle/interface-adapter/-/interface-adapter-0.5.12.tgz#8cc34e9a5363970bfec1001520ae55fad6a26bfd" + integrity sha512-Qrc5VARnvSILYqZNsAM0xsUHqGqphLXVdIvDnhUA1Xj1xyNz8iboTr8bXorMd+Uspw+PXmsW44BJ/Wioo/jL2A== dependencies: bn.js "^5.1.3" ethers "^4.0.32" - web3 "1.5.0" + web3 "1.5.3" "@truffle/provider@^0.2.24": - version "0.2.35" - resolved "https://registry.yarnpkg.com/@truffle/provider/-/provider-0.2.35.tgz#206373d97c911f2ce53ceb56d4bd15f73739a336" - integrity sha512-50go3TQOcfNVN8srUfHb7OIKFi6oEOFyybzZneDwSG6z6yD0A0Oc2Vk7/CeYiEkto7qrXXbIcVOP5zb++t3Q+g== + version "0.2.50" + resolved "https://registry.yarnpkg.com/@truffle/provider/-/provider-0.2.50.tgz#5c8a8fd7724bacac15cf7396cecc51c0ee1e597d" + integrity sha512-GCoyX8SncfgizXYJfordv5kiysQS7S1311D3ewciixaoQpTkbqC3s0wxwHlPxU7m5wJOCw2K8rQtL3oIFfeHwA== dependencies: - "@truffle/error" "^0.0.14" - "@truffle/interface-adapter" "^0.5.3" - web3 "1.5.0" + "@truffle/error" "^0.1.0" + "@truffle/interface-adapter" "^0.5.12" + web3 "1.5.3" + +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + +"@typechain/ethers-v5@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-9.0.0.tgz#6aa93bea7425c0463bd8a61eea3643540ef851bd" + integrity sha512-bAanuPl1L2itaUdMvor/QvwnIH+TM/CmG00q17Ilv3ZZMeJ2j8HcarhgJUZ9pBY1teBb85P8cC03dz3mSSx+tQ== + dependencies: + lodash "^4.17.15" + ts-essentials "^7.0.1" "@typechain/ethers-v5@^2.0.0": version "2.0.0" @@ -1211,21 +1544,24 @@ ethers "^5.0.2" "@typechain/ethers-v5@^7.0.0": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-7.0.1.tgz#f9ae60ae5bd9e8ea8a996f66244147e8e74034ae" - integrity sha512-mXEJ7LG0pOYO+MRPkHtbf30Ey9X2KAsU0wkeoVvjQIn7iAY6tB3k3s+82bbmJAUMyENbQ04RDOZit36CgSG6Gg== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-7.2.0.tgz#d559cffe0efe6bdbc20e644b817f6fa8add5e8f8" + integrity sha512-jfcmlTvaaJjng63QsT49MT6R1HFhtO/TBMWbyzPFSzMmVIqb2tL6prnKBs4ZJrSvmgIXWy+ttSjpaxCTq8D/Tw== + dependencies: + lodash "^4.17.15" + ts-essentials "^7.0.1" "@typechain/hardhat@^2.0.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@typechain/hardhat/-/hardhat-2.2.0.tgz#a3e99b49c215c364c375c554bdce626bb21ecc47" - integrity sha512-ICZdbc5QA/bSZEvdAHsEL3/u260ZPIK7WZLNnoGywQNo6A98w7VKQW4DR7hPPMHe1FaSI1LTuIesRQvCUVyT3A== + version "2.3.1" + resolved "https://registry.yarnpkg.com/@typechain/hardhat/-/hardhat-2.3.1.tgz#1e8a6e3795e115a5d5348526282b5c597fab0b78" + integrity sha512-BQV8OKQi0KAzLXCdsPO0pZBNQQ6ra8A2ucC26uFX/kquRBtJu1yEyWnVSmtr07b5hyRoJRpzUeINLnyqz4/MAw== dependencies: fs-extra "^9.1.0" "@types/abstract-leveldown@*": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-5.0.2.tgz#ee81917fe38f770e29eec8139b6f16ee4a8b0a5f" - integrity sha512-+jA1XXF3jsz+Z7FcuiNqgK53hTa/luglT2TyTpKPqoYbxVY+mCPF22Rm+q3KPBrMHJwNXFrTViHszBOfU4vftQ== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz#f055979a99f7654e84d6b8e6267419e9c4cfff87" + integrity sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ== "@types/bn.js@*", "@types/bn.js@^5.1.0": version "5.1.0" @@ -1249,9 +1585,9 @@ base-x "^3.0.6" "@types/chai@*": - version "4.2.21" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650" - integrity sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg== + version "4.3.0" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" + integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw== "@types/concat-stream@^1.6.0": version "1.6.1" @@ -1260,6 +1596,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" + integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + dependencies: + "@types/ms" "*" + "@types/dotenv@^8.2.0": version "8.2.0" resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053" @@ -1275,9 +1618,9 @@ "@types/node" "*" "@types/glob@^7.1.1": - version "7.1.4" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" - integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== dependencies: "@types/minimatch" "*" "@types/node" "*" @@ -1291,9 +1634,9 @@ rxjs "^6.4.0" "@types/json-schema@^7.0.7": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/level-errors@*": version "3.0.0" @@ -1336,18 +1679,23 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323" integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw== +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/node-fetch@^2.5.5": - version "2.5.12" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" - integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw== + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" + integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== dependencies: "@types/node" "*" form-data "^3.0.0" "@types/node@*": - version "16.4.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.12.tgz#961e3091f263e6345d2d84afab4e047a60b4b11b" - integrity sha512-zxrTNFl9Z8boMJXs6ieqZP0wAhvkdzmHSxTlJabM16cf5G9xBc1uPRH5Bbv2omEDDiM8MzTfqTJXBf0Ba4xFWA== + version "17.0.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" + integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== "@types/node@^10.0.3": version "10.17.60" @@ -1355,14 +1703,14 @@ integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== "@types/node@^12.12.6": - version "12.20.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.19.tgz#538e61fc220f77ae4a4663c3d8c3cb391365c209" - integrity sha512-niAuZrwrjKck4+XhoCw6AAVQBENHftpXw9F4ryk66fTgYaKQ53R4FI7c9vUGGw5vQis1HKBHDR1gcYI/Bq1xvw== + version "12.20.47" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.47.tgz#ca9237d51f2a2557419688511dab1c8daf475188" + integrity sha512-BzcaRsnFuznzOItW1WpQrDHM7plAa7GIDMZ6b5pnMbkqEtM/6WCOhvZar39oeMQP79gwvFUWjjptE7/KGcNqFg== "@types/node@^15.0.1": - version "15.14.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.7.tgz#29fea9a5b14e2b75c19028e1c7a32edd1e89fe92" - integrity sha512-FA45p37/mLhpebgbPWWCKfOisTjxGK9lwcHlJ6XVLfu3NgfcazOJHdYUZCWPMK8QX4LhNZdmfo6iMz9FqpUbaw== + version "15.14.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" + integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== "@types/node@^8.0.0": version "8.10.66" @@ -1387,9 +1735,16 @@ "@types/node" "*" "@types/prettier@^2.1.1": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.2.tgz#fc8c2825e4ed2142473b4a81064e6e081463d1b3" - integrity sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17" + integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA== + +"@types/prompts@^2.0.14": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.0.14.tgz#10cb8899844bb0771cabe57c1becaaaca9a3b521" + integrity sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA== + dependencies: + "@types/node" "*" "@types/qs@^6.2.31": version "6.9.7" @@ -1411,19 +1766,24 @@ "@types/node" "*" "@types/sinon-chai@^3.2.3": - version "3.2.5" - resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.5.tgz#df21ae57b10757da0b26f512145c065f2ad45c48" - integrity sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ== + version "3.2.8" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.8.tgz#5871d09ab50d671d8e6dd72e9073f8e738ac61dc" + integrity sha512-d4ImIQbT/rKMG8+AXpmcan5T2/PNeSjrYhvkwet6z0p8kzYtfgA32xzOBlbU0yqJfq+/0Ml805iFoODO0LP5/g== dependencies: "@types/chai" "*" "@types/sinon" "*" "@types/sinon@*": - version "10.0.2" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.2.tgz#f360d2f189c0fd433d14aeb97b9d705d7e4cc0e4" - integrity sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw== + version "10.0.11" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.11.tgz#8245827b05d3fc57a6601bd35aee1f7ad330fc42" + integrity sha512-dmZsHlBsKUtBpHriNjlK0ndlvEh8dcb9uV9Afsbt89QIyydpC7NcR+nWlAhASfy3GHnxTl4FX/aKE7XZUt/B4g== dependencies: - "@sinonjs/fake-timers" "^7.1.0" + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e" + integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA== "@types/through@*": version "0.0.30" @@ -1433,9 +1793,9 @@ "@types/node" "*" "@types/underscore@*": - version "1.11.3" - resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.3.tgz#d6734f3741ce41b2630018c6b61c6745f6188c07" - integrity sha512-Fl1TX1dapfXyDqFg2ic9M+vlXRktcPJrc4PR7sRc7sdVrjavg/JHlbUXBt8qWWqhJrmSqg3RNAkAPRiOYw6Ahw== + version "1.11.4" + resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.4.tgz#62e393f8bc4bd8a06154d110c7d042a93751def3" + integrity sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg== "@types/web3@1.0.19": version "1.0.19" @@ -1453,9 +1813,9 @@ winston "*" "@types/yargs-parser@*": - version "20.2.1" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" - integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^16.0.0": version "16.0.4" @@ -1464,73 +1824,81 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.9": + version "17.0.10" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" + integrity sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^4.0.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.0.tgz#b866c9cd193bfaba5e89bade0015629ebeb27996" - integrity sha512-eiREtqWRZ8aVJcNru7cT/AMVnYd9a2UHsfZT8MR1dW3UUEg6jDv9EQ9Cq4CUPZesyQ58YUpoAADGv71jY8RwgA== + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" + integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== dependencies: - "@typescript-eslint/experimental-utils" "4.29.0" - "@typescript-eslint/scope-manager" "4.29.0" + "@typescript-eslint/experimental-utils" "4.33.0" + "@typescript-eslint/scope-manager" "4.33.0" debug "^4.3.1" functional-red-black-tree "^1.0.1" + ignore "^5.1.8" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.0.tgz#19b1417602d0e1ef325b3312ee95f61220542df5" - integrity sha512-FpNVKykfeaIxlArLUP/yQfv/5/3rhl1ov6RWgud4OgbqWLkEq7lqgQU9iiavZRzpzCRQV4XddyFz3wFXdkiX9w== +"@typescript-eslint/experimental-utils@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" + integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.29.0" - "@typescript-eslint/types" "4.29.0" - "@typescript-eslint/typescript-estree" "4.29.0" + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" "@typescript-eslint/parser@^4.0.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.0.tgz#e5367ca3c63636bb5d8e0748fcbab7a4f4a04289" - integrity sha512-+92YRNHFdXgq+GhWQPT2bmjX09X7EH36JfgN2/4wmhtwV/HPxozpCNst8jrWcngLtEVd/4zAwA6BKojAlf+YqA== + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899" + integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA== dependencies: - "@typescript-eslint/scope-manager" "4.29.0" - "@typescript-eslint/types" "4.29.0" - "@typescript-eslint/typescript-estree" "4.29.0" + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" debug "^4.3.1" -"@typescript-eslint/scope-manager@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.0.tgz#cf5474f87321bedf416ef65839b693bddd838599" - integrity sha512-HPq7XAaDMM3DpmuijxLV9Io8/6pQnliiXMQUcAdjpJJSR+fdmbD/zHCd7hMkjJn04UQtCQBtshgxClzg6NIS2w== +"@typescript-eslint/scope-manager@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" + integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== dependencies: - "@typescript-eslint/types" "4.29.0" - "@typescript-eslint/visitor-keys" "4.29.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" -"@typescript-eslint/types@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.0.tgz#c8f1a1e4441ea4aca9b3109241adbc145f7f8a4e" - integrity sha512-2YJM6XfWfi8pgU2HRhTp7WgRw78TCRO3dOmSpAvIQ8MOv4B46JD2chnhpNT7Jq8j0APlIbzO1Bach734xxUl4A== +"@typescript-eslint/types@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" + integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== -"@typescript-eslint/typescript-estree@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.0.tgz#af7ab547757b86c91bfdbc54ff86845410856256" - integrity sha512-8ZpNHDIOyqzzgZrQW9+xQ4k5hM62Xy2R4RPO3DQxMc5Rq5QkCdSpk/drka+DL9w6sXNzV5nrdlBmf8+x495QXQ== +"@typescript-eslint/typescript-estree@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" + integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== dependencies: - "@typescript-eslint/types" "4.29.0" - "@typescript-eslint/visitor-keys" "4.29.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" debug "^4.3.1" globby "^11.0.3" is-glob "^4.0.1" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.0.tgz#1ff60f240def4d85ea68d4fd2e4e9759b7850c04" - integrity sha512-LoaofO1C/jAJYs0uEpYMXfHboGXzOJeV118X4OsZu9f7rG7Pr9B3+4HTU8+err81rADa4xfQmAxnRnPAI2jp+Q== +"@typescript-eslint/visitor-keys@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" + integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== dependencies: - "@typescript-eslint/types" "4.29.0" + "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" "@ungap/promise-all-settled@1.1.2": @@ -1538,27 +1906,28 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@urql/core@1.13.1": - version "1.13.1" - resolved "https://registry.yarnpkg.com/@urql/core/-/core-1.13.1.tgz#7247c27dccd7570010de91730d1f16fd15892829" - integrity sha512-Zl4UwvcE9JbWKzrtxnlmfF+rkX50GzK5dpMlB6FnUYF0sLmuGMxp67lnhTQsfTNJ+41bkj4lk0PMWEnG7KUsTw== +"@urql/core@2.3.6": + version "2.3.6" + resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.3.6.tgz#ee0a6f8fde02251e9560c5f17dce5cd90f948552" + integrity sha512-PUxhtBh7/8167HJK6WqBv6Z0piuiaZHQGYbhwpNL9aIQmLROPEdaUYkY4wh45wPQXcTpnd11l0q3Pw+TI11pdw== dependencies: + "@graphql-typed-document-node/core" "^3.1.0" wonka "^4.0.14" -"@urql/core@>=1.12.0", "@urql/core@^2.1.3": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.1.6.tgz#04cafa4e6eaad909f5e90ae48dacfc3f442183e0" - integrity sha512-wYem2NGdrFMNm9ViJo5FFoq2liU0WIp73hPppkXnVB8+U/fSwnIabTxwUy5/FsQfNVdqn3M2NgWCMU5ai79WJg== +"@urql/core@>=2.3.6", "@urql/core@^2.1.3": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.4.3.tgz#af35355cd2e3eeef4657f91616098e7cba8154dc" + integrity sha512-FpapxUKF0nLdzRLoB1QsudDjeLXJhBwzkzl8bSOJ6Cnj7LRRKJ+dYdqHfqGykswB/ILrkZks2Isp4a4BhqyUow== dependencies: - "@graphql-typed-document-node/core" "^3.1.0" + "@graphql-typed-document-node/core" "^3.1.1" wonka "^4.0.14" -"@urql/exchange-execute@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@urql/exchange-execute/-/exchange-execute-1.0.1.tgz#4cda7cbb8130c6c99f035927ca5d4756685f1ab4" - integrity sha512-7WgchTfgfPr+9KAiRlsf/MocMcjbP+1Tvlfjd5jWCiJDtIfaGA6ALx/ibAUhTRKF97zXuHfyeD3dWrodmPLuag== +"@urql/exchange-execute@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@urql/exchange-execute/-/exchange-execute-1.2.2.tgz#41a68500c44d12c4c1e6a80dfc77cefccde15687" + integrity sha512-KebdnKWMKI1NkRtIxp8YIouynOaFnhcdaMNCcJEtp+kmY4vGZUgdxT/SIzTPXXYJvk5G2aiQ/JMr97I+wM/EHA== dependencies: - "@urql/core" ">=1.12.0" + "@urql/core" ">=2.3.6" wonka "^4.0.14" "@yarnpkg/lockfile@^1.1.0": @@ -1646,19 +2015,24 @@ abstract-leveldown@~6.2.1: level-supports "~1.0.0" xtend "~4.0.0" -accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== +accepts@~1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" + mime-types "~2.1.34" + negotiator "0.6.3" acorn-jsx@^5.0.0, acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^6.0.7: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" @@ -1669,6 +2043,11 @@ acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.4.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + address@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" @@ -1715,9 +2094,9 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.6.1, ajv@^6.9.1: uri-js "^4.2.2" ajv@^8.0.1: - version "8.6.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" - integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -1734,7 +2113,7 @@ ansi-colors@3.2.3: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== -ansi-colors@4.1.1, ansi-colors@^4.1.1: +ansi-colors@4.1.1, ansi-colors@^4.1.0, ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== @@ -1757,19 +2136,19 @@ ansi-regex@^2.0.0: integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^2.2.1: version "2.2.1" @@ -1820,7 +2199,30 @@ anymatch@~3.1.1, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -arbos-precompiles@^1.0.2: +arb-bridge-eth@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/arb-bridge-eth/-/arb-bridge-eth-0.7.5.tgz#465a304b145eaad0af168e63192f714ee02cb025" + integrity sha512-OzsOqKBdU7AwmMJS1Pt27yqt/tYIteSXUIYNJYahiM7lUZk2ZsnPRnYKUc64ONDpTo/VGhO0cVXGjkrseBeJrA== + dependencies: + "@openzeppelin/contracts" "3.4.2" + "@openzeppelin/contracts-0.8" "npm:@openzeppelin/contracts@^4.3.2" + "@openzeppelin/contracts-upgradeable" "3.4.2" + hardhat "^2.6.1" + +arb-bridge-peripherals@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/arb-bridge-peripherals/-/arb-bridge-peripherals-1.0.6.tgz#91024740b96bf10fc43bf6582ee0debf93cf3d81" + integrity sha512-nrarOZ72VEo/DprRO+CPaD4dwM9EuhHqZ/pyPjTjgprLtvQ9gVewIi0O8dtM2E83JC7O3zUxHOPBtTLuJaZ/oQ== + dependencies: + "@openzeppelin/contracts" "3.4.2" + "@openzeppelin/contracts-upgradeable" "3.4.2" + arb-bridge-eth "0.7.5" + arbos-precompiles "^1.0.2" + hardhat "^2.6.1" + optionalDependencies: + "@openzeppelin/upgrades-core" "^1.7.6" + +arbos-precompiles@1.0.2, arbos-precompiles@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/arbos-precompiles/-/arbos-precompiles-1.0.2.tgz#7bebd5963aef972cd259eb41f3116ea065013ea6" integrity sha512-1dOFYFJUN0kKoofh6buZJ8qCqTs+oLGSsGzHI0trA/Pka/TCERflCRsNVxez2lihOvK7MT/a2RA8AepKtBXdPQ== @@ -1873,6 +2275,16 @@ array-back@^2.0.0: dependencies: typical "^2.6.1" +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -1888,6 +2300,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array-uniq@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -1914,9 +2331,9 @@ asn1.js@^5.2.0: safer-buffer "^2.1.0" asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== dependencies: safer-buffer "~2.1.0" @@ -1981,10 +2398,10 @@ async@^2.0.1, async@^2.1.2, async@^2.4.0, async@^2.5.0, async@^2.6.1: dependencies: lodash "^4.17.14" -async@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== +async@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== asynckit@^0.4.0: version "0.4.0" @@ -2006,10 +2423,10 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== -available-typed-arrays@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz#9e0ae84ecff20caae6a94a1c3bc39b955649b7a9" - integrity sha512-SA5mXJWrId1TaQjfxUYghbqQ/hYioKmLJvPJyDuYRtXXenFNMjj4hSSt1Cf1xsuXSXrtxrVC5Ot4eU6cOtBDdA== +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== aws-sign2@~0.7.0: version "0.7.0" @@ -2022,9 +2439,9 @@ aws4@^1.8.0: integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== axios@^0.21.1: - version "0.21.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" - integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== dependencies: follow-redirects "^1.14.0" @@ -2560,9 +2977,9 @@ balanced-match@^1.0.0: integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base-x@^3.0.2, base-x@^3.0.6, base-x@^3.0.8: - version "3.0.8" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d" - integrity sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA== + version "3.0.9" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" + integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== dependencies: safe-buffer "^5.0.1" @@ -2604,22 +3021,15 @@ bech32@1.1.4: integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== bignumber.js@^9.0.0, bignumber.js@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" - integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== + version "9.0.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673" + integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw== binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - bintrees@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" @@ -2636,13 +3046,6 @@ bip39@2.5.0: safe-buffer "^5.0.1" unorm "^1.3.3" -bip66@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" - integrity sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI= - dependencies: - safe-buffer "^5.0.1" - bl@^4.0.0, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -2653,9 +3056,9 @@ bl@^4.0.0, bl@^4.1.0: readable-stream "^3.4.0" blakejs@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.1.tgz#bf313053978b2cd4c444a48795710be05c785702" - integrity sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" + integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== blob-to-it@0.0.2: version "0.0.2" @@ -2674,31 +3077,65 @@ bn.js@4.11.6: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" integrity sha1-UzRK2xRhehP26N0s4okF0cC6MhU= -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.8.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.8.0: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.1.3: +bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.1.3, bn.js@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== -body-parser@1.19.0, body-parser@^1.16.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== +body-parser@1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.1.tgz#1499abbaa9274af3ecc9f6f10396c995943e31d4" + integrity sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA== dependencies: - bytes "3.1.0" + bytes "3.1.1" content-type "~1.0.4" debug "2.6.9" depd "~1.1.2" - http-errors "1.7.2" + http-errors "1.8.1" iconv-lite "0.4.24" on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" + qs "6.9.6" + raw-body "2.4.2" + type-is "~1.6.18" + +body-parser@1.19.2: + version "1.19.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" + integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.8.1" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.9.7" + raw-body "2.4.3" + type-is "~1.6.18" + +body-parser@^1.16.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" borc@^2.1.2: version "2.1.2" @@ -2721,6 +3158,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -2737,7 +3181,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2759,7 +3203,7 @@ browser-stdout@1.3.1: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6, browserify-aes@^1.2.0: +browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== @@ -2873,16 +3317,21 @@ buffer@^5.0.5, buffer@^5.2.1, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: ieee754 "^1.1.13" bufferutil@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.3.tgz#66724b756bed23cd7c28c4d306d7994f9943cc6b" - integrity sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw== + version "4.0.6" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.6.tgz#ebd6c67c7922a0e902f053e5d8be5ec850e48433" + integrity sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw== dependencies: - node-gyp-build "^4.2.0" + node-gyp-build "^4.3.0" -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" + integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== bytewise-core@^1.2.2: version "1.2.3" @@ -2992,9 +3441,9 @@ camelcase@^6.0.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30000844: - version "1.0.30001249" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8" - integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw== + version "1.0.30001325" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz#2b4ad19b77aa36f61f2eaf72e636d7481d55e606" + integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ== caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" @@ -3009,23 +3458,23 @@ cbor@^5.0.2: bignumber.js "^9.0.1" nofilter "^1.0.4" -cbor@^7.0.0: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cbor/-/cbor-7.0.6.tgz#ce47d1d5f946760f199f848dbbcfe762cf492fd6" - integrity sha512-rgt2RFogHGDLFU5r0kSfyeBc+de55DwYHP73KxKsQxsR5b0CYuQPH6AnJaXByiohpLdjQqj/K0SFcOV+dXdhSA== +cbor@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/cbor/-/cbor-8.1.0.tgz#cfc56437e770b73417a2ecbfc9caf6b771af60d5" + integrity sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg== dependencies: - "@cto.af/textdecoder" "^0.0.0" - nofilter "^2.0.3" + nofilter "^3.1.0" chai@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" - integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA== + version "4.3.6" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" + integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== dependencies: assertion-error "^1.1.0" check-error "^1.0.2" deep-eql "^3.0.1" get-func-name "^2.0.0" + loupe "^2.3.1" pathval "^1.1.1" type-detect "^4.0.5" @@ -3094,7 +3543,7 @@ chokidar@3.3.0: optionalDependencies: fsevents "~2.1.1" -chokidar@3.5.3: +chokidar@3.5.3, chokidar@^3.4.0: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -3109,21 +3558,6 @@ chokidar@3.5.3: optionalDependencies: fsevents "~2.3.2" -chokidar@^3.4.0: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - chownr@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -3146,14 +3580,14 @@ cids@^0.7.1: multihashes "~0.4.15" cids@^1.0.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/cids/-/cids-1.1.7.tgz#06aee89b9b5d615a7def86f2308a72bb642b7c7e" - integrity sha512-dlh+K0hMwFAFFjWQ2ZzxOhgGVNVREPdmk8cqHFui2U4sOodcemLMxdE5Ujga4cDcDQhWfldEPThkfu6KWBt1eA== + version "1.1.9" + resolved "https://registry.yarnpkg.com/cids/-/cids-1.1.9.tgz#402c26db5c07059377bcd6fb82f2a24e7f2f4a4f" + integrity sha512-l11hWRfugIcbGuTZwAM5PwpjPPjyb6UZOGwlHSnOBV5o07XhQ4gNpBN67FbODvpjyHtd+0Xs6KNvUcGBiDRsdg== dependencies: multibase "^4.0.1" multicodec "^3.0.1" multihashes "^4.0.1" - uint8arrays "^2.1.3" + uint8arrays "^3.0.0" cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -3198,9 +3632,9 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-spinners@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" - integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== cli-table3@^0.5.0: version "0.5.1" @@ -3213,19 +3647,18 @@ cli-table3@^0.5.0: colors "^1.1.2" cli-table3@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" - integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + version "0.6.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" + integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== dependencies: - object-assign "^4.1.0" string-width "^4.2.0" optionalDependencies: - colors "^1.1.2" + colors "1.4.0" cli-table@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" - integrity sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ== + version "0.3.11" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.11.tgz#ac69cdecbe81dccdba4889b9a18b7da312a9d3ee" + integrity sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ== dependencies: colors "1.0.3" @@ -3304,7 +3737,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0, color-convert@^1.9.1: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -3328,43 +3761,43 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312" - integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA== +color-string@^1.6.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" -color@3.0.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" - integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w== +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== dependencies: - color-convert "^1.9.1" - color-string "^1.5.2" + color-convert "^1.9.3" + color-string "^1.6.0" -colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== +colorette@^2.0.16: + version "2.0.16" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" + integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -colors@^1.1.2, colors@^1.2.1, colors@^1.4.0: +colors@1.4.0, colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== colorspace@1.1.x: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5" - integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== dependencies: - color "3.0.x" + color "^3.1.3" text-hex "1.0.x" combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: @@ -3388,6 +3821,26 @@ command-line-args@^4.0.7: find-replace "^1.0.3" typical "^2.6.1" +command-line-args@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^6.1.0: + version "6.1.2" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.2.tgz#2b7ccd48a93fb19bd71ca8fe9900feab00e557b0" + integrity sha512-I+0XN613reAhpBQ6icsPOTwu9cvhc9NtLtUcY2fGYuwm9JZiWBzFDA8w0PHqQjru7Xth7fM/y9TJ13+VKdjh7Q== + dependencies: + array-back "^4.0.1" + chalk "^2.4.2" + table-layout "^1.0.1" + typical "^5.2.0" + commander@2.18.0: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -3416,10 +3869,10 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -compare-versions@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" - integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== +compare-versions@^4.0.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-4.1.3.tgz#8f7b8966aef7dc4282b45dfa6be98434fc18a1a4" + integrity sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg== component-emitter@^1.2.1: version "1.3.0" @@ -3442,18 +3895,18 @@ concat-stream@^1.5.1, concat-stream@^1.6.0, concat-stream@^1.6.2: typedarray "^0.0.6" console-table-printer@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.9.0.tgz#5d75374bc94ae57048587604829cb914b282a088" - integrity sha512-20o73riqnclLYJt5ggNqP2ZjtgL5OxJPWzTVi3LyVFVtubMH0XN+oOn9outL1XI5ldJgPcjdMAWc92vRscHAKA== + version "2.11.0" + resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.11.0.tgz#704a74cb56d66267a2527f500fedcaa78ca76259" + integrity sha512-F5H5/lFciTV2OBI/xDh/x7nZAM6YPqkbkoIxr8px4B/nSvUV4ymsi4BL0bwRwrRcOgtp/LqGJGgGmkicAMd8LQ== dependencies: simple-wcswidth "^1.0.1" -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.1.2" + safe-buffer "5.2.1" content-hash@^2.5.2: version "2.5.2" @@ -3478,18 +3931,18 @@ conventional-changelog-angular@^5.0.11: q "^1.5.1" conventional-changelog-conventionalcommits@^4.3.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.1.tgz#f4c0921937050674e578dc7875f908351ccf4014" - integrity sha512-lzWJpPZhbM1R0PIzkwzGBCnAkH5RKJzJfFQZcl/D+2lsJxAwGnDKBqn/F4C1RD31GJNn8NuKWQzAZDAVXPp2Mw== + version "4.6.3" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz#0765490f56424b46f6cb4db9135902d6e5a36dc2" + integrity sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g== dependencies: compare-func "^2.0.0" lodash "^4.17.15" q "^1.5.1" conventional-commits-parser@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.2.tgz#190fb9900c6e02be0c0bca9b03d57e24982639fd" - integrity sha512-Jr9KAKgqAkwXMRHjxDwO/zOCDKod1XdAESHAGuJX38iZ7ZzVti/tvVoysO0suMsdAObp9NQ2rHSsSbnAqZ5f5g== + version "3.2.4" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz#a7d3b77758a202a9b2293d2112a8d8052c740972" + integrity sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q== dependencies: JSONStream "^1.0.4" is-text-path "^1.0.1" @@ -3510,20 +3963,20 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -cookie@^0.4.1: +cookie@0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookie@0.4.2, cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + cookiejar@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" - integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + version "2.1.3" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" + integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== copy-descriptor@^0.1.0: version "0.1.1" @@ -3531,20 +3984,25 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-pure@^3.0.1: - version "3.16.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.16.0.tgz#218e07add3f1844e53fab195c47871fc5ba18de8" - integrity sha512-wzlhZNepF/QA9yvx3ePDgNGudU5KDB8lu/TRPKelYA/QtSnkS/cLl2W+TIdEX1FAFcBr0YpY7tPDlcmXJ7AyiQ== + version "3.21.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.1.tgz#8c4d1e78839f5f46208de7230cebfb72bc3bdb51" + integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== core-js@^2.4.0, core-js@^2.5.0: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cors@2.8.5, cors@^2.8.1: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -3564,9 +4022,9 @@ cosmiconfig@^5.0.7: parse-json "^4.0.0" cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== dependencies: "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" @@ -3575,12 +4033,9 @@ cosmiconfig@^7.0.0: yaml "^1.10.0" crc-32@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" - integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== - dependencies: - exit-on-epipe "~1.0.1" - printj "~1.1.0" + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== create-ecdh@^4.0.0: version "4.0.4" @@ -3618,10 +4073,10 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-fetch@3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" - integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== +cross-fetch@3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" + integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== dependencies: node-fetch "2.6.1" @@ -3633,7 +4088,7 @@ cross-fetch@^2.1.0, cross-fetch@^2.1.1: node-fetch "^2.6.7" whatwg-fetch "^2.0.4" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -3714,10 +4169,10 @@ debug@3.2.6: dependencies: ms "^2.1.1" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" @@ -3735,13 +4190,6 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.3.3: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -3796,10 +4244,15 @@ deep-equal@~1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3, deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== defaults@^1.0.3: version "1.0.3" @@ -3875,21 +4328,31 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delete-empty@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/delete-empty/-/delete-empty-3.0.0.tgz#f8040f2669f26fa7060bc2304e9859c593b685e8" + integrity sha512-ZUyiwo76W+DYnKsL3Kim6M/UOavPdBJgDYWOmuQhYaZvJH0AXAHbUNyEDtRbBra8wqqr686+63/0azfEk1ebUQ== + dependencies: + ansi-colors "^4.1.0" + minimist "^1.2.0" + path-starts-with "^2.0.0" + rimraf "^2.6.2" + delimit-stream@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/delimit-stream/-/delimit-stream-0.1.0.tgz#9b8319477c0e5f8aeb3ce357ae305fc25ea1cd2b" integrity sha1-m4MZR3wOX4rrPONXrjBfwl6hzSs= +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -depd@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - des.js@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" @@ -3898,6 +4361,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -3978,6 +4446,11 @@ dot-prop@^5.1.0: is-obj "^2.0.0" dotenv@*: + version "16.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" + integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== + +dotenv@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== @@ -3994,26 +4467,17 @@ dotignore@~0.1.2: dependencies: minimatch "^3.0.4" -dottie@^2.0.0: +dottie@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.2.tgz#cc91c0726ce3a054ebf11c55fbc92a7f266dd154" integrity sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg== -drbg.js@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b" - integrity sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs= - dependencies: - browserify-aes "^1.0.6" - create-hash "^1.1.2" - create-hmac "^1.1.4" - duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -duplexify@^4.1.1: +duplexify@^4.1.1, duplexify@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== @@ -4037,11 +4501,11 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.47: - version "1.3.796" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.796.tgz#bd74a4367902c9d432d129f265bf4542cddd9f54" - integrity sha512-agwJFgM0FUC1UPPbQ4aII3HamaaJ09fqWGAWYHmzxDWqdmTleCHyyA0kt3fJlTd5M440IaeuBfzXzXzCotnZcQ== + version "1.4.104" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.104.tgz#60973b0a7d398efa877196e8ccb0c93d48b918d8" + integrity sha512-2kjoAyiG7uMyGRM9mx25s3HAzmQG2ayuYXxsFmYugHSDcwxREgLtscZvbL1JcW9S/OemeQ3f/SG6JhDwpnCclQ== -elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3: +elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== @@ -4054,6 +4518,11 @@ elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +emoji-regex@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.1.0.tgz#d50e383743c0f7a5945c47087295afc112e3cf66" + integrity sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -4062,12 +4531,7 @@ emoji-regex@^7.0.1: emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== enabled@2.0.x: version "2.0.0" @@ -4150,23 +4614,26 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: - version "1.18.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.5.tgz#9b10de7d4c206a3581fd5b2124233e04db49ae19" - integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA== +es-abstract@^1.18.5, es-abstract@^1.19.1: + version "1.19.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.2.tgz#8f7b696d8f15b167ae3640b4060670f3d054143f" + integrity sha512-gfSBJoZdlL2xRiOCy0g8gLMryhoe1TlimjzU99L/31Z8QEGIhVQI+EWwt5lT+AuU9SnorVupXFqqOGqGfsyO6w== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" has "^1.0.3" - has-symbols "^1.0.2" + has-symbols "^1.0.3" internal-slot "^1.0.3" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.3" - is-string "^1.0.6" - object-inspect "^1.11.0" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" object-keys "^1.1.1" object.assign "^4.1.2" string.prototype.trimend "^1.0.4" @@ -4183,15 +4650,15 @@ es-to-primitive@^1.2.1: is-symbol "^1.0.2" es5-ext@^0.10.35, es5-ext@^0.10.50: - version "0.10.53" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== + version "0.10.59" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.59.tgz#71038939730eb6f4f165f1421308fb60be363bc6" + integrity sha512-cOgyhW0tIJyQY1Kfw6Kr0viu9ZlUctVchRMZ7R0HiH3dxTSp5zJDLecwxUqPUrGKMsgBI1wd1FL+d9Jxfi4cLw== dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" -es6-iterator@~2.0.3: +es6-iterator@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= @@ -4200,7 +4667,7 @@ es6-iterator@~2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" -es6-symbol@^3.1.1, es6-symbol@~3.1.3: +es6-symbol@^3.1.1, es6-symbol@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== @@ -4241,9 +4708,9 @@ escodegen@1.8.x: source-map "~0.2.0" eslint-config-prettier@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== eslint-plugin-no-only-tests@^2.4.0: version "2.6.0" @@ -4251,9 +4718,9 @@ eslint-plugin-no-only-tests@^2.4.0: integrity sha512-T9SmE/g6UV1uZo1oHAqOvL86XWl7Pl2EpRpnLI8g/bkJu+h7XBCB+1LnubRZ2CUQXj805vh4/CYZdnqtVaEo2Q== eslint-plugin-prettier@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz#cdbad3bf1dbd2b177e9825737fe63b476a08f0c7" - integrity sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw== + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5" + integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g== dependencies: prettier-linter-helpers "^1.0.0" @@ -4445,9 +4912,9 @@ estraverse@^4.1.1: integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" @@ -4480,16 +4947,16 @@ eth-ens-namehash@2.0.8, eth-ens-namehash@^2.0.8: idna-uts46-hx "^2.3.1" js-sha3 "^0.5.7" -eth-gas-reporter@^0.2.20: - version "0.2.22" - resolved "https://registry.yarnpkg.com/eth-gas-reporter/-/eth-gas-reporter-0.2.22.tgz#bbe91f5d7b22433d26f099eeb5b20118ced0e575" - integrity sha512-L1FlC792aTf3j/j+gGzSNlGrXKSxNPXQNk6TnV5NNZ2w3jnQCRyJjDl0zUo25Cq2t90IS5vGdbkwqFQK7Ce+kw== +eth-gas-reporter@^0.2.24: + version "0.2.25" + resolved "https://registry.yarnpkg.com/eth-gas-reporter/-/eth-gas-reporter-0.2.25.tgz#546dfa946c1acee93cb1a94c2a1162292d6ff566" + integrity sha512-1fRgyE4xUB8SoqLgN3eDfpDfwEfRxh2Sz1b7wzFbyQA+9TekMmvSjjoRu9SKcSVyK+vLkLIsVbJDsTWjw195OQ== dependencies: "@ethersproject/abi" "^5.0.0-beta.146" - "@solidity-parser/parser" "^0.12.0" + "@solidity-parser/parser" "^0.14.0" cli-table3 "^0.5.0" - colors "^1.1.2" - ethereumjs-util "6.2.0" + colors "1.4.0" + ethereum-cryptography "^1.0.3" ethers "^4.0.40" fs-readdir-recursive "^1.1.0" lodash "^4.17.14" @@ -4643,15 +5110,25 @@ ethereum-cryptography@^0.1.2, ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" -ethereum-waffle@^3.3.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-3.4.0.tgz#990b3c6c26db9c2dd943bf26750a496f60c04720" - integrity sha512-ADBqZCkoSA5Isk486ntKJVjFEawIiC+3HxNqpJqONvh3YXBTNiRfXvJtGuAFLXPG91QaqkGqILEHANAo7j/olQ== +ethereum-cryptography@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.0.3.tgz#b1f8f4e702434b2016248dbb2f9fdd60c54772d8" + integrity sha512-NQLTW0x0CosoVb/n79x/TRHtfvS3hgNUPTUSCu0vM+9k6IIhHFFrAOJReneexjZsoZxMjJHnJn4lrE8EbnSyqQ== dependencies: - "@ethereum-waffle/chai" "^3.4.0" - "@ethereum-waffle/compiler" "^3.4.0" - "@ethereum-waffle/mock-contract" "^3.3.0" - "@ethereum-waffle/provider" "^3.4.0" + "@noble/hashes" "1.0.0" + "@noble/secp256k1" "1.5.5" + "@scure/bip32" "1.0.1" + "@scure/bip39" "1.0.0" + +ethereum-waffle@^3.3.0: + version "3.4.4" + resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-3.4.4.tgz#1378b72040697857b7f5e8f473ca8f97a37b5840" + integrity sha512-PA9+jCjw4WC3Oc5ocSMBj5sXvueWQeAbvCA+hUlb6oFgwwKyq5ka3bWQ7QZcjzIX+TdFkxP4IbFmoY2D8Dkj9Q== + dependencies: + "@ethereum-waffle/chai" "^3.4.4" + "@ethereum-waffle/compiler" "^3.4.4" + "@ethereum-waffle/mock-contract" "^3.4.4" + "@ethereum-waffle/provider" "^3.4.4" ethers "^5.0.1" ethereumjs-abi@0.6.5: @@ -4759,19 +5236,6 @@ ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^1.2.2, ethereumjs-tx@ ethereum-common "^0.0.18" ethereumjs-util "^5.0.0" -ethereumjs-util@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.0.tgz#23ec79b2488a7d041242f01e25f24e5ad0357960" - integrity sha512-vb0XN9J2QGdZGIEKG2vXM+kUdEivUfU6Wmi5y0cg+LRhDYKnXIZ/Lz7XjFbHRR9VIKq2lVGLzGBkA++y2nOdOQ== - dependencies: - "@types/bn.js" "^4.11.3" - bn.js "^4.11.0" - create-hash "^1.1.2" - ethjs-util "0.1.6" - keccak "^2.0.0" - rlp "^2.2.3" - secp256k1 "^3.0.1" - ethereumjs-util@6.2.1, ethereumjs-util@^6.0.0, ethereumjs-util@^6.1.0, ethereumjs-util@^6.2.0, ethereumjs-util@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69" @@ -4809,19 +5273,7 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum rlp "^2.0.0" safe-buffer "^5.1.1" -ethereumjs-util@^7.0.10, ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.0.tgz#e2b43a30bfcdbcb432a4eb42bd5f2393209b3fd5" - integrity sha512-kR+vhu++mUDARrsMMhsjjzPduRVAeundLGXucGRHF3B4oEltOUspfgCVco4kckucj3FMlLaZHUl9n7/kdmr6Tw== - dependencies: - "@types/bn.js" "^5.1.0" - bn.js "^5.1.2" - create-hash "^1.1.2" - ethereum-cryptography "^0.1.3" - ethjs-util "0.1.6" - rlp "^2.2.4" - -ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.3, ethereumjs-util@^7.1.4: +ethereumjs-util@^7.0.10, ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.3, ethereumjs-util@^7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.4.tgz#a6885bcdd92045b06f596c7626c3e89ab3312458" integrity sha512-p6KmuPCX4mZIqsQzXfmSx9Y0l2hqf+VkAiwSisW3UKUFdk8ZkAt+AYaor83z2nSi6CU2zSsXMlD80hAbNEGM0A== @@ -4885,6 +5337,42 @@ ethereumjs-wallet@0.6.5: utf8 "^3.0.0" uuid "^3.3.2" +ethers@5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.2.tgz#cd2e508c7342c44fa70392f722e8de8f2416489f" + integrity sha512-EF5W+6Wwcu6BqVwpgmyR5U2+L4c1FQzlM/02dkZOugN3KF0cG9bzHZP+TDJglmPm2/IzCEJDT7KBxzayk7SAHw== + dependencies: + "@ethersproject/abi" "5.5.0" + "@ethersproject/abstract-provider" "5.5.1" + "@ethersproject/abstract-signer" "5.5.0" + "@ethersproject/address" "5.5.0" + "@ethersproject/base64" "5.5.0" + "@ethersproject/basex" "5.5.0" + "@ethersproject/bignumber" "5.5.0" + "@ethersproject/bytes" "5.5.0" + "@ethersproject/constants" "5.5.0" + "@ethersproject/contracts" "5.5.0" + "@ethersproject/hash" "5.5.0" + "@ethersproject/hdnode" "5.5.0" + "@ethersproject/json-wallets" "5.5.0" + "@ethersproject/keccak256" "5.5.0" + "@ethersproject/logger" "5.5.0" + "@ethersproject/networks" "5.5.1" + "@ethersproject/pbkdf2" "5.5.0" + "@ethersproject/properties" "5.5.0" + "@ethersproject/providers" "5.5.1" + "@ethersproject/random" "5.5.0" + "@ethersproject/rlp" "5.5.0" + "@ethersproject/sha2" "5.5.0" + "@ethersproject/signing-key" "5.5.0" + "@ethersproject/solidity" "5.5.0" + "@ethersproject/strings" "5.5.0" + "@ethersproject/transactions" "5.5.0" + "@ethersproject/units" "5.5.0" + "@ethersproject/wallet" "5.5.0" + "@ethersproject/web" "5.5.1" + "@ethersproject/wordlists" "5.5.0" + ethers@^4.0.32, ethers@^4.0.40: version "4.0.49" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.49.tgz#0eb0e9161a0c8b4761be547396bbe2fb121a8894" @@ -4900,77 +5388,41 @@ ethers@^4.0.32, ethers@^4.0.40: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@^5.0.0, ethers@^5.0.1, ethers@^5.0.2, ethers@^5.1.3, ethers@^5.4.0, ethers@^5.4.4: - version "5.4.4" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.4.4.tgz#35cce530505b84c699da944162195cfb3f894947" - integrity sha512-zaTs8yaDjfb0Zyj8tT6a+/hEkC+kWAA350MWRp6yP5W7NdGcURRPMOpOU+6GtkfxV9wyJEShWesqhE/TjdqpMA== - dependencies: - "@ethersproject/abi" "5.4.0" - "@ethersproject/abstract-provider" "5.4.1" - "@ethersproject/abstract-signer" "5.4.1" - "@ethersproject/address" "5.4.0" - "@ethersproject/base64" "5.4.0" - "@ethersproject/basex" "5.4.0" - "@ethersproject/bignumber" "5.4.1" - "@ethersproject/bytes" "5.4.0" - "@ethersproject/constants" "5.4.0" - "@ethersproject/contracts" "5.4.1" - "@ethersproject/hash" "5.4.0" - "@ethersproject/hdnode" "5.4.0" - "@ethersproject/json-wallets" "5.4.0" - "@ethersproject/keccak256" "5.4.0" - "@ethersproject/logger" "5.4.0" - "@ethersproject/networks" "5.4.2" - "@ethersproject/pbkdf2" "5.4.0" - "@ethersproject/properties" "5.4.0" - "@ethersproject/providers" "5.4.3" - "@ethersproject/random" "5.4.0" - "@ethersproject/rlp" "5.4.0" - "@ethersproject/sha2" "5.4.0" - "@ethersproject/signing-key" "5.4.0" - "@ethersproject/solidity" "5.4.0" - "@ethersproject/strings" "5.4.0" - "@ethersproject/transactions" "5.4.0" - "@ethersproject/units" "5.4.0" - "@ethersproject/wallet" "5.4.0" - "@ethersproject/web" "5.4.0" - "@ethersproject/wordlists" "5.4.0" - -ethers@^5.0.24: - version "5.4.7" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.4.7.tgz#0fd491a5da7c9793de2d6058d76b41b1e7efba8f" - integrity sha512-iZc5p2nqfWK1sj8RabwsPM28cr37Bpq7ehTQ5rWExBr2Y09Sn1lDKZOED26n+TsZMye7Y6mIgQ/1cwpSD8XZew== - dependencies: - "@ethersproject/abi" "5.4.1" - "@ethersproject/abstract-provider" "5.4.1" - "@ethersproject/abstract-signer" "5.4.1" - "@ethersproject/address" "5.4.0" - "@ethersproject/base64" "5.4.0" - "@ethersproject/basex" "5.4.0" - "@ethersproject/bignumber" "5.4.2" - "@ethersproject/bytes" "5.4.0" - "@ethersproject/constants" "5.4.0" - "@ethersproject/contracts" "5.4.1" - "@ethersproject/hash" "5.4.0" - "@ethersproject/hdnode" "5.4.0" - "@ethersproject/json-wallets" "5.4.0" - "@ethersproject/keccak256" "5.4.0" - "@ethersproject/logger" "5.4.1" - "@ethersproject/networks" "5.4.2" - "@ethersproject/pbkdf2" "5.4.0" - "@ethersproject/properties" "5.4.1" - "@ethersproject/providers" "5.4.5" - "@ethersproject/random" "5.4.0" - "@ethersproject/rlp" "5.4.0" - "@ethersproject/sha2" "5.4.0" - "@ethersproject/signing-key" "5.4.0" - "@ethersproject/solidity" "5.4.0" - "@ethersproject/strings" "5.4.0" - "@ethersproject/transactions" "5.4.0" - "@ethersproject/units" "5.4.0" - "@ethersproject/wallet" "5.4.0" - "@ethersproject/web" "5.4.0" - "@ethersproject/wordlists" "5.4.0" +ethers@^5.0.1, ethers@^5.0.2, ethers@^5.0.24, ethers@^5.1.0, ethers@^5.4.4, ethers@^5.5.2, ethers@^5.6.0: + version "5.6.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.2.tgz#e75bac7f038c5e0fdde667dba62fc223924143a2" + integrity sha512-EzGCbns24/Yluu7+ToWnMca3SXJ1Jk1BvWB7CCmVNxyOeM4LLvw2OLuIHhlkhQk1dtOcj9UMsdkxUh8RiG1dxQ== + dependencies: + "@ethersproject/abi" "5.6.0" + "@ethersproject/abstract-provider" "5.6.0" + "@ethersproject/abstract-signer" "5.6.0" + "@ethersproject/address" "5.6.0" + "@ethersproject/base64" "5.6.0" + "@ethersproject/basex" "5.6.0" + "@ethersproject/bignumber" "5.6.0" + "@ethersproject/bytes" "5.6.1" + "@ethersproject/constants" "5.6.0" + "@ethersproject/contracts" "5.6.0" + "@ethersproject/hash" "5.6.0" + "@ethersproject/hdnode" "5.6.0" + "@ethersproject/json-wallets" "5.6.0" + "@ethersproject/keccak256" "5.6.0" + "@ethersproject/logger" "5.6.0" + "@ethersproject/networks" "5.6.1" + "@ethersproject/pbkdf2" "5.6.0" + "@ethersproject/properties" "5.6.0" + "@ethersproject/providers" "5.6.2" + "@ethersproject/random" "5.6.0" + "@ethersproject/rlp" "5.6.0" + "@ethersproject/sha2" "5.6.0" + "@ethersproject/signing-key" "5.6.0" + "@ethersproject/solidity" "5.6.0" + "@ethersproject/strings" "5.6.0" + "@ethersproject/transactions" "5.6.0" + "@ethersproject/units" "5.6.0" + "@ethersproject/wallet" "5.6.0" + "@ethersproject/web" "5.6.0" + "@ethersproject/wordlists" "5.6.0" ethjs-unit@0.1.6: version "0.1.6" @@ -5016,19 +5468,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" @@ -5059,11 +5498,6 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -exit-on-epipe@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" - integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== - expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -5077,17 +5511,17 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -express@4.17.1, express@^4.14.0: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== +express@4.17.2: + version "4.17.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.2.tgz#c18369f265297319beed4e5558753cc8c1364cb3" + integrity sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg== dependencies: accepts "~1.3.7" array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" + body-parser "1.19.1" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.0" + cookie "0.4.1" cookie-signature "1.0.6" debug "2.6.9" depd "~1.1.2" @@ -5101,24 +5535,60 @@ express@4.17.1, express@^4.14.0: on-finished "~2.3.0" parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" + proxy-addr "~2.0.7" + qs "6.9.6" range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" + safe-buffer "5.2.1" + send "0.17.2" + serve-static "1.14.2" + setprototypeof "1.2.0" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +express@^4.14.0: + version "4.17.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" + integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.19.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.4.2" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.9.7" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.17.2" + serve-static "1.14.2" + setprototypeof "1.2.0" statuses "~1.5.0" type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" ext@^1.1.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" - integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== + version "1.6.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" + integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== dependencies: - type "^2.0.0" + type "^2.5.0" extend-shallow@^2.0.1: version "2.0.1" @@ -5169,9 +5639,9 @@ extsprintf@1.3.0: integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== fake-merkle-patricia-tree@^1.0.1: version "1.0.1" @@ -5191,14 +5661,14 @@ fast-diff@^1.1.2: integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== fast-fifo@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.0.0.tgz#9bc72e6860347bb045a876d1c5c0af11e9b984e7" - integrity sha512-4VEXmjxLj7sbs8J//cn2qhRap50dGzF5n8fjay8mau+Jn4hxSeR3xPFwxMaQq/pDaq7+KQk0PAbC2+nWDkJrmQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.1.0.tgz#17d1a3646880b9891dfa0c54e69c5fef33cad779" + integrity sha512-Kl29QoNbNvn4nhDsLYjyIAaIqaJB6rBx5p3sL9VjaefJ+eMFBWVZiaoguaoZfzEKr5RhAti0UgM8703akGPJ6g== -fast-glob@^3.0.3, fast-glob@^3.1.1: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== +fast-glob@^3.0.3, fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -5217,19 +5687,19 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fast-redact@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.1.tgz#d6015b971e933d03529b01333ba7f22c29961e92" - integrity sha512-kYpn4Y/valC9MdrISg47tZOpYBNoTXKgT9GYXFpHN/jYFs+lFkPoisY+LcBODdKVMY96ATzvzsWv+ES/4Kmufw== + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.1.tgz#790fcff8f808c2e12fabbfb2be5cb2deda448fa0" + integrity sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A== -fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" - integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== +fastify-warning@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" + integrity sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw== fastq@^1.6.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.1.tgz#5d8175aae17db61947f8b162cfc7f63264d22807" - integrity sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw== + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== dependencies: reusify "^1.0.4" @@ -5273,11 +5743,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -5316,6 +5781,13 @@ find-replace@^1.0.3: array-back "^1.0.4" test-value "^2.1.0" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + find-up@3.0.0, find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -5398,20 +5870,15 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flatstr@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" - integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== - flatted@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== flatted@^3.1.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" - integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== + version "3.2.5" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== flow-stoplight@^1.0.0: version "1.0.0" @@ -5424,9 +5891,9 @@ fn.name@1.x.x: integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== follow-redirects@^1.12.1, follow-redirects@^1.14.0: - version "1.14.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" - integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== for-each@^0.3.3, for-each@~0.3.3: version "0.3.3" @@ -5525,9 +5992,9 @@ fs-extra@^0.30.0: rimraf "^2.2.8" fs-extra@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" - integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + version "10.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" + integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -5607,15 +6074,6 @@ functional-red-black-tree@^1.0.1, functional-red-black-tree@~1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -ganache-cli@^6.11.0: - version "6.12.2" - resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.12.2.tgz#c0920f7db0d4ac062ffe2375cb004089806f627a" - integrity sha512-bnmwnJDBDsOWBUP8E/BExWf85TsdDEFelQSzihSJm9VChVO1SHp94YXLP5BlA4j/OTxp0wR4R1Tje9OHOuAJVw== - dependencies: - ethereumjs-util "6.2.1" - source-map-support "0.5.12" - yargs "13.2.4" - ganache-core@^2.13.2: version "2.13.2" resolved "https://registry.yarnpkg.com/ganache-core/-/ganache-core-2.13.2.tgz#27e6fc5417c10e6e76e2e646671869d7665814a3" @@ -5697,7 +6155,7 @@ get-stream@^3.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= -get-stream@^4.0.0, get-stream@^4.1.0: +get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== @@ -5716,6 +6174,14 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -5737,9 +6203,9 @@ ghost-testrpc@^0.0.2: node-emoji "^1.10.0" git-raw-commits@^2.0.0: - version "2.0.10" - resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" - integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== + version "2.0.11" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723" + integrity sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A== dependencies: dargs "^7.0.0" lodash "^4.17.15" @@ -5766,7 +6232,7 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.2.0, glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6: +glob@7.2.0, glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6, glob@~7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -5789,18 +6255,6 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@~7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -5838,9 +6292,9 @@ globals@^11.7.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.10.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676" - integrity sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g== + version "13.13.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" + integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== dependencies: type-fest "^0.20.2" @@ -5864,15 +6318,15 @@ globby@^10.0.1: slash "^3.0.0" globby@^11.0.3: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" slash "^3.0.0" got@9.6.0: @@ -5913,26 +6367,21 @@ got@^7.1.0: url-to-options "^1.0.1" graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -graphql-tag@2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd" - integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA== + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graphql-tag@^2.12.4: - version "2.12.5" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.5.tgz#5cff974a67b417747d05c8d9f5f3cb4495d0db8f" - integrity sha512-5xNhP4063d16Pz3HBtKprutsPrmHZi5IdUGOWRxA2B6VF7BIRGOHZ5WQvDmJXZuPcBg7rYwaFxvQYjqkSdR3TQ== +graphql-tag@2.12.6, graphql-tag@^2.12.4: + version "2.12.6" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" + integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== dependencies: tslib "^2.1.0" -graphql@15.4.0: - version "15.4.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.4.0.tgz#e459dea1150da5a106486ba7276518b5295a4347" - integrity sha512-EB3zgGchcabbsU9cFe1j+yxdzKQKAbGUWRb13DsrsMN1yyfmmIq+2+L5MqVWcDCE4V89R5AyUOi7sMOGxdsYtA== +graphql@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.2.0.tgz#de3150e80f1fc009590b92a9d16ab1b46e12b656" + integrity sha512-MuQd7XXrdOcmfwuLwC2jNvx0n3rxIuNYOxUtiee5XOmfrWo613ar2U8pE7aHAKh8VwfpifubpD9IP+EdEAEOsA== growl@1.10.5: version "1.10.5" @@ -5970,24 +6419,28 @@ hard-rejection@^2.1.0: integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== hardhat-abi-exporter@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/hardhat-abi-exporter/-/hardhat-abi-exporter-2.2.1.tgz#98ad242d08987d34416c6315729f36f9d544e4fa" - integrity sha512-Um7+RPvJEj+OqWjPoPKlTTkO1Akr10pqpgMk8Pw2jz2wrGv5XQBGNW5aQgGVDUosYktUIWDaEhcwwFKbFsir9A== + version "2.8.0" + resolved "https://registry.yarnpkg.com/hardhat-abi-exporter/-/hardhat-abi-exporter-2.8.0.tgz#e7ed6216c16acf84158909d856577f0a8832ec55" + integrity sha512-HQwd9Agr2O5znUg9Dzicw8grsXacoMSQsS5ZhBBNyaxKeVbvxL1Ubm9ss8sSVGr74511a8qiR2Ljm/lsLS9Mew== + dependencies: + "@ethersproject/abi" "^5.5.0" + delete-empty "^3.0.0" hardhat-contract-sizer@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/hardhat-contract-sizer/-/hardhat-contract-sizer-2.0.3.tgz#604455fd803865f81c29f60364e863eaa19395a7" - integrity sha512-iaixOzWxwOSIIE76cl2uk4m9VXI1hKU3bFt+gl7jDhyb2/JB2xOp5wECkfWqAoc4V5lD4JtjldZlpSTbzX+nPQ== + version "2.5.1" + resolved "https://registry.yarnpkg.com/hardhat-contract-sizer/-/hardhat-contract-sizer-2.5.1.tgz#cb0b8dd32593b7a28c8d96ecde04841292bbd603" + integrity sha512-28yRb73e30aBVaZOOHTlHZFIdIasA/iFunIehrUviIJTubvdQjtSiQUo2wexHFtt71mQeMPP8qjw2sdbgatDnQ== dependencies: + chalk "^4.0.0" cli-table3 "^0.6.0" - colors "^1.4.0" hardhat-gas-reporter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.4.tgz#59e3137e38e0dfeac2e4f90d5c74160b50ad4829" - integrity sha512-G376zKh81G3K9WtDA+SoTLWsoygikH++tD1E7llx+X7J+GbIqfwhDKKgvJjcnEesMrtR9UqQHK02lJuXY1RTxw== + version "1.0.8" + resolved "https://registry.yarnpkg.com/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.8.tgz#93ce271358cd748d9c4185dbb9d1d5525ec145e0" + integrity sha512-1G5thPnnhcwLHsFnl759f2tgElvuwdkzxlI65fC9PwxYMEe9cmjkVAAWTf3/3y8uP6ZSPiUiOW8PgZnykmZe0g== dependencies: - eth-gas-reporter "^0.2.20" + array-uniq "1.0.3" + eth-gas-reporter "^0.2.24" sha1 "^1.1.1" hardhat-storage-layout@0.1.6: @@ -6004,10 +6457,10 @@ hardhat-tracer@^1.0.0-alpha.6: dependencies: ethers "^5.0.24" -hardhat@^2.6.4: - version "2.9.2" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.9.2.tgz#123f3fed6810ef8637b127b73ca44bb9c9efc249" - integrity sha512-elTcUK1EdFverWinybQ+DoJzsM6sgiHUYs0ZYNNXMfESty6ESHiFSwkfJsC88/q09vmIz6YVaMh73BYnYd+feQ== +hardhat@^2.6.1, hardhat@^2.6.4: + version "2.9.3" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.9.3.tgz#4759dc3c468c7d15f34334ca1be7d59b04e47b1e" + integrity sha512-7Vw99RbYbMZ15UzegOR/nqIYIqddZXvLwJGaX5sX4G5bydILnbjmDU6g3jMKJSiArEixS3vHAEaOs5CW1JQ3hg== dependencies: "@ethereumjs/block" "^3.6.0" "@ethereumjs/blockchain" "^5.5.0" @@ -6144,10 +6597,10 @@ has-symbol-support-x@^1.4.1: resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== -has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== has-to-string-tag-x@^1.2.0: version "1.4.1" @@ -6156,6 +6609,13 @@ has-to-string-tag-x@^1.2.0: dependencies: has-symbol-support-x "^1.4.1" +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -6229,10 +6689,10 @@ heap@0.2.6: resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw= -helmet@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.1.1.tgz#751f0e273d809ace9c172073e0003bed27d27a4a" - integrity sha512-Avg4XxSBrehD94mkRwEljnO+6RZx7AGfk8Wa6K1nxaU+hbXlFOhlOIMgPfFqOYQB/dBCsTpootTGuiOG+CHiQA== +helmet@4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df" + integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg== hmac-drbg@^1.0.1: version "1.0.1" @@ -6257,9 +6717,9 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.6.0: integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== hosted-git-info@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" - integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== dependencies: lru-cache "^6.0.0" @@ -6278,27 +6738,27 @@ http-cache-semantics@^4.0.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== +http-errors@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== dependencies: depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" + inherits "2.0.4" + setprototypeof "1.2.0" statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + toidentifier "1.0.1" -http-errors@1.7.3, http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: - depd "~1.1.2" + depd "2.0.0" inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" http-https@^1.0.0: version "1.0.0" @@ -6375,10 +6835,10 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1, ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== immediate@^3.2.3: version "3.3.0" @@ -6391,9 +6851,9 @@ immediate@~3.2.3: integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw= immutable@^4.0.0-rc.12: - version "4.0.0-rc.14" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.14.tgz#29ba96631ec10867d1348515ac4e6bdba462f071" - integrity sha512-pfkvmRKJSoW7JFx0QeYlAmT+kNYvn5j0u7bnpNq4N2RCvHSTlLT208G8jgaquNe+Q8kCPHKOSpxJkyvLDpYq0w== + version "4.0.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" + integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== import-fresh@^2.0.0: version "2.0.0" @@ -6421,10 +6881,10 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inflection@1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" - integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= +inflection@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.13.2.tgz#15e8c797c6c3dadf31aa658f8df8a4ea024798b0" + integrity sha512-cmZlljCRTBFouT8UzMzrGcVEvkv6D/wBdcdKG7J1QH5cXjtU75Dm+P27v9EKu/Y43UYyCJd1WC4zLebRrC8NBw== inflight@^1.0.4: version "1.0.6" @@ -6439,11 +6899,6 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - ini@^1.3.4, ini@^1.3.5: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -6469,9 +6924,9 @@ inquirer@^6.2.2: through "^2.3.6" inquirer@^8.0.0: - version "8.1.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.1.2.tgz#65b204d2cd7fb63400edd925dfe428bafd422e3d" - integrity sha512-DHLKJwLPNgkfwNmsuEUKSejJFbkv0FMO9SMiQbjI3n5NQuCrSIBqP66ggqyz2a6t2qEolKrMjhQ3+W/xXgUQ+Q== + version "8.2.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.2.tgz#1310517a87a0814d25336c78a20b44c3d9b7629d" + integrity sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.1" @@ -6481,9 +6936,9 @@ inquirer@^8.0.0: figures "^3.0.0" lodash "^4.17.21" mute-stream "0.0.8" - ora "^5.3.0" + ora "^5.4.1" run-async "^2.4.0" - rxjs "^7.2.0" + rxjs "^7.5.5" string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" @@ -6514,11 +6969,6 @@ invert-kv@^1.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - io-ts@1.10.4: version "1.10.4" resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-1.10.4.tgz#cd5401b138de88e4f920adbcb7026e2d1967e6e2" @@ -6663,11 +7113,12 @@ is-accessor-descriptor@^1.0.0: kind-of "^6.0.0" is-arguments@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" - integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" is-arrayish@^0.2.1: version "0.2.1" @@ -6680,9 +7131,11 @@ is-arrayish@^0.3.1: integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== is-bigint@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" - integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" is-binary-path@~2.1.0: version "2.1.0" @@ -6692,11 +7145,12 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-boolean-object@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" - integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng== + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: call-bind "^1.0.2" + has-tostringtag "^1.0.0" is-buffer@^1.1.5: version "1.1.6" @@ -6708,10 +7162,10 @@ is-buffer@~2.0.3: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" - integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== is-ci@^2.0.0: version "2.0.0" @@ -6725,21 +7179,7 @@ is-circular@^1.0.2: resolved "https://registry.yarnpkg.com/is-circular/-/is-circular-1.0.2.tgz#2e0ab4e9835f4c6b0ea2b9855a84acd501b8366c" integrity sha512-YttjnrswnUYRVJvxCvu8z+PGMUSzC2JttP0OEXezlAEdp3EXzhf7IZ3j0gRAybJBQupedIZFhY61Tga6E0qASA== -is-core-module@^2.2.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" - integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg== - dependencies: - has "^1.0.3" - -is-core-module@^2.5.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.7.0.tgz#3c0ef7d31b4acfc574f80c58409d568a836848e3" - integrity sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ== - dependencies: - has "^1.0.3" - -is-core-module@^2.8.0: +is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== @@ -6761,9 +7201,11 @@ is-data-descriptor@^1.0.0: kind-of "^6.0.0" is-date-object@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5" - integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A== + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" is-descriptor@^0.1.0: version "0.1.6" @@ -6794,9 +7236,9 @@ is-docker@^2.0.0: integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-electron@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.0.tgz#8943084f09e8b731b3a7a0298a7b5d56f6b7eef0" - integrity sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q== + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.1.tgz#751b1dd8a74907422faa5c35aaa0cf66d98086e9" + integrity sha512-r8EEQQsqT+Gn0aXFx7lTFygYQhILLCB+wn0WCDL5LZRINeLH/Rvw1j2oKodELLXYNImQ3CRlVsY8wW4cGOsyuw== is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" @@ -6848,14 +7290,16 @@ is-function@^1.0.1: integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== is-generator-function@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.9.tgz#e5f82c2323673e7fcad3d12858c83c4039f6399c" - integrity sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A== + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -6876,15 +7320,17 @@ is-ip@^3.1.0: dependencies: ip-regex "^4.0.0" -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== is-number-object@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" - integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" is-number@^3.0.0: version "3.0.0" @@ -6930,13 +7376,13 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.0.4, is-regex@^1.1.3, is-regex@~1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" - integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== +is-regex@^1.0.4, is-regex@^1.1.4, is-regex@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== dependencies: call-bind "^1.0.2" - has-symbols "^1.0.2" + has-tostringtag "^1.0.0" is-regexp@^1.0.0: version "1.0.0" @@ -6948,7 +7394,14 @@ is-retry-allowed@^1.0.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== -is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: +is-shared-array-buffer@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^1.0.0, is-stream@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -6958,10 +7411,12 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-string@^1.0.5, is-string@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" - integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" @@ -6977,16 +7432,16 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" -is-typed-array@^1.1.3: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.5.tgz#f32e6e096455e329eb7b423862456aa213f0eb4e" - integrity sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug== +is-typed-array@^1.1.3, is-typed-array@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.8.tgz#cbaa6585dc7db43318bc5b89523ea384a6f65e79" + integrity sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA== dependencies: - available-typed-arrays "^1.0.2" + available-typed-arrays "^1.0.5" call-bind "^1.0.2" - es-abstract "^1.18.0-next.2" + es-abstract "^1.18.5" foreach "^2.0.5" - has-symbols "^1.0.1" + has-tostringtag "^1.0.0" is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" @@ -7008,6 +7463,13 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7079,9 +7541,9 @@ isurl@^1.0.0-alpha5: is-object "^1.0.1" it-all@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/it-all/-/it-all-1.0.5.tgz#e880510d7e73ebb79063a76296a2eb3cb77bbbdb" - integrity sha512-ygD4kA4vp8fi+Y+NBgEKt6W06xSbv6Ub/0V8d1r3uCyJ9Izwa1UspkIOlqY9fOee0Z1w3WRo1+VWyAU4DgtufA== + version "1.0.6" + resolved "https://registry.yarnpkg.com/it-all/-/it-all-1.0.6.tgz#852557355367606295c4c3b7eff0136f07749335" + integrity sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A== it-concat@^1.0.0: version "1.0.3" @@ -7099,14 +7561,14 @@ it-glob@0.0.8: minimatch "^3.0.4" it-last@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/it-last/-/it-last-1.0.5.tgz#5c711c7d58948bcbc8e0cb129af3a039ba2a585b" - integrity sha512-PV/2S4zg5g6dkVuKfgrQfN2rUN4wdTI1FzyAvU+i8RV96syut40pa2s9Dut5X7SkjwA3P0tOhLABLdnOJ0Y/4Q== + version "1.0.6" + resolved "https://registry.yarnpkg.com/it-last/-/it-last-1.0.6.tgz#4106232e5905ec11e16de15a0e9f7037eaecfc45" + integrity sha512-aFGeibeiX/lM4bX3JY0OkVCFkAw8+n9lkukkLNivbJRvNz8lI3YXv5xcqhFUV2lDJiraEK3OXRDbGuevnnR67Q== it-map@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/it-map/-/it-map-1.0.5.tgz#2f6a9b8f0ba1ed1aeadabf86e00b38c73a1dc299" - integrity sha512-EElupuWhHVStUgUY+OfTJIS2MZed96lDrAXzJUuqiiqLnIKoBRqtX1ZG2oR0bGDsSppmz83MtzCeKLZ9TVAUxQ== + version "1.0.6" + resolved "https://registry.yarnpkg.com/it-map/-/it-map-1.0.6.tgz#6aa547e363eedcf8d4f69d8484b450bc13c9882c" + integrity sha512-XT4/RM6UHIFG9IobGlQPFQUrlEKkU4eBUFG3qhWhfAdh1JfF2x11ShCrKCdmZ0OiZppPfoLuzcfA4cey6q3UAQ== it-peekable@0.0.1: version "0.0.1" @@ -7258,10 +7720,10 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" @@ -7331,16 +7793,16 @@ jsonschema@^1.2.4: integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw== jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== dependencies: assert-plus "1.0.0" extsprintf "1.3.0" - json-schema "0.2.3" + json-schema "0.4.0" verror "1.10.0" -keccak@3.0.1, keccak@^3.0.0: +keccak@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.1.tgz#ae30a0e94dbe43414f741375cff6d64c8bea0bff" integrity sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA== @@ -7348,15 +7810,14 @@ keccak@3.0.1, keccak@^3.0.0: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" -keccak@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/keccak/-/keccak-2.1.0.tgz#734ea53f2edcfd0f42cdb8d5f4c358fef052752b" - integrity sha512-m1wbJRTo+gWbctZWay9i26v5fFnYkOn7D5PCxJ3fZUGUEb49dE1Pm4BREUYCt/aoO6di7jeoGmhvqN9Nzylm3Q== +keccak@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" + integrity sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ== dependencies: - bindings "^1.5.0" - inherits "^2.0.4" - nan "^2.14.0" - safe-buffer "^5.2.0" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + readable-stream "^3.6.0" keyv@^3.0.0: version "3.1.0" @@ -7415,13 +7876,6 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - level-codec@^9.0.0: version "9.0.2" resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-9.0.2.tgz#fd60df8c64786a80d44e63423096ffead63d8cbc" @@ -7636,9 +8090,9 @@ levn@^0.4.1: type-check "~0.4.0" lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== lint-staged@^10.5.4: version "10.5.4" @@ -7662,15 +8116,16 @@ lint-staged@^10.5.4: stringify-object "^3.3.0" listr2@^3.2.2: - version "3.11.0" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.11.0.tgz#9771b02407875aa78e73d6e0ff6541bbec0aaee9" - integrity sha512-XLJVe2JgXCyQTa3FbSv11lkKExYmEyA4jltVo8z4FX10Vt1Yj8IMekBfwim0BSOM9uj1QMTJvDQQpHyuPbB/dQ== + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== dependencies: cli-truncate "^2.1.0" - colorette "^1.2.2" + colorette "^2.0.16" log-update "^4.0.0" p-map "^4.0.0" - rxjs "^6.6.7" + rfdc "^1.3.0" + rxjs "^7.5.1" through "^2.3.8" wrap-ansi "^7.0.0" @@ -7720,10 +8175,10 @@ lodash.assign@^4.0.3, lodash.assign@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= lodash.get@^4: version "4.4.2" @@ -7745,11 +8200,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.toarray@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" - integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= - lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -7790,15 +8240,15 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" -logform@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" - integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== +logform@^2.3.2, logform@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" + integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== dependencies: - colors "^1.2.1" - fast-safe-stringify "^2.0.4" + "@colors/colors" "1.5.0" fecha "^4.2.0" ms "^2.1.1" + safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" looper@^2.0.0: @@ -7818,6 +8268,13 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +loupe@^2.3.1: + version "2.3.4" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" + integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== + dependencies: + get-func-name "^2.0.0" + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -7849,6 +8306,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.4.0: + version "7.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.7.3.tgz#98cd19eef89ce6a4a3c4502c17c833888677c252" + integrity sha512-WY9wjJNQt9+PZilnLbuFKM+SwDull9+6IAguOrarOMoOHTcJ9GnXSO11+Gw6c7xtDkBkthR57OZMtZKYr+1CEw== + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -7869,13 +8331,6 @@ make-error@^1, make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -7904,11 +8359,9 @@ markdown-table@^1.1.3: integrity sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q== mcl-wasm@^0.7.1: - version "0.7.8" - resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-0.7.8.tgz#4d0dc5a92f7bd20892fd3fcd41764acf86fd1e6e" - integrity sha512-qNHlYO6wuEtSoH5A8TcZfCEHtw8gGPqF6hLZpQn2SVd/Mck0ELIKOkmj072D98S9B9CI/jZybTUC96q1P2/ZDw== - dependencies: - typescript "^4.3.4" + version "0.7.9" + resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-0.7.9.tgz#c1588ce90042a8700c3b60e40efb339fc07ab87f" + integrity sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ== md5.js@^1.3.4: version "1.3.5" @@ -7924,15 +8377,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - memdown@^1.0.0: version "1.4.1" resolved "https://registry.yarnpkg.com/memdown/-/memdown-1.4.1.tgz#b4e4e192174664ffbae41361aa500f3119efe215" @@ -8008,7 +8452,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.2.3, merge2@^1.3.0: +merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -8077,12 +8521,12 @@ micromatch@^3.1.4: to-regex "^3.0.2" micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.2" + picomatch "^2.3.1" miller-rabin@^4.0.0: version "4.0.1" @@ -8092,17 +8536,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.49.0: - version "1.49.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" - integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.32" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" - integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== +mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.49.0" + mime-db "1.52.0" mime@1.6.0: version "1.6.0" @@ -8114,7 +8558,7 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -8146,7 +8590,21 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@*, "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.4: +minimatch@*: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +"minimatch@2 || 3", minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -8169,7 +8627,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -8209,19 +8667,26 @@ mkdirp@*, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@0.5.5, mkdirp@0.5.x, mkdirp@^0.5.1, mkdirp@^0.5.5: +mkdirp@0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" +mkdirp@0.5.x, mkdirp@^0.5.1, mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + mnemonist@^0.38.0: - version "0.38.3" - resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.3.tgz#35ec79c1c1f4357cfda2fe264659c2775ccd7d9d" - integrity sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw== + version "0.38.5" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade" + integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg== dependencies: - obliterator "^1.6.1" + obliterator "^2.0.0" mocha@^7.1.1: version "7.2.0" @@ -8288,17 +8753,17 @@ mock-fs@^4.1.0: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== -moment-timezone@^0.5.31: - version "0.5.33" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" - integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== +moment-timezone@^0.5.34: + version "0.5.34" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.26.0: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== +"moment@>= 2.9.0", moment@^2.29.1: + version "2.29.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" + integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== morgan@1.10.0: version "1.10.0" @@ -8369,9 +8834,9 @@ multibase@^3.0.0, multibase@^3.1.0: web-encoding "^1.0.6" multibase@^4.0.1: - version "4.0.4" - resolved "https://registry.yarnpkg.com/multibase/-/multibase-4.0.4.tgz#55ef53e6acce223c5a09341a8a3a3d973871a577" - integrity sha512-8/JmrdSGzlw6KTgAJCOqUBSGd1V6186i/X8dDCGy/lbCKrQ+1QB6f3HE+wPr7Tpdj4U3gutaj9jG2rNX6UpiJg== + version "4.0.6" + resolved "https://registry.yarnpkg.com/multibase/-/multibase-4.0.6.tgz#6e624341483d6123ca1ede956208cb821b440559" + integrity sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ== dependencies: "@multiformats/base-x" "^4.0.1" @@ -8407,17 +8872,17 @@ multicodec@^2.0.0: varint "^6.0.0" multicodec@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-3.1.0.tgz#bc96faee2118d1ff114a3ee9e870a030a3b65743" - integrity sha512-f6d4DhbQ9a8WiJ/wpbKgeJSeR0/juP/1wnjbKdZ0KAWDkC/z7Lb3xOegMUG+uTcfwSYf6j1eTvFf8HDgqPRGmQ== + version "3.2.1" + resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-3.2.1.tgz#82de3254a0fb163a107c1aab324f2a91ef51efb2" + integrity sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw== dependencies: - uint8arrays "^2.1.5" + uint8arrays "^3.0.0" varint "^6.0.0" multiformats@^9.4.2: - version "9.4.4" - resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.4.4.tgz#a4e7181705c8e4c8ec2fd62e08a31526697f2503" - integrity sha512-lGAP3Cuc4nHRq5q9EQZFGegXBlElmlfcz7d2xsDO/u4TG7M2kkdsGOaZPe9FIbfWugWPx643VTXgZctqqJfTzg== + version "9.6.4" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.6.4.tgz#5dce1f11a407dbb69aa612cb7e5076069bb759ca" + integrity sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg== multihashes@^0.4.15, multihashes@~0.4.15: version "0.4.21" @@ -8438,25 +8903,25 @@ multihashes@^3.0.1: varint "^6.0.0" multihashes@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-4.0.2.tgz#d76aeac3a302a1bed9fe1ec964fb7a22fa662283" - integrity sha512-xpx++1iZr4ZQHjN1mcrXS6904R36LWLxX/CBifczjtmrtCXEX623DMWOF1eiNSg+pFpiZDFVBgou/4v6ayCHSQ== + version "4.0.3" + resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-4.0.3.tgz#426610539cd2551edbf533adeac4c06b3b90fb05" + integrity sha512-0AhMH7Iu95XjDLxIeuCOOE4t9+vQZsACyKZ9Fxw2pcsRmlX4iCn1mby0hS0bb+nQOVpdQYWPpnyusw4da5RPhA== dependencies: multibase "^4.0.1" - uint8arrays "^2.1.3" + uint8arrays "^3.0.0" varint "^5.0.2" multihashing-async@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/multihashing-async/-/multihashing-async-2.1.2.tgz#9ed68f183bde70e0416b166bbc59a0c0623a0ede" - integrity sha512-FTPNnWWxwIK5dXXmTFhySSF8Fkdqf7vzqpV09+RWsmfUhrsL/b3Arg3+bRrBnXTtjxm3JRGI3wSAtQHL0QCxhQ== + version "2.1.4" + resolved "https://registry.yarnpkg.com/multihashing-async/-/multihashing-async-2.1.4.tgz#26dce2ec7a40f0e7f9e732fc23ca5f564d693843" + integrity sha512-sB1MiQXPSBTNRVSJc2zM157PXgDtud2nMFUEIvBrsq5Wv96sUclMRK/ecjoP1T/W61UJBqt4tCTwMkUpt2Gbzg== dependencies: blakejs "^1.1.0" err-code "^3.0.0" js-sha3 "^0.8.0" multihashes "^4.0.1" murmurhash3js-revisited "^3.0.0" - uint8arrays "^2.1.3" + uint8arrays "^3.0.0" murmurhash3js-revisited@^3.0.0: version "3.0.0" @@ -8473,11 +8938,6 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.14.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" - integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== - nano-json-stream-parser@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz#0cc8f6d0e2b622b479c40d499c46d64b755c6f5f" @@ -8489,9 +8949,9 @@ nanoid@3.3.1: integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== nanoid@^3.0.2, nanoid@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== + version "3.3.2" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557" + integrity sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA== nanomatch@^1.2.9: version "1.2.13" @@ -8520,20 +8980,20 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== neo-async@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next-tick@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== ngeohash@0.6.3: version "0.6.3" @@ -8551,11 +9011,11 @@ node-addon-api@^2.0.0: integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== node-emoji@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" - integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw== + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== dependencies: - lodash.toarray "^4.4.0" + lodash "^4.17.21" node-environment-flags@1.0.6: version "1.0.6" @@ -8585,22 +9045,20 @@ node-fetch@~1.7.1: encoding "^0.1.11" is-stream "^1.0.1" -node-gyp-build@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" - integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== +node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" + integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ== nofilter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.4.tgz#78d6f4b6a613e7ced8b015cec534625f7667006e" integrity sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA== -nofilter@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-2.0.3.tgz#f5460f3cb33147005883e3f5d4476239501fa187" - integrity sha512-FbuXC+lK+GU2+63D1kC1ETiZo+Z7SIi7B+mxKTCH1byrh6WFvfBCN/wpherFz0a0bjGd7EKTst/cz0yLeNngug== - dependencies: - "@cto.af/textdecoder" "^0.0.0" +nofilter@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-3.1.0.tgz#c757ba68801d41ff930ba2ec55bab52ca184aa66" + integrity sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g== nopt@3.x: version "3.0.6" @@ -8639,13 +9097,6 @@ normalize-url@^4.1.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -8685,10 +9136,10 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.11.0, object-inspect@^1.9.0, object-inspect@~1.11.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" - integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== +object-inspect@^1.12.0, object-inspect@^1.9.0, object-inspect@~1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== object-is@^1.0.1: version "1.1.5" @@ -8736,13 +9187,13 @@ object.assign@^4.1.2: object-keys "^1.1.1" object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" - integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" + integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" + es-abstract "^1.19.1" object.pick@^1.3.0: version "1.3.0" @@ -8751,10 +9202,10 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -obliterator@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3" - integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig== +obliterator@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21" + integrity sha512-g0TrA7SbUggROhDPK8cEu/qpItwH2LSKcNl4tlfBNT54XY+nOsqrs0Q68h1V9b3HOSpIWv15jb1lax2hAggdIg== oboe@2.1.4: version "2.1.4" @@ -8770,6 +9221,18 @@ oboe@2.1.5: dependencies: http-https "^1.0.0" +on-exit-leak-free@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209" + integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -8842,7 +9305,7 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -ora@^5.3.0: +ora@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== @@ -8869,15 +9332,6 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -8893,11 +9347,6 @@ p-cancelable@^1.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-defer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-3.0.0.tgz#d1dceb4ee9b2b604b1d94ffec83760175d4e6f83" @@ -8916,11 +9365,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -9043,9 +9487,9 @@ parse-duration@^0.4.4: integrity sha512-KbAJuYGUhZkB9gotDiKLnZ7Z3VTacK3fgwmDdB6ZVDtJbMBT6MfLga0WJaYpPDu0mzqT0NgHtHDt5PY4l0nidg== parse-headers@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.4.tgz#9eaf2d02bed2d1eff494331ce3df36d7924760bf" - integrity sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw== + version "2.0.5" + resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.5.tgz#069793f9356a54008571eb7f9761153e6c770da9" + integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA== parse-json@^2.2.0: version "2.2.0" @@ -9151,7 +9595,7 @@ path-is-inside@^1.0.2: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= -path-key@^2.0.0, path-key@^2.0.1: +path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= @@ -9166,6 +9610,11 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-starts-with@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-starts-with/-/path-starts-with-2.0.0.tgz#ffd6d51926cd497022b44d392196033d5451892f" + integrity sha512-3UHTHbJz5+NLkPafFR+2ycJOjoc4WV2e9qCZCnm71zHiWaFrm1XniLVTkZXvaRgxr1xFh9JsTdicpH2yM03nLA== + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -9206,29 +9655,29 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -pg-connection-string@^2.4.0: +pg-connection-string@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== -pg-hstore@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.3.tgz#d1978c12a85359830b1388d3b0ff233b88928e96" - integrity sha512-qpeTpdkguFgfdoidtfeTho1Q1zPVPbtMHgs8eQ+Aan05iLmIs3Z3oo5DOZRclPGoQ4i68I1kCtQSJSa7i0ZVYg== +pg-hstore@2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.4.tgz#4425e3e2a3e15d2a334c35581186c27cf2e9b8dd" + integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA== dependencies: - underscore "^1.7.0" + underscore "^1.13.1" pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== -pg-pool@^3.2.2: - version "3.4.1" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.4.1.tgz#0e71ce2c67b442a5e862a9c182172c37eda71e9c" - integrity sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ== +pg-pool@^3.4.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.5.1.tgz#f499ce76f9bf5097488b3b83b19861f28e4ed905" + integrity sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ== -pg-protocol@^1.3.0: +pg-protocol@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== @@ -9244,30 +9693,30 @@ pg-types@^2.1.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@8.4.2: - version "8.4.2" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.4.2.tgz#2aa58166a23391e91d56a7ea57c6d99931c0642a" - integrity sha512-E9FlUrrc7w3+sbRmL1CSw99vifACzB2TjhMM9J5w9D1LIg+6un0jKkpHS1EQf2CWhKhec2bhrBLVMmUBDbjPRQ== +pg@8.7.1: + version "8.7.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.7.1.tgz#9ea9d1ec225980c36f94e181d009ab9f4ce4c471" + integrity sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA== dependencies: buffer-writer "2.0.0" packet-reader "1.0.0" - pg-connection-string "^2.4.0" - pg-pool "^3.2.2" - pg-protocol "^1.3.0" + pg-connection-string "^2.5.0" + pg-pool "^3.4.1" + pg-protocol "^1.5.0" pg-types "^2.1.0" pgpass "1.x" pgpass@1.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.4.tgz#85eb93a83800b20f8057a2b029bf05abaf94ea9c" - integrity sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w== + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== dependencies: - split2 "^3.1.1" + split2 "^4.1.0" -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== pify@^2.0.0, pify@^2.3.0: version "2.3.0" @@ -9291,46 +9740,57 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= -pino-multi-stream@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/pino-multi-stream/-/pino-multi-stream-5.1.1.tgz#abd634705b48bcb0e6b8d5c0145b82c2e45475f7" - integrity sha512-P9z/5OXRaaZJulNcxvvvV4p+m1tmiuKC4RT3I5NixOQwCWj9uHbDQaXP/NLtKgFqjrGHl5bkIb73nIvWe7KnbQ== +pino-abstract-transport@v0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz#4b54348d8f73713bfd14e3dc44228739aa13d9c0" + integrity sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ== dependencies: - pino "^6.0.0" + duplexify "^4.1.2" + split2 "^4.0.0" -pino-std-serializers@^2.4.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.5.0.tgz#40ead781c65a0ce7ecd9c1c33f409d31fe712315" - integrity sha512-wXqbqSrIhE58TdrxxlfLwU9eDhrzppQDvGhBEr1gYbzzM4KKo3Y63gSjiDXRKLVS2UOXdPNR2v+KnQgNrs+xUg== +pino-multi-stream@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pino-multi-stream/-/pino-multi-stream-6.0.0.tgz#2116bca740cb5eb606f430b20fd480f4944b2b99" + integrity sha512-oCuTtaDSUB5xK1S45r9oWE0Dj8RWdHVvaGTft5pO/rmzgIqQRkilf5Ooilz3uRm0IYj8sPRho3lVx48LCmXjvQ== + dependencies: + pino "^7.0.0" -pino-std-serializers@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz#b56487c402d882eb96cd67c257868016b61ad671" - integrity sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg== +pino-std-serializers@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1791ccd2539c091ae49ce9993205e2cd5dbba1e2" + integrity sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q== -pino@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-6.7.0.tgz#d5d96b7004fed78816b5694fda3eab02b5ca6d23" - integrity sha512-vPXJ4P9rWCwzlTJt+f0Ni4THc3DWyt8iDDCO4edQ8narTu6hnpzdXu8FqeSJCGndl1W6lfbYQUQihUO54y66Lw== +pino@7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-7.6.0.tgz#f9abd13e221e15855e3cf6e88a2b6280527b3800" + integrity sha512-CCCdryvM/chT0CDt9jQ1//z62RpSXPrzUFUpY4b8eKCVq3T2T3UF6DomoczkPze9d6VFiTyVF6Y8A6F9iAyAxg== dependencies: fast-redact "^3.0.0" - fast-safe-stringify "^2.0.7" - flatstr "^1.0.12" - pino-std-serializers "^2.4.2" - quick-format-unescaped "^4.0.1" - sonic-boom "^1.0.2" + fastify-warning "^0.2.0" + on-exit-leak-free "^0.2.0" + pino-abstract-transport v0.5.0 + pino-std-serializers "^4.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.1.0" + safe-stable-stringify "^2.1.0" + sonic-boom "^2.2.1" + thread-stream "^0.13.0" -pino@^6.0.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-6.13.0.tgz#41810b9be213af6f8f7c23a1b17058d880267e7b" - integrity sha512-mRXSTfa34tbfrWqCIp1sUpZLqBhcoaGapoyxfEwaWwJGMpLijlRdDKIQUyvq4M3DUfFH5vEglwSw8POZYwbThA== +pino@^7.0.0: + version "7.9.2" + resolved "https://registry.yarnpkg.com/pino/-/pino-7.9.2.tgz#0519ac79b9db17f0c63bb4267f7e8133f5c517ad" + integrity sha512-c8wmB2PuhdJurYPRl/eo3+PosHe7Ep6GZvBJFIrp9oV1YRZQ3qm3MujaEolaKUfwX8cDL96WKCWWMedB2drXqw== dependencies: fast-redact "^3.0.0" - fast-safe-stringify "^2.0.8" - flatstr "^1.0.12" - pino-std-serializers "^3.1.0" + on-exit-leak-free "^0.2.0" + pino-abstract-transport v0.5.0 + pino-std-serializers "^4.0.0" + process-warning "^1.0.0" quick-format-unescaped "^4.0.3" - sonic-boom "^1.0.2" + real-require "^0.1.0" + safe-stable-stringify "^2.1.0" + sonic-boom "^2.2.1" + thread-stream "^0.15.0" please-upgrade-node@^3.2.0: version "3.2.0" @@ -9404,16 +9864,16 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier-plugin-solidity@^1.0.0-beta.9: - version "1.0.0-beta.17" - resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.17.tgz#fc0fe977202b6503763a338383efeceaa6c7661e" - integrity sha512-YFkxV/rHi1mphi17/XKcJ9QjZlb+L/J0yY2erix21BZfzPv2BN9dfmSRGr/poDp/FBOFSW+jteP2BCMe7HndVQ== + version "1.0.0-beta.19" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.19.tgz#7c3607fc4028f5e6a425259ff03e45eedf733df3" + integrity sha512-xxRQ5ZiiZyUoMFLE9h7HnUDXI/daf1tnmL1msEdcKmyh7ZGQ4YklkYLC71bfBpYU2WruTb5/SFLUaEb3RApg5g== dependencies: - "@solidity-parser/parser" "^0.13.2" - emoji-regex "^9.2.2" + "@solidity-parser/parser" "^0.14.0" + emoji-regex "^10.0.0" escape-string-regexp "^4.0.0" semver "^7.3.5" solidity-comments-extractor "^0.0.7" - string-width "^4.2.2" + string-width "^4.2.3" prettier@^1.14.3: version "1.19.1" @@ -9421,14 +9881,9 @@ prettier@^1.14.3: integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== prettier@^2.1.2, prettier@^2.2.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" - integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== - -printj@~1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" - integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== + version "2.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" + integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== private@^0.1.6, private@^0.1.8: version "0.1.8" @@ -9440,6 +9895,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-warning@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" + integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -9450,10 +9910,10 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prom-client@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-12.0.0.tgz#9689379b19bd3f6ab88a9866124db9da3d76c6ed" - integrity sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ== +prom-client@14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-14.0.1.tgz#bdd9583e02ec95429677c0e013712d42ef1f86a8" + integrity sha512-HxTArb6fkOntQHoRGvv4qd/BkorjliiuO2uSWC2KC17MUTKYttWdDoXX/vxOhQdkoECEM9BBH0pj2l8G8kev6w== dependencies: tdigest "^0.1.1" @@ -9482,21 +9942,21 @@ proper-lockfile@^4.1.1: signal-exit "^3.0.2" protocol-buffers-schema@^3.3.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz#8388e768d383ac8cbea23e1280dfadb79f4122ad" - integrity sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw== + version "3.6.0" + resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" + integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== protons@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/protons/-/protons-2.0.1.tgz#bfee5123c100001dcf56ab8f71b1b36f2e8289f1" - integrity sha512-FlmPorLEeCEDPu+uIn0Qardgiy5XqVA4IyNTz9wb9c0e2U7BEXdRcIbx64r09o4Abtf+4B7mkTtMbsIXMxZzKw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/protons/-/protons-2.0.3.tgz#94f45484d04b66dfedc43ad3abff1e8907994bb2" + integrity sha512-j6JikP/H7gNybNinZhAHMN07Vjr1i4lVupg598l4I9gSTjJqOvKnwjzYX2PzvBTSVf2eZ2nWv4vG+mtW8L6tpA== dependencies: protocol-buffers-schema "^3.3.1" signed-varint "^2.0.1" - uint8arrays "^2.1.3" + uint8arrays "^3.0.0" varint "^5.0.0" -proxy-addr@~2.0.5: +proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -9616,22 +10076,27 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -qs@^6.4.0, qs@^6.7.0: - version "6.10.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" - integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== +qs@6.10.3, qs@^6.4.0, qs@^6.7.0: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== dependencies: side-channel "^1.0.4" +qs@6.9.6: + version "6.9.6" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" + integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== + +qs@6.9.7: + version "6.9.7" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" + integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== + qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== query-string@^5.0.1: version "5.1.1" @@ -9652,10 +10117,10 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quick-format-unescaped@^4.0.1, quick-format-unescaped@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz#6d6b66b8207aa2b35eef12be1421bb24c428f652" - integrity sha512-MaL/oqh02mhEo5m5J2rwsVL23Iw2PEaGVHgT2vFt8AAsr0lfvQA5dpXo9TPu0rz7tSBdUPgkbam0j/fj5ZM8yg== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== quick-lru@^4.0.1: version "4.0.1" @@ -9682,23 +10147,33 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== +raw-body@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" + integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== dependencies: - bytes "3.1.0" - http-errors "1.7.2" + bytes "3.1.1" + http-errors "1.8.1" iconv-lite "0.4.24" unpipe "1.0.0" -raw-body@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" - integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== +raw-body@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" + integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== dependencies: - bytes "3.1.0" - http-errors "1.7.3" + bytes "3.1.2" + http-errors "1.8.1" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@2.5.1, raw-body@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" iconv-lite "0.4.24" unpipe "1.0.0" @@ -9757,7 +10232,7 @@ readable-stream@^1.0.33: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.2.8, readable-stream@^2.2.9, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.2.8, readable-stream@^2.2.9, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -9794,6 +10269,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381" + integrity sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg== + receptacle@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/receptacle/-/receptacle-1.3.2.tgz#a7994c7efafc7a01d0e2041839dab6c4951360d2" @@ -9823,6 +10303,11 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + regenerate@^1.2.1: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" @@ -9851,9 +10336,9 @@ regex-not@^1.0.0, regex-not@^1.0.2: safe-regex "^1.1.0" regexp.prototype.flags@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" - integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" + integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" @@ -10031,23 +10516,15 @@ resolve@1.17.0: dependencies: path-parse "^1.0.6" -resolve@^1.1.6: - version "1.21.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f" - integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA== +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.8.1, resolve@~1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== dependencies: - is-core-module "^2.8.0" + is-core-module "^2.8.1" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.10.0, resolve@^1.8.1, resolve@~1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -10083,10 +10560,10 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry-as-promised@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-3.2.0.tgz#769f63d536bec4783549db0777cb56dadd9d8543" - integrity sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg== +retry-as-promised@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-4.0.0.tgz#e19dc63474860f751e371ccccbfa16f6c1efeaa0" + integrity sha512-zuqltYoBckZPoqLjC0eyvGpmM/psgpcreq0PLYVzBSb0Xq382XJrKNgu+fgHDy9U3R66adgFe5Viyx3D+gRvXA== dependencies: any-promise "^1.3.0" @@ -10100,6 +10577,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -10107,7 +10589,7 @@ rimraf@2.6.3: dependencies: glob "^7.1.3" -rimraf@^2.2.8, rimraf@^2.6.3: +rimraf@^2.2.8, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -10130,11 +10612,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: inherits "^2.0.1" rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3, rlp@^2.2.4: - version "2.2.6" - resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.6.tgz#c80ba6266ac7a483ef1e69e8e2f056656de2fb2c" - integrity sha512-HAfAmL6SDYNWPUOJNrM500x4Thn4PZsEy5pijPh40U9WfNk0z15hUYzO9xVIMAdIHdFtD8CBDHd75Td1g36Mjg== + version "2.2.7" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf" + integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== dependencies: - bn.js "^4.11.1" + bn.js "^5.2.0" run-async@^2.2.0, run-async@^2.4.0: version "2.4.1" @@ -10160,26 +10642,26 @@ rustbn.js@~0.2.0: resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca" integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== -rxjs@^6.4.0, rxjs@^6.6.7: +rxjs@^6.4.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== dependencies: tslib "^1.9.0" -rxjs@^7.2.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.3.0.tgz#39fe4f3461dc1e50be1475b2b85a0a88c1e938c6" - integrity sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw== +rxjs@^7.2.0, rxjs@^7.5.1, rxjs@^7.5.5: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== dependencies: - tslib "~2.1.0" + tslib "^2.1.0" safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -10198,6 +10680,11 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +safe-stable-stringify@^2.1.0, safe-stable-stringify@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" + integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -10240,26 +10727,12 @@ scryptsy@^1.2.1: dependencies: pbkdf2 "^3.0.3" -secp256k1@^3.0.1: - version "3.8.0" - resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.8.0.tgz#28f59f4b01dbee9575f56a47034b7d2e3b3b352d" - integrity sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw== - dependencies: - bindings "^1.5.0" - bip66 "^1.1.5" - bn.js "^4.11.8" - create-hash "^1.2.0" - drbg.js "^1.0.1" - elliptic "^6.5.2" - nan "^2.14.0" - safe-buffer "^5.1.2" - secp256k1@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.2.tgz#15dd57d0f0b9fdb54ac1fa1694f40e5e9a54f4a1" - integrity sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg== + version "4.0.3" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" + integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== dependencies: - elliptic "^6.5.2" + elliptic "^6.5.4" node-addon-api "^2.0.0" node-gyp-build "^4.2.0" @@ -10288,7 +10761,7 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.3.5, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: +semver@7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -10300,15 +10773,22 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.2.1, semver@^7.3.4, semver@^7.3.5: + version "7.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.6.tgz#5d73886fb9c0c6602e79440b97165c29581cbb2b" + integrity sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w== + dependencies: + lru-cache "^7.4.0" + semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" integrity sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg== -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== +send@0.17.2: + version "0.17.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" + integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== dependencies: debug "2.6.9" depd "~1.1.2" @@ -10317,35 +10797,37 @@ send@0.17.1: escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.7.2" + http-errors "1.8.1" mime "1.6.0" - ms "2.1.1" + ms "2.1.3" on-finished "~2.3.0" range-parser "~1.2.1" statuses "~1.5.0" -sequelize-pool@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-6.1.0.tgz#caaa0c1e324d3c2c3a399fed2c7998970925d668" - integrity sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg== +sequelize-pool@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz#210b391af4002762f823188fd6ecfc7413020768" + integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg== -sequelize@6.3.5: - version "6.3.5" - resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.3.5.tgz#80e3db7ac8b76d98c45ca93334197eb6e2335158" - integrity sha512-MiwiPkYSA8NWttRKAXdU9h0TxP6HAc1fl7qZmMO/VQqQOND83G4nZLXd0kWILtAoT9cxtZgFqeb/MPYgEeXwsw== +sequelize@6.12.0: + version "6.12.0" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.12.0.tgz#065993b35370dab443086b2e98e684d690c7a7f4" + integrity sha512-vn1Mq98EnwXM/xtohBcF9dQsIbgeTm7u8elwD40wu4bgnXOGTjKCVqGTekySMjnnihM06rIbDbpcLA4uwNTyCw== dependencies: - debug "^4.1.1" - dottie "^2.0.0" - inflection "1.12.0" - lodash "^4.17.15" - moment "^2.26.0" - moment-timezone "^0.5.31" - retry-as-promised "^3.2.0" - semver "^7.3.2" - sequelize-pool "^6.0.0" + "@types/debug" "^4.1.7" + debug "^4.3.3" + dottie "^2.0.2" + inflection "^1.13.1" + lodash "^4.17.21" + moment "^2.29.1" + moment-timezone "^0.5.34" + pg-connection-string "^2.5.0" + retry-as-promised "^4.0.0" + semver "^7.3.5" + sequelize-pool "^7.1.0" toposort-class "^1.0.1" - uuid "^8.1.0" - validator "^10.11.0" + uuid "^8.3.2" + validator "^13.7.0" wkx "^0.5.0" serialize-javascript@6.0.0: @@ -10355,15 +10837,15 @@ serialize-javascript@6.0.0: dependencies: randombytes "^2.1.0" -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== +serve-static@1.14.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" + integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.17.1" + send "0.17.2" servify@^0.1.12: version "0.1.12" @@ -10406,10 +10888,10 @@ setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" @@ -10469,15 +10951,10 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -signal-exit@^3.0.3: - version "3.0.5" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" - integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signed-varint@^2.0.1: version "2.0.1" @@ -10632,11 +11109,11 @@ solhint-plugin-prettier@^0.0.5: prettier-linter-helpers "^1.0.0" solhint@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.3.6.tgz#abe9af185a9a7defefba480047b3e42cbe9a1210" - integrity sha512-HWUxTAv2h7hx3s3hAab3ifnlwb02ZWhwFU/wSudUHqteMS3ll9c+m1FlGn9V8ztE2rf3Z82fQZA005Wv7KpcFA== + version "3.3.7" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.3.7.tgz#b5da4fedf7a0fee954cb613b6c55a5a2b0063aa7" + integrity sha512-NjjjVmXI3ehKkb3aNtRJWw55SUVJ8HMKKodwe0HnejA+k0d2kmhw7jvpa+MCTbcEgt8IWSwx0Hu6aCo/iYOZzQ== dependencies: - "@solidity-parser/parser" "^0.13.2" + "@solidity-parser/parser" "^0.14.1" ajv "^6.6.1" antlr4 "4.7.1" ast-parents "0.0.1" @@ -10654,9 +11131,9 @@ solhint@^3.3.6: prettier "^1.14.3" solidity-ast@^0.4.15: - version "0.4.26" - resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.26.tgz#6e644bdb897c245dd07695a63ffa95edfe02c11f" - integrity sha512-UR9Ip3QoiEvNON5lOA28JNEzKT+1fLFA4xpIbZSEl4CEnYr/a4Pj0qMJh0652UQ51pKplI/nncZsDOMzdHdCcg== + version "0.4.31" + resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.31.tgz#c63e42f894cd047826a05dbb8d1e1dfc17282d39" + integrity sha512-kX6o4XE4ihaqENuRRTMJfwQNHoqWusPENZUlX4oVb19gQdfi7IswFWnThONHSW/61umgfWdKtCBgW45iuOTryQ== solidity-comments-extractor@^0.0.7: version "0.0.7" @@ -10664,17 +11141,16 @@ solidity-comments-extractor@^0.0.7: integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== solidity-coverage@^0.7.16: - version "0.7.16" - resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.16.tgz#c8c8c46baa361e2817bbf275116ddd2ec90a55fb" - integrity sha512-ttBOStywE6ZOTJmmABSg4b8pwwZfYKG8zxu40Nz+sRF5bQX7JULXWj/XbX0KXps3Fsp8CJXg8P29rH3W54ipxw== + version "0.7.20" + resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.20.tgz#246e9b0dd62f698bb8ddeecdcc46cab26c48b637" + integrity sha512-edOXTugUYdqxrtEnIn4vgrGjLPxdexcL0WD8LzAvVA3d1dwgcfRO3k8xQR02ZQnOnWMBi8Cqs0F+kAQQp3JW8g== dependencies: - "@solidity-parser/parser" "^0.12.0" + "@solidity-parser/parser" "^0.14.0" "@truffle/provider" "^0.2.24" chalk "^2.4.2" death "^1.1.0" detect-port "^1.3.0" fs-extra "^8.1.0" - ganache-cli "^6.11.0" ghost-testrpc "^0.0.2" global-modules "^2.0.0" globby "^10.0.1" @@ -10688,13 +11164,12 @@ solidity-coverage@^0.7.16: shelljs "^0.8.3" web3-utils "^1.3.0" -sonic-boom@^1.0.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" - integrity sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg== +sonic-boom@^2.2.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.6.0.tgz#8786fc78be07c18a90381acd816d1d4afe3537a2" + integrity sha512-6xYZFRmDEtxGqfOKcDQ4cPLrNa0SPEDI+wlzDAHowXE6YV42NeXqg9mP2KkiM8JVu3lHfZ2iQKYlGOz+kTpphg== dependencies: atomic-sleep "^1.0.0" - flatstr "^1.0.12" source-map-resolve@^0.5.0: version "0.5.3" @@ -10723,9 +11198,9 @@ source-map-support@^0.4.15: source-map "^0.5.6" source-map-support@^0.5.13, source-map-support@^0.5.17: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -10774,9 +11249,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f" - integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ== + version "3.0.11" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" + integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -10792,15 +11267,20 @@ split2@^3.0.0, split2@^3.1.1: dependencies: readable-stream "^3.0.0" +split2@^4.0.0, split2@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" + integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -10837,6 +11317,11 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -10877,6 +11362,11 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== +string-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" + integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -10903,23 +11393,23 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" -string.prototype.trim@~1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.4.tgz#6014689baf5efaf106ad031a5fa45157666ed1bd" - integrity sha512-hWCk/iqf7lp0/AgTF7/ddO1IWtSNPASjlzCicV5irAVdE1grjsneK26YG6xACMBEdCvO8fUST0UzDMh/2Qy+9Q== +string.prototype.trim@~1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.5.tgz#a587bcc8bfad8cb9829a577f5de30dd170c1682c" + integrity sha512-Lnh17webJVsD6ECeovpVN17RlAKjmz4rF9S+8Y45CkMc/ufVpTkU3vZIyIC7sllQ1FCvObZnnCdNs/HXTUOTlg== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" + es-abstract "^1.19.1" string.prototype.trimend@^1.0.4: version "1.0.4" @@ -10986,12 +11476,12 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" strip-bom@^2.0.0: version "2.0.0" @@ -11000,11 +11490,6 @@ strip-bom@^2.0.0: dependencies: is-utf8 "^0.2.0" -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -11112,6 +11597,16 @@ sync-rpc@^1.2.1: dependencies: get-port "^3.1.0" +table-layout@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" @@ -11123,36 +11618,35 @@ table@^5.2.3: string-width "^3.0.0" table@^6.0.9: - version "6.7.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" - integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== + version "6.8.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" + integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== dependencies: ajv "^8.0.1" - lodash.clonedeep "^4.5.0" lodash.truncate "^4.4.2" slice-ansi "^4.0.0" - string-width "^4.2.0" - strip-ansi "^6.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" tape@^4.6.3: - version "4.14.0" - resolved "https://registry.yarnpkg.com/tape/-/tape-4.14.0.tgz#e4d46097e129817175b90925f2385f6b1bcfa826" - integrity sha512-z0+WrUUJuG6wIdWrl4W3rTte2CR26G6qcPOj3w1hfRdcmhF3kHBhOBW9VHsPVAkz08ZmGzp7phVpDupbLzrYKQ== + version "4.15.0" + resolved "https://registry.yarnpkg.com/tape/-/tape-4.15.0.tgz#1b8a9563b4bc7e51302216c137732fb2ce6d1a99" + integrity sha512-SfRmG2I8QGGgJE/MCiLH8c11L5XxyUXxwK9xLRD0uiK5fehRkkSZGmR6Y1pxOt8vJ19m3sY+POTQpiaVv45/LQ== dependencies: call-bind "~1.0.2" deep-equal "~1.1.1" defined "~1.0.0" dotignore "~0.1.2" for-each "~0.3.3" - glob "~7.1.7" + glob "~7.2.0" has "~1.0.3" inherits "~2.0.4" - is-regex "~1.1.3" + is-regex "~1.1.4" minimist "~1.2.5" - object-inspect "~1.11.0" - resolve "~1.20.0" + object-inspect "~1.12.0" + resolve "~1.22.0" resumer "~0.0.0" - string.prototype.trim "~1.2.4" + string.prototype.trim "~1.2.5" through "~2.3.8" tar@^4.0.2: @@ -11220,6 +11714,20 @@ then-request@^6.0.0: promise "^8.0.0" qs "^6.4.0" +thread-stream@^0.13.0: + version "0.13.2" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-0.13.2.tgz#de8ea87584baee625c631947ec73494aa86131c8" + integrity sha512-woZFt0cLFkPdhsa+IGpRo1jiSouaHxMIljzTgt30CMjBWoUYbbcHqnunW5Yv+BXko9H05MVIcxMipI3Jblallw== + dependencies: + real-require "^0.1.0" + +thread-stream@^0.15.0: + version "0.15.1" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-0.15.1.tgz#5aba24a35aa5e9e4eb66173826443d7167b34b07" + integrity sha512-SCnuIT27gc2h/F/RY2peuC7brgLy+1oXU+7yOIAITz1z5stDpXCF5rAoFcykjuK6ifbTlKAHL7Ccq8oc5Btv1w== + dependencies: + real-require "^0.1.0" + through2@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -11309,10 +11817,10 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== toposort-class@^1.0.1: version "1.0.1" @@ -11342,7 +11850,7 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= -triple-beam@^1.2.0, triple-beam@^1.3.0: +triple-beam@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== @@ -11352,6 +11860,16 @@ triple-beam@^1.2.0, triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-2.2.1.tgz#c5bf04a5bbec3fd118be4084461b3a27c4d796bf" integrity sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q== +ts-command-line-args@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ts-command-line-args/-/ts-command-line-args-2.2.1.tgz#fd6913e542099012c0ffb2496126a8f38305c7d6" + integrity sha512-mnK68QA86FYzQYTSA/rxIjT/8EpKsvQw9QkawPic8I8t0gjAOw3Oa509NIRoaY1FmH7hdrncMp7t7o+vYoceNQ== + dependencies: + chalk "^4.1.0" + command-line-args "^5.1.1" + command-line-usage "^6.1.0" + string-format "^2.0.0" + ts-essentials@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-1.0.4.tgz#ce3b5dade5f5d97cf69889c11bf7d2da8555b15a" @@ -11382,6 +11900,25 @@ ts-generator@^0.1.1: resolve "^1.8.1" ts-essentials "^1.0.0" +ts-node@^10.2.1: + version "10.7.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.0" + yn "3.1.1" + ts-node@^9, ts-node@^9.1.1: version "9.1.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" @@ -11399,21 +11936,11 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2: +tslib@^2, tslib@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" - integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== - -tslib@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== - tsort@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" @@ -11462,7 +11989,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -11497,7 +12024,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.17, type-is@~1.6.18: +type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -11510,10 +12037,26 @@ type@^1.0.1: resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== -type@^2.0.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" - integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== +type@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f" + integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== + +typechain@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/typechain/-/typechain-7.0.0.tgz#258ca136de1d451368bde01c318976a83062f110" + integrity sha512-ILfvBBFJ7j9aIk0crX03+N2GmzoDN1gtk32G1+XrasjuvXS0XAw2XxwQeQMMgKwlnxViJjIkG87sTMYXPkXA9g== + dependencies: + "@types/prettier" "^2.1.1" + debug "^4.1.1" + fs-extra "^7.0.0" + glob "^7.1.6" + js-sha3 "^0.8.0" + lodash "^4.17.15" + mkdirp "^1.0.4" + prettier "^2.1.2" + ts-command-line-args "^2.2.0" + ts-essentials "^7.0.1" typechain@^3.0.0: version "3.0.0" @@ -11529,9 +12072,9 @@ typechain@^3.0.0: ts-generator "^0.1.1" typechain@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/typechain/-/typechain-5.1.2.tgz#c8784d6155a8e69397ca47f438a3b4fb2aa939da" - integrity sha512-FuaCxJd7BD3ZAjVJoO+D6TnqKey3pQdsqOBsC83RKYWKli5BDhdf0TPkwfyjt20TUlZvOzJifz+lDwXsRkiSKA== + version "5.2.0" + resolved "https://registry.yarnpkg.com/typechain/-/typechain-5.2.0.tgz#10525a44773a34547eb2eed8978cb72c0a39a0f4" + integrity sha512-0INirvQ+P+MwJOeMct+WLkUE4zov06QxC96D+i3uGFEHoiSkZN70MKDQsaj8zkL86wQwByJReI2e7fOUwECFuw== dependencies: "@types/prettier" "^2.1.1" command-line-args "^4.0.7" @@ -11556,15 +12099,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^4.2.4, typescript@^4.3.4: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== - -typescript@^4.4.3: - version "4.4.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324" - integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA== +typescript@^4.2.4, typescript@^4.4.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" + integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== typewise-core@^1.2, typewise-core@^1.2.0: version "1.2.0" @@ -11588,10 +12126,20 @@ typical@^2.6.0, typical@^2.6.1: resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0= +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + uglify-js@^3.1.4: - version "3.14.1" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.1.tgz#e2cb9fe34db9cb4cf7e35d1d26dfea28e09a7d06" - integrity sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g== + version "3.15.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471" + integrity sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg== uint8arrays@1.1.0, uint8arrays@^1.0.0, uint8arrays@^1.1.0: version "1.1.0" @@ -11601,13 +12149,20 @@ uint8arrays@1.1.0, uint8arrays@^1.0.0, uint8arrays@^1.1.0: multibase "^3.0.0" web-encoding "^1.0.2" -uint8arrays@^2.0.5, uint8arrays@^2.1.3, uint8arrays@^2.1.5: +uint8arrays@^2.0.5, uint8arrays@^2.1.3: version "2.1.10" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.10.tgz#34d023c843a327c676e48576295ca373c56e286a" integrity sha512-Q9/hhJa2836nQfEJSZTmr+pg9+cDJS9XEAp7N2Vg5MzL3bK/mkMVfjscRGYruP9jNda6MAdf4QD/y78gSzkp6A== dependencies: multiformats "^9.4.2" +uint8arrays@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" + integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== + dependencies: + multiformats "^9.4.2" + ultron@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" @@ -11628,10 +12183,10 @@ underscore@1.9.1: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== -underscore@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" - integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== +underscore@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.2.tgz#276cea1e8b9722a8dbed0100a407dda572125881" + integrity sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g== undici@^4.14.1: version "4.16.0" @@ -11726,11 +12281,11 @@ use@^3.1.0: integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== utf-8-validate@^5.0.2: - version "5.0.5" - resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.5.tgz#dd32c2e82c72002dc9f02eb67ba6761f43456ca1" - integrity sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ== + version "5.0.9" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.9.tgz#ba16a822fbeedff1a58918f2a6a6b36387493ea3" + integrity sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q== dependencies: - node-gyp-build "^4.2.0" + node-gyp-build "^4.3.0" utf8@3.0.0, utf8@^3.0.0: version "3.0.0" @@ -11785,11 +12340,16 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.1.0, uuid@^8.3.2: +uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" + integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -11803,10 +12363,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -validator@^10.11.0: - version "10.11.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" - integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== varint@^5.0.0, varint@^5.0.2, varint@~5.0.0: version "5.0.2" @@ -11858,10 +12418,10 @@ web3-bzz@1.2.11: swarm-js "^0.1.40" underscore "1.9.1" -web3-bzz@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.5.0.tgz#fed3f0895b4c51392eed4557235c1aaf79e0810b" - integrity sha512-IqlecWpwTMO/O5qa0XZZubQh4GwAtO/CR+e2FQ/7oB5eXQyre3DZ/MYu8s5HCLxCR33Fcqda9q2dbNtm1wSQYw== +web3-bzz@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.5.3.tgz#e36456905ce051138f9c3ce3623cbc73da088c2b" + integrity sha512-SlIkAqG0eS6cBS9Q2eBOTI1XFzqh83RqGJWnyrNZMDxUwsTVHL+zNnaPShVPvrWQA1Ub5b0bx1Kc5+qJVxsTJg== dependencies: "@types/node" "^12.12.6" got "9.6.0" @@ -11876,13 +12436,13 @@ web3-core-helpers@1.2.11: web3-eth-iban "1.2.11" web3-utils "1.2.11" -web3-core-helpers@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.5.0.tgz#bca7645aaf2f22910df15d6d359e7f466b5d65ca" - integrity sha512-7s5SrJbG5O0C0Oi9mqKLYchco72djZhk59B7kTla5vUorAxMc99SY7k9BoDgwbFl2dlZon2GtFUEW2RXUNkb1g== +web3-core-helpers@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.5.3.tgz#099030235c477aadf39a94199ef40092151d563c" + integrity sha512-Ip1IjB3S8vN7Kf1PPjK41U5gskmMk6IJQlxIVuS8/1U7n/o0jC8krqtpRwiMfAgYyw3TXwBFtxSRTvJtnLyXZw== dependencies: - web3-eth-iban "1.5.0" - web3-utils "1.5.0" + web3-eth-iban "1.5.3" + web3-utils "1.5.3" web3-core-method@1.2.11: version "1.2.11" @@ -11896,16 +12456,17 @@ web3-core-method@1.2.11: web3-core-subscriptions "1.2.11" web3-utils "1.2.11" -web3-core-method@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.5.0.tgz#1940e4da7def63d00f9141b84c4d0d66d25428a7" - integrity sha512-izPhpjbn9jVBjMeFcsU7a5+/nqni9hS5oU+d00HJGTVbp8KV6zplhYw4GjkRqyy6OQzooO8Gx2MMUyRdv5x1wg== +web3-core-method@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.5.3.tgz#6cff97ed19fe4ea2e9183d6f703823a079f5132c" + integrity sha512-8wJrwQ2qD9ibWieF9oHXwrJsUGrv3XAtEkNeyvyNMpktNTIjxJ2jaFGQUuLiyUrMubD18XXgLk4JS6PJU4Loeg== dependencies: + "@ethereumjs/common" "^2.4.0" "@ethersproject/transactions" "^5.0.0-beta.135" - web3-core-helpers "1.5.0" - web3-core-promievent "1.5.0" - web3-core-subscriptions "1.5.0" - web3-utils "1.5.0" + web3-core-helpers "1.5.3" + web3-core-promievent "1.5.3" + web3-core-subscriptions "1.5.3" + web3-utils "1.5.3" web3-core-promievent@1.2.11: version "1.2.11" @@ -11914,10 +12475,10 @@ web3-core-promievent@1.2.11: dependencies: eventemitter3 "4.0.4" -web3-core-promievent@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.5.0.tgz#fab9fe72520e46d8fee73ccf8d2f15243e4bc4fd" - integrity sha512-7GkbOIMtcp1qN8LRMMmwIhulzEldT+3Mu7ii2WgAcFFKT1yzUl6Gmycf8mmoEKpAuADAQ9Qeyk0PskTR6rTYlQ== +web3-core-promievent@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.5.3.tgz#3f11833c3dc6495577c274350b61144e0a4dba01" + integrity sha512-CFfgqvk3Vk6PIAxtLLuX+pOMozxkKCY+/GdGr7weMh033mDXEPvwyVjoSRO1PqIKj668/hMGQsVoIgbyxkJ9Mg== dependencies: eventemitter3 "4.0.4" @@ -11932,16 +12493,16 @@ web3-core-requestmanager@1.2.11: web3-providers-ipc "1.2.11" web3-providers-ws "1.2.11" -web3-core-requestmanager@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.5.0.tgz#126427fb29efe15bbac090d3aad09b3842c6dbf6" - integrity sha512-Sr5T2JuXOAsINJ2tf7Rgi2a+Dy2suBDKT8eMc1pcspPmaBhvTKOQfM9XdsO4yjJKYw6tt/Tagw4GKZm4IOx7mw== +web3-core-requestmanager@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.5.3.tgz#b339525815fd40e3a2a81813c864ddc413f7b6f7" + integrity sha512-9k/Bze2rs8ONix5IZR+hYdMNQv+ark2Ek2kVcrFgWO+LdLgZui/rn8FikPunjE+ub7x7pJaKCgVRbYFXjo3ZWg== dependencies: util "^0.12.0" - web3-core-helpers "1.5.0" - web3-providers-http "1.5.0" - web3-providers-ipc "1.5.0" - web3-providers-ws "1.5.0" + web3-core-helpers "1.5.3" + web3-providers-http "1.5.3" + web3-providers-ipc "1.5.3" + web3-providers-ws "1.5.3" web3-core-subscriptions@1.2.11: version "1.2.11" @@ -11952,13 +12513,13 @@ web3-core-subscriptions@1.2.11: underscore "1.9.1" web3-core-helpers "1.2.11" -web3-core-subscriptions@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.5.0.tgz#c7f77fc0db061cd9290987b08540f91e9d4b8bca" - integrity sha512-dx9P1mZvJkQRiYpSo9SvFhYNzy5E9GHeLOc3uqxPaDxKU7Cu9fJnFHo/P6+wfD6ZhGIP23ZLK/uyor5UpdTqDQ== +web3-core-subscriptions@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.5.3.tgz#d7d69c4caad65074212028656e9dc56ca5c2159d" + integrity sha512-L2m9vG1iRN6thvmv/HQwO2YLhOQlmZU8dpLG6GSo9FBN14Uch868Swk0dYVr3rFSYjZ/GETevSXU+O+vhCummA== dependencies: eventemitter3 "4.0.4" - web3-core-helpers "1.5.0" + web3-core-helpers "1.5.3" web3-core@1.2.11: version "1.2.11" @@ -11973,18 +12534,18 @@ web3-core@1.2.11: web3-core-requestmanager "1.2.11" web3-utils "1.2.11" -web3-core@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.5.0.tgz#46c09283bcfe197df0c543dbe751650cea157a7f" - integrity sha512-1o/etaPSK8tFOWTA6df3t9J6ez4epeyzlNmyh/gx8uHasfa16XLKD8//A9T+O/TmvyQAaA4hWAsQcvlRcuaZ8Q== +web3-core@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.5.3.tgz#59f8728b27c8305b349051326aa262b9b7e907bf" + integrity sha512-ACTbu8COCu+0eUNmd9pG7Q9EVsNkAg2w3Y7SqhDr+zjTgbSHZV01jXKlapm9z+G3AN/BziV3zGwudClJ4u4xXQ== dependencies: "@types/bn.js" "^4.11.5" "@types/node" "^12.12.6" bignumber.js "^9.0.0" - web3-core-helpers "1.5.0" - web3-core-method "1.5.0" - web3-core-requestmanager "1.5.0" - web3-utils "1.5.0" + web3-core-helpers "1.5.3" + web3-core-method "1.5.3" + web3-core-requestmanager "1.5.3" + web3-utils "1.5.3" web3-eth-abi@1.2.11: version "1.2.11" @@ -11995,13 +12556,13 @@ web3-eth-abi@1.2.11: underscore "1.9.1" web3-utils "1.2.11" -web3-eth-abi@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.5.0.tgz#10a4bf11ec2302c6cf313b5de4e2e12d9620d648" - integrity sha512-rfT/SvfZY9+SNJRzTHxLFaebQRBhS67tGqUqLxlyy6EsAcEmIs/g4mAUH5atYwPE9bOQeiVoLKLbwJEBIcw86w== +web3-eth-abi@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.5.3.tgz#5aea9394d797f99ca0d9bd40c3417eb07241c96c" + integrity sha512-i/qhuFsoNrnV130CSRYX/z4SlCfSQ4mHntti5yTmmQpt70xZKYZ57BsU0R29ueSQ9/P+aQrL2t2rqkQkAloUxg== dependencies: "@ethersproject/abi" "5.0.7" - web3-utils "1.5.0" + web3-utils "1.5.3" web3-eth-accounts@1.2.11: version "1.2.11" @@ -12020,10 +12581,10 @@ web3-eth-accounts@1.2.11: web3-core-method "1.2.11" web3-utils "1.2.11" -web3-eth-accounts@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.5.0.tgz#1a71e12758440884450f4939290569ff82976cc3" - integrity sha512-tqvF2bKECaS6jDux8h1dkdsrfb5SHIVVA6hu2lJmZNlTBqFIq2A8rfOkqcanie6Vh5n5U7Dnc2LUoN9rxgaSSg== +web3-eth-accounts@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.5.3.tgz#076c816ff4d68c9dffebdc7fd2bfaddcfc163d77" + integrity sha512-pdGhXgeBaEJENMvRT6W9cmji3Zz/46ugFSvmnLLw79qi5EH7XJhKISNVb41eWCrs4am5GhI67GLx5d2s2a72iw== dependencies: "@ethereumjs/common" "^2.3.0" "@ethereumjs/tx" "^3.2.1" @@ -12032,10 +12593,10 @@ web3-eth-accounts@1.5.0: ethereumjs-util "^7.0.10" scrypt-js "^3.0.1" uuid "3.3.2" - web3-core "1.5.0" - web3-core-helpers "1.5.0" - web3-core-method "1.5.0" - web3-utils "1.5.0" + web3-core "1.5.3" + web3-core-helpers "1.5.3" + web3-core-method "1.5.3" + web3-utils "1.5.3" web3-eth-contract@1.2.11: version "1.2.11" @@ -12052,19 +12613,19 @@ web3-eth-contract@1.2.11: web3-eth-abi "1.2.11" web3-utils "1.2.11" -web3-eth-contract@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.5.0.tgz#f584a083316424110af95c3ad00c1c3a8a1796d2" - integrity sha512-v4laiJRzdcoDwvqaMCzJH1BUosbTVsd01Qp+9v05Q94KycjkdeahPRXX6PEcUNW/ZF8N006iExUweGjajTZnTA== +web3-eth-contract@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.5.3.tgz#12b03a4a16ce583a945f874bea2ff2fb4c5b81ad" + integrity sha512-Gdlt1L6cdHe83k7SdV6xhqCytVtOZkjD0kY/15x441AuuJ4JLubCHuqu69k2Dr3tWifHYVys/vG8QE/W16syGg== dependencies: "@types/bn.js" "^4.11.5" - web3-core "1.5.0" - web3-core-helpers "1.5.0" - web3-core-method "1.5.0" - web3-core-promievent "1.5.0" - web3-core-subscriptions "1.5.0" - web3-eth-abi "1.5.0" - web3-utils "1.5.0" + web3-core "1.5.3" + web3-core-helpers "1.5.3" + web3-core-method "1.5.3" + web3-core-promievent "1.5.3" + web3-core-subscriptions "1.5.3" + web3-eth-abi "1.5.3" + web3-utils "1.5.3" web3-eth-ens@1.2.11: version "1.2.11" @@ -12081,19 +12642,19 @@ web3-eth-ens@1.2.11: web3-eth-contract "1.2.11" web3-utils "1.2.11" -web3-eth-ens@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.5.0.tgz#f92ce19a541e42a0da4b8b04f7161d7a20ad3e86" - integrity sha512-NiaGfOnsCqP+3hOCeP3Q9IrlV/1ZCDiv8VmN1yF5Ya6n6YeO4TJU9MKP8i5038RFETjLIfGtXr5fthbsob30hA== +web3-eth-ens@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.5.3.tgz#ef6eee1ddf32b1ff9536fc7c599a74f2656bafe1" + integrity sha512-QmGFFtTGElg0E+3xfCIFhiUF+1imFi9eg/cdsRMUZU4F1+MZCC/ee+IAelYLfNTGsEslCqfAusliKOT9DdGGnw== dependencies: content-hash "^2.5.2" eth-ens-namehash "2.0.8" - web3-core "1.5.0" - web3-core-helpers "1.5.0" - web3-core-promievent "1.5.0" - web3-eth-abi "1.5.0" - web3-eth-contract "1.5.0" - web3-utils "1.5.0" + web3-core "1.5.3" + web3-core-helpers "1.5.3" + web3-core-promievent "1.5.3" + web3-eth-abi "1.5.3" + web3-eth-contract "1.5.3" + web3-utils "1.5.3" web3-eth-iban@1.2.11: version "1.2.11" @@ -12103,13 +12664,13 @@ web3-eth-iban@1.2.11: bn.js "^4.11.9" web3-utils "1.2.11" -web3-eth-iban@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.5.0.tgz#8c3a1aa7aeed4080ba7d077612ce17025eb0d67d" - integrity sha512-cFfiPA8xs4lemMJjDb9KfXzPvs6rBrRl8y4rgvh/JWlZZgKolzo7KLXq4NR3oFd/C81s0Lslvz2st1EREp5CNA== +web3-eth-iban@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.5.3.tgz#91b1475893a877b10eac1de5cce6eb379fb81b5d" + integrity sha512-vMzmGqolYZvRHwP9P4Nf6G8uYM5aTLlQu2a34vz78p0KlDC+eV1th3+90Qeaupa28EG7OO0IT1F0BejiIauOPw== dependencies: bn.js "^4.11.9" - web3-utils "1.5.0" + web3-utils "1.5.3" web3-eth-personal@1.2.11: version "1.2.11" @@ -12123,17 +12684,17 @@ web3-eth-personal@1.2.11: web3-net "1.2.11" web3-utils "1.2.11" -web3-eth-personal@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.5.0.tgz#79e604f38439fbb7a9d4dcb20094359d20d3d388" - integrity sha512-FYBrzMS6q/df8ud1kAN1p6lqdP/pd0szogcuyrVyi++bFQiovnR+QosudFsbn/aAZPDHOEh0UV4P3KVKbLqw9g== +web3-eth-personal@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.5.3.tgz#4ebe09e9a77dd49d23d93b36b36cfbf4a6dae713" + integrity sha512-JzibJafR7ak/Icas8uvos3BmUNrZw1vShuNR5Cxjo+vteOC8XMqz1Vr7RH65B4bmlfb3bm9xLxetUHO894+Sew== dependencies: "@types/node" "^12.12.6" - web3-core "1.5.0" - web3-core-helpers "1.5.0" - web3-core-method "1.5.0" - web3-net "1.5.0" - web3-utils "1.5.0" + web3-core "1.5.3" + web3-core-helpers "1.5.3" + web3-core-method "1.5.3" + web3-net "1.5.3" + web3-utils "1.5.3" web3-eth@1.2.11: version "1.2.11" @@ -12154,23 +12715,23 @@ web3-eth@1.2.11: web3-net "1.2.11" web3-utils "1.2.11" -web3-eth@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.5.0.tgz#819466117dfdc191095d6feb58b24023e016cb20" - integrity sha512-31ni3YliTDYLKuWt8naitZ4Ru86whZlqvz6kFzCaBaCR/EumzA9ejzNbcX9okio9zUtKSHH37Bk0+WogfU9Jqg== - dependencies: - web3-core "1.5.0" - web3-core-helpers "1.5.0" - web3-core-method "1.5.0" - web3-core-subscriptions "1.5.0" - web3-eth-abi "1.5.0" - web3-eth-accounts "1.5.0" - web3-eth-contract "1.5.0" - web3-eth-ens "1.5.0" - web3-eth-iban "1.5.0" - web3-eth-personal "1.5.0" - web3-net "1.5.0" - web3-utils "1.5.0" +web3-eth@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.5.3.tgz#d7d1ac7198f816ab8a2088c01e0bf1eda45862fe" + integrity sha512-saFurA1L23Bd7MEf7cBli6/jRdMhD4X/NaMiO2mdMMCXlPujoudlIJf+VWpRWJpsbDFdu7XJ2WHkmBYT5R3p1Q== + dependencies: + web3-core "1.5.3" + web3-core-helpers "1.5.3" + web3-core-method "1.5.3" + web3-core-subscriptions "1.5.3" + web3-eth-abi "1.5.3" + web3-eth-accounts "1.5.3" + web3-eth-contract "1.5.3" + web3-eth-ens "1.5.3" + web3-eth-iban "1.5.3" + web3-eth-personal "1.5.3" + web3-net "1.5.3" + web3-utils "1.5.3" web3-net@1.2.11: version "1.2.11" @@ -12181,14 +12742,14 @@ web3-net@1.2.11: web3-core-method "1.2.11" web3-utils "1.2.11" -web3-net@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.5.0.tgz#21ccbe7af3c3065633086b1e82ef100d833944b4" - integrity sha512-oGgEtO2fRtJjAp0K1/fvH247MeeDemFL+5tF+PxII9b/gBxnVe+MzP+oNLr4dTrweromjv34tioR3kUgsqwCWg== +web3-net@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.5.3.tgz#545fee49b8e213b0c55cbe74ffd0295766057463" + integrity sha512-0W/xHIPvgVXPSdLu0iZYnpcrgNnhzHMC888uMlGP5+qMCt8VuflUZHy7tYXae9Mzsg1kxaJAS5lHVNyeNw4CoQ== dependencies: - web3-core "1.5.0" - web3-core-method "1.5.0" - web3-utils "1.5.0" + web3-core "1.5.3" + web3-core-method "1.5.3" + web3-utils "1.5.3" web3-provider-engine@14.2.1: version "14.2.1" @@ -12224,12 +12785,12 @@ web3-providers-http@1.2.11: web3-core-helpers "1.2.11" xhr2-cookies "1.1.0" -web3-providers-http@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.5.0.tgz#47297ac0f058e1c9af7a1528d1dfc2a67d602e93" - integrity sha512-y1RuxsCGrWdsIUyuZBEN+3F8trl3bDZNajwLS2KYBGlB99sWYZHPmvbAsBpaW1d/I12W0fQiWOVzp63L7KPTow== +web3-providers-http@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.5.3.tgz#74f170fc3d79eb7941d9fbc34e2a067d61ced0b2" + integrity sha512-5DpUyWGHtDAr2RYmBu34Fu+4gJuBAuNx2POeiJIooUtJ+Mu6pIx4XkONWH6V+Ez87tZAVAsFOkJRTYuzMr3rPw== dependencies: - web3-core-helpers "1.5.0" + web3-core-helpers "1.5.3" xhr2-cookies "1.1.0" web3-providers-ipc@1.2.11: @@ -12241,13 +12802,13 @@ web3-providers-ipc@1.2.11: underscore "1.9.1" web3-core-helpers "1.2.11" -web3-providers-ipc@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.5.0.tgz#69d9b3a23f6bfd52f649f3bfbfa6696b159fa80a" - integrity sha512-Hda9wlOaIJC9/qMOVkayK+fbBHDZBmPcoL7TfjQX7hrtZn8V3+gR27ciyRXmuW7QD3hDg7CJfe5uRK8brh3nSA== +web3-providers-ipc@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.5.3.tgz#4bd7f5e445c2f3c2595fce0929c72bb879320a3f" + integrity sha512-JmeAptugVpmXI39LGxUSAymx0NOFdgpuI1hGQfIhbEAcd4sv7fhfd5D+ZU4oLHbRI8IFr4qfGU0uhR8BXhDzlg== dependencies: oboe "2.1.5" - web3-core-helpers "1.5.0" + web3-core-helpers "1.5.3" web3-providers-ws@1.2.11: version "1.2.11" @@ -12259,13 +12820,13 @@ web3-providers-ws@1.2.11: web3-core-helpers "1.2.11" websocket "^1.0.31" -web3-providers-ws@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.5.0.tgz#c78253af17dfdcd4f8a4c3a8ac1a684a73886ae7" - integrity sha512-TCwOhu5WbuQCSUoar+U+7N1NqI4A6MlcdZqsC7AhTogYYtnXOPRWfiHMZtUP7Qw50GKJ37FIH3YDItcHTNHd6A== +web3-providers-ws@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.5.3.tgz#eec6cfb32bb928a4106de506f13a49070a21eabf" + integrity sha512-6DhTw4Q7nm5CFYEUHOJM0gAb3xFx+9gWpVveg3YxJ/ybR1BUvEWo3bLgIJJtX56cYX0WyY6DS35a7f0LOI1kVg== dependencies: eventemitter3 "4.0.4" - web3-core-helpers "1.5.0" + web3-core-helpers "1.5.3" websocket "^1.0.32" web3-shh@1.2.11: @@ -12278,15 +12839,15 @@ web3-shh@1.2.11: web3-core-subscriptions "1.2.11" web3-net "1.2.11" -web3-shh@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.5.0.tgz#eabf7c346605b107f51dfe5e6df9643a4b5eb7aa" - integrity sha512-TwpcxXNh+fBnyRcCPPqVqaCB4IjSpVL2/5OR2WwCnZwejs1ife+pej8DYVZWm0m1tSzIDRTdNbsJf/DN0cAxYQ== +web3-shh@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.5.3.tgz#3c04aa4cda9ba0b746d7225262401160f8e38b13" + integrity sha512-COfEXfsqoV/BkcsNLRxQqnWc1Teb8/9GxdGag5GtPC5gQC/vsN+7hYVJUwNxY9LtJPKYTij2DHHnx6UkITng+Q== dependencies: - web3-core "1.5.0" - web3-core-method "1.5.0" - web3-core-subscriptions "1.5.0" - web3-net "1.5.0" + web3-core "1.5.3" + web3-core-method "1.5.3" + web3-core-subscriptions "1.5.3" + web3-net "1.5.3" web3-utils@1.2.11: version "1.2.11" @@ -12302,10 +12863,10 @@ web3-utils@1.2.11: underscore "1.9.1" utf8 "3.0.0" -web3-utils@1.5.0, web3-utils@^1.0.0-beta.31, web3-utils@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.5.0.tgz#48c8ba0d95694e73b9a6d473d955880cd4758e4a" - integrity sha512-hNyw7Oxi6TM3ivXmv4hK5Cvyi9ML3UoKtcCYvLF9woPWh5v2dwCCVO1U3Iq5HHK7Dqq28t1d4CxWHqUfOfAkgg== +web3-utils@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.5.3.tgz#e914c9320cd663b2a09a5cb920ede574043eb437" + integrity sha512-56nRgA+Ad9SEyCv39g36rTcr5fpsd4L9LgV3FK0aB66nAMazLAA6Qz4lH5XrUKPDyBIPGJIR+kJsyRtwcu2q1Q== dependencies: bn.js "^4.11.9" eth-lib "0.2.8" @@ -12315,6 +12876,19 @@ web3-utils@1.5.0, web3-utils@^1.0.0-beta.31, web3-utils@^1.3.0: randombytes "^2.1.0" utf8 "3.0.0" +web3-utils@^1.0.0-beta.31, web3-utils@^1.3.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.7.1.tgz#77d8bacaf426c66027d8aa4864d77f0ed211aacd" + integrity sha512-fef0EsqMGJUgiHPdX+KN9okVWshbIumyJPmR+btnD1HgvoXijKEkuKBv0OmUqjbeqmLKP2/N9EiXKJel5+E1Dw== + dependencies: + bn.js "^4.11.9" + ethereum-bloom-filters "^1.0.6" + ethereumjs-util "^7.1.0" + ethjs-unit "0.1.6" + number-to-bn "1.7.0" + randombytes "^2.1.0" + utf8 "3.0.0" + web3@1.2.11: version "1.2.11" resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.11.tgz#50f458b2e8b11aa37302071c170ed61cff332975" @@ -12328,18 +12902,18 @@ web3@1.2.11: web3-shh "1.2.11" web3-utils "1.2.11" -web3@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/web3/-/web3-1.5.0.tgz#2c1d8c910ce9c8c33ca4e5a130c02eda9c0f82bf" - integrity sha512-p6mOU+t11tV5Z0W9ISO2ReZlbB1ICp755ogl3OXOWZ+/oWy12wwnIva+z+ypsZc3P8gaoGaTvEwSfXM9NF164w== +web3@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/web3/-/web3-1.5.3.tgz#11882679453c645bf33620fbc255a243343075aa" + integrity sha512-eyBg/1K44flfv0hPjXfKvNwcUfIVDI4NX48qHQe6wd7C8nPSdbWqo9vLy6ksZIt9NLa90HjI8HsGYgnMSUxn6w== dependencies: - web3-bzz "1.5.0" - web3-core "1.5.0" - web3-eth "1.5.0" - web3-eth-personal "1.5.0" - web3-net "1.5.0" - web3-shh "1.5.0" - web3-utils "1.5.0" + web3-bzz "1.5.3" + web3-core "1.5.3" + web3-eth "1.5.3" + web3-eth-personal "1.5.3" + web3-net "1.5.3" + web3-shh "1.5.3" + web3-utils "1.5.3" webidl-conversions@^3.0.0: version "3.0.1" @@ -12410,17 +12984,16 @@ which-module@^2.0.0: integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= which-typed-array@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff" - integrity sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA== + version "1.1.7" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.7.tgz#2761799b9a22d4b8660b3c1b40abaa7739691793" + integrity sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw== dependencies: - available-typed-arrays "^1.0.2" - call-bind "^1.0.0" - es-abstract "^1.18.0-next.1" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.18.5" foreach "^2.0.5" - function-bind "^1.1.1" - has-symbols "^1.0.1" - is-typed-array "^1.1.3" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.7" which@1.3.1, which@^1.1.1, which@^1.2.9, which@^1.3.1: version "1.3.1" @@ -12448,28 +13021,30 @@ window-size@^0.2.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= -winston-transport@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" - integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== +winston-transport@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" + integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== dependencies: - readable-stream "^2.3.7" - triple-beam "^1.2.0" + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" winston@*, winston@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" - integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + version "3.7.2" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.7.2.tgz#95b4eeddbec902b3db1424932ac634f887c400b1" + integrity sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng== dependencies: "@dabh/diagnostics" "^2.0.2" - async "^3.1.0" + async "^3.2.3" is-stream "^2.0.0" - logform "^2.2.0" + logform "^2.4.0" one-time "^1.0.0" readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" stack-trace "0.0.x" triple-beam "^1.3.0" - winston-transport "^4.4.0" + winston-transport "^4.5.0" wkx@^0.5.0: version "0.5.0" @@ -12493,6 +13068,14 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" + workerpool@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" @@ -12567,9 +13150,9 @@ ws@^5.1.1: async-limiter "~1.0.0" ws@^7.4.6: - version "7.5.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" - integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== + version "7.5.7" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" + integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== xhr-request-promise@^0.1.2: version "0.1.3" @@ -12660,7 +13243,7 @@ yaml@^1.10.0, yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargs-parser@13.1.2, yargs-parser@^13.1.0, yargs-parser@^13.1.2: +yargs-parser@13.1.2, yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== @@ -12686,6 +13269,11 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.0.0: + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== + yargs-unparser@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" @@ -12705,23 +13293,6 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" - yargs@13.3.2, yargs@^13.3.0: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -12752,17 +13323,17 @@ yargs@16.2.0: yargs-parser "^20.2.2" yargs@^17.0.0: - version "17.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.0.tgz#0cd9827a0572c9a1795361c4d1530e53ada168cf" - integrity sha512-SQr7qqmQ2sNijjJGHL4u7t8vyDZdZ3Ahkmo4sc1w5xI9TBX0QDdG/g4SFnxtWOsGLjwHQue57eFALfwFCnixgg== + version "17.4.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.0.tgz#9fc9efc96bd3aa2c1240446af28499f0e7593d00" + integrity sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA== dependencies: cliui "^7.0.2" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" - string-width "^4.2.0" + string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^20.2.2" + yargs-parser "^21.0.0" yargs@^4.7.1: version "4.8.1" From 497a7d8ef2ecc6f0a741aa32d5de0a62c4670e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 4 May 2022 10:41:31 -0300 Subject: [PATCH 05/78] fix: Take a snapshot of GRT supply when signal is updated --- config/graph.mainnet.yml | 4 +++- contracts/rewards/RewardsManager.sol | 10 ++++------ contracts/rewards/RewardsManagerStorage.sol | 5 +++++ test/lib/deployment.ts | 2 +- test/lib/fixtures.ts | 1 + 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index 9ba114ec5..55da96fbb 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -106,7 +106,9 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + calls: + - fn: "setIssuanceRate" + _issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 AllocationExchange: init: graphToken: "${{GraphToken.address}}" diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 9a0e24d2a..6d8f1be74 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -27,7 +27,7 @@ import "./IRewardsManager.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; uint256 private constant TOKEN_DECIMALS = 1e18; @@ -68,11 +68,8 @@ contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsMa /** * @dev Initialize this contract. */ - function initialize(address _controller, uint256 _issuanceRate) external onlyImpl { + function initialize(address _controller) external onlyImpl { Managed._initialize(_controller); - - // Settings - _setIssuanceRate(_issuanceRate); } // -- Config -- @@ -224,7 +221,7 @@ contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsMa } uint256 r = issuanceRate; - uint256 p = graphToken.totalSupply(); + uint256 p = tokenSupplySnapshot; uint256 a = p.mul(_pow(r, t, TOKEN_DECIMALS)).div(TOKEN_DECIMALS); // New issuance of tokens during time steps @@ -315,6 +312,7 @@ contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsMa function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; + tokenSupplySnapshot = graphToken().totalSupply(); return accRewardsPerSignal; } diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index 2bb0c2978..7626992da 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -26,3 +26,8 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage { // Minimum amount of signaled tokens on a subgraph required to accrue rewards uint256 public minimumSubgraphSignal; } + +contract RewardsManagerV3Storage is RewardsManagerV2Storage { + // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated + uint256 public tokenSupplySnapshot; +} diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index fe5eed768..cadbc58d0 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -235,7 +235,7 @@ export async function deployRewardsManager( return network.deployContractWithProxy( proxyAdmin, 'RewardsManager', - [controller, defaults.rewards.issuanceRate], + [controller], deployer, ) as unknown as RewardsManager } diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 3a829c181..73c73052a 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -86,6 +86,7 @@ export class NetworkFixture { await staking.connect(deployer).setSlasher(slasherAddress, true) await grt.connect(deployer).addMinter(rewardsManager.address) await gns.connect(deployer).approveAll() + await rewardsManager.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) // Unpause the protocol await controller.connect(deployer).setPaused(false) From 1bf6515bec44c6bd51ff1da1c5f12393b8ecfeb7 Mon Sep 17 00:00:00 2001 From: Ariel Barmat Date: Sun, 15 May 2022 16:05:03 -0300 Subject: [PATCH 06/78] test: add a test for rewards distribution accrual with multiple allocations --- test/rewards/rewards.test.ts | 104 +++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 23 deletions(-) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 7c6d45c12..96d37bce5 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -50,12 +50,15 @@ describe('Rewards', () => { let rewardsManagerMock: RewardsManagerMock // Derive some channel keys for each indexer used to sign attestations - const channelKey = deriveChannelKey() + const channelKey1 = deriveChannelKey() + const channelKey2 = deriveChannelKey() const subgraphDeploymentID1 = randomHexBytes() const subgraphDeploymentID2 = randomHexBytes() - const allocationID = channelKey.address + const allocationID1 = channelKey1.address + const allocationID2 = channelKey2.address + const metadata = HashZero const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 5% rewards @@ -97,6 +100,10 @@ describe('Rewards', () => { async accrued() { const nBlocks = await this.elapsedBlocks() + return this.accruedByElapsed(nBlocks) + } + + async accruedByElapsed(nBlocks: BigNumber | number) { const n = getRewardsPerSignal( new BN(this.totalSupply.toString()), new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18), @@ -395,9 +402,9 @@ describe('Rewards', () => { indexer1.address, subgraphDeploymentID1, tokensToAllocate, - allocationID, + allocationID1, metadata, - await channelKey.generateProof(indexer1.address), + await channelKey1.generateProof(indexer1.address), ) // Jump @@ -433,9 +440,9 @@ describe('Rewards', () => { indexer1.address, subgraphDeploymentID1, tokensToAllocate, - allocationID, + allocationID1, metadata, - await channelKey.generateProof(indexer1.address), + await channelKey1.generateProof(indexer1.address), ) // Jump @@ -477,16 +484,16 @@ describe('Rewards', () => { indexer1.address, subgraphDeploymentID1, tokensToAllocate, - allocationID, + allocationID1, metadata, - await channelKey.generateProof(indexer1.address), + await channelKey1.generateProof(indexer1.address), ) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Rewards - const contractRewards = await rewardsManager.getRewards(allocationID) + const contractRewards = await rewardsManager.getRewards(allocationID1) // We trust using this function in the test because we tested it // standalone in a previous test @@ -523,9 +530,9 @@ describe('Rewards', () => { indexer1.address, subgraphDeploymentID1, tokensToAllocate, - allocationID, + allocationID1, metadata, - await channelKey.generateProof(indexer1.address), + await channelKey1.generateProof(indexer1.address), ) } @@ -566,9 +573,9 @@ describe('Rewards', () => { indexer1.address, subgraphDeploymentID1, tokensToAllocate, - allocationID, + allocationID1, metadata, - await channelKey.generateProof(indexer1.address), + await channelKey1.generateProof(indexer1.address), ) } @@ -599,11 +606,11 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) - .closeAllocation(allocationID, randomHexBytes()) + .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() const event = rewardsManager.interface.parseLog(receipt.logs[1]).args expect(event.indexer).eq(indexer1.address) - expect(event.allocationID).eq(allocationID) + expect(event.allocationID1).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) @@ -658,11 +665,11 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) - .closeAllocation(allocationID, randomHexBytes()) + .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() const event = rewardsManager.interface.parseLog(receipt.logs[1]).args expect(event.indexer).eq(indexer1.address) - expect(event.allocationID).eq(allocationID) + expect(event.allocationID1).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) @@ -710,7 +717,7 @@ describe('Rewards', () => { const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) // Close allocation. At this point rewards should be collected for that indexer - await staking.connect(indexer1.signer).closeAllocation(allocationID, randomHexBytes()) + await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) // After state const afterTokenSupply = await grt.totalSupply() @@ -756,10 +763,10 @@ describe('Rewards', () => { await advanceToNextEpoch(epochManager) // Close allocation. At this point rewards should be collected for that indexer - const tx = staking.connect(indexer1.signer).closeAllocation(allocationID, randomHexBytes()) + const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) await expect(tx) .emit(rewardsManager, 'RewardsDenied') - .withArgs(indexer1.address, allocationID, await epochManager.currentEpoch()) + .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch()) }) }) }) @@ -791,9 +798,9 @@ describe('Rewards', () => { indexer1.address, subgraphDeploymentID1, tokensToAllocate, - allocationID, + allocationID1, metadata, - await channelKey.generateProof(indexer1.address), + await channelKey1.generateProof(indexer1.address), ) // Jump @@ -804,7 +811,58 @@ describe('Rewards', () => { await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) // Close allocation. At this point rewards should be collected for that indexer - await staking.connect(indexer1.signer).closeAllocation(allocationID, randomHexBytes()) + await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + }) + }) + + describe('multiple allocations', function () { + it('two simultanous-similar allocations should get same amount of rewards', async function () { + await advanceToNextEpoch(epochManager) + + // Setup + await epochManager.setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) + + // Allocate simultaneously + const tokensToAlloc = toGRT('5000') + const tx1 = await staking.populateTransaction.allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + const tx2 = await staking.populateTransaction.allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) + await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) + + // Jump + await advanceToNextEpoch(epochManager) + + // Close allocations simultaneously + const tx3 = await staking.populateTransaction.closeAllocation(allocationID1, randomHexBytes()) + const tx4 = await staking.populateTransaction.closeAllocation(allocationID2, randomHexBytes()) + const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) + + // Both allocations should receive the same amount of rewards + const receipt = await tx5.wait() + const event1 = rewardsManager.interface.parseLog(receipt.logs[1]).args + const event2 = rewardsManager.interface.parseLog(receipt.logs[5]).args + expect(event1.amount).eq(event2.amount) }) }) }) From ebf51fe36d8908f03128bc1b05b05b945b71e342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 16 May 2022 16:28:35 -0300 Subject: [PATCH 07/78] test: Add a test for two simultaneous allocations with a GRT burn in the middle --- test/lib/fixtures.ts | 1 + test/rewards/rewards.test.ts | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 73c73052a..9f87fd6da 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -143,6 +143,7 @@ export class NetworkFixture { async setUp(): Promise { this.lastSnapshotId = await evmSnapshot() + provider().send('evm_setAutomine', [true]) } async tearDown(): Promise { diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 96d37bce5..9f3e64ce2 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -23,6 +23,7 @@ import { formatGRT, Account, advanceToNextEpoch, + provider, } from '../lib/testHelpers' const MAX_PPM = 1000000 @@ -816,6 +817,55 @@ describe('Rewards', () => { }) describe('multiple allocations', function () { + it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { + // If rewards are not monotonically increasing, this can trigger + // a subtraction overflow error as seen in mainnet tx: + // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab + await advanceToNextEpoch(epochManager) + + // Setup + await epochManager.setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) + + // Allocate simultaneously, burning in the middle + const tokensToAlloc = toGRT('5000') + await provider().send('evm_setAutomine', [false]) + const tx1 = await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) + const tx3 = await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) + + await provider().send('evm_mine', []) + await provider().send('evm_setAutomine', [true]) + + await expect(tx1).emit(staking, 'AllocationCreated') + await expect(tx2).emit(grt, 'Transfer') + await expect(tx3).emit(staking, 'AllocationCreated') + }) it('two simultanous-similar allocations should get same amount of rewards', async function () { await advanceToNextEpoch(epochManager) From dbeaa7c0ebbbeab2175fade816cb4b3f2a31d3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 16 May 2022 16:37:54 -0300 Subject: [PATCH 08/78] fix: Typo fix in tests --- test/rewards/rewards.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 9f3e64ce2..2c9021d76 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -611,7 +611,7 @@ describe('Rewards', () => { const receipt = await tx.wait() const event = rewardsManager.interface.parseLog(receipt.logs[1]).args expect(event.indexer).eq(indexer1.address) - expect(event.allocationID1).eq(allocationID1) + expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) @@ -670,7 +670,7 @@ describe('Rewards', () => { const receipt = await tx.wait() const event = rewardsManager.interface.parseLog(receipt.logs[1]).args expect(event.indexer).eq(indexer1.address) - expect(event.allocationID1).eq(allocationID1) + expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) From 403be9401a693068ec380a9fbc4f24036ae8794e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 17 May 2022 16:17:49 -0300 Subject: [PATCH 09/78] feat: implement distribution of rewards to L1 and L2 using a Reservoir BREAKING CHANGE: remove setIssuanceRate from RewardsManager interface BREAKING CHANGE: RewardsDenied event now includes amount --- cli/commands/migrate.ts | 42 +- config/graph.arbitrum-one.yml | 105 +++ config/graph.mainnet.yml | 16 +- contracts/governance/Managed.sol | 1 + contracts/l2/reservoir/L2Reservoir.sol | 114 +++ contracts/l2/reservoir/L2ReservoirStorage.sol | 13 + contracts/reservoir/IReservoir.sol | 58 ++ contracts/reservoir/L1Reservoir.sol | 294 +++++++ contracts/reservoir/L1ReservoirStorage.sol | 25 + contracts/reservoir/Reservoir.sol | 124 +++ contracts/reservoir/ReservoirStorage.sol | 18 + contracts/rewards/IRewardsManager.sol | 4 +- contracts/rewards/RewardsManager.sol | 198 ++--- contracts/rewards/RewardsManagerStorage.sol | 9 +- contracts/staking/Staking.sol | 22 +- contracts/tests/ReservoirMock.sol | 27 + contracts/tests/RewardsManagerMock.sol | 68 -- contracts/token/IGraphToken.sol | 2 + hardhat.config.ts | 6 +- test/gateway/l1GraphTokenGateway.test.ts | 53 +- test/l2/l2GraphTokenGateway.test.ts | 62 +- test/l2/l2Reservoir.test.ts | 364 +++++++++ test/lib/deployment.ts | 29 + test/lib/fixtures.ts | 299 +++++-- test/lib/testHelpers.ts | 103 +++ test/reservoir/l1Reservoir.test.ts | 750 +++++++++++++++++ test/rewards/rewards.test.ts | 770 ++++++++++-------- 27 files changed, 2880 insertions(+), 696 deletions(-) create mode 100644 contracts/l2/reservoir/L2Reservoir.sol create mode 100644 contracts/l2/reservoir/L2ReservoirStorage.sol create mode 100644 contracts/reservoir/IReservoir.sol create mode 100644 contracts/reservoir/L1Reservoir.sol create mode 100644 contracts/reservoir/L1ReservoirStorage.sol create mode 100644 contracts/reservoir/Reservoir.sol create mode 100644 contracts/reservoir/ReservoirStorage.sol create mode 100644 contracts/tests/ReservoirMock.sol delete mode 100644 contracts/tests/RewardsManagerMock.sol create mode 100644 test/l2/l2Reservoir.test.ts create mode 100644 test/reservoir/l1Reservoir.test.ts diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 9a88f0b57..d129f931c 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -35,30 +35,28 @@ let allContracts = [ 'AllocationExchange', 'L1GraphTokenGateway', 'BridgeEscrow', + 'L1Reservoir', ] -// This is all we'll want to deploy to L2 eventually: -// const l2Contracts = [ -// 'GraphProxyAdmin', -// 'BancorFormula', -// 'Controller', -// 'EpochManager', -// 'L2GraphToken', -// 'GraphCurationToken', -// 'ServiceRegistry', -// 'Curation', -// 'SubgraphNFTDescriptor', -// 'SubgraphNFT', -// 'GNS', -// 'Staking', -// 'RewardsManager', -// 'DisputeManager', -// 'AllocationExchange', -// 'L2GraphTokenGateway', -// ] -// -// But for now we'll only include a subset: -const l2Contracts = ['GraphProxyAdmin', 'Controller', 'L2GraphToken', 'L2GraphTokenGateway'] +const l2Contracts = [ + 'GraphProxyAdmin', + 'BancorFormula', + 'Controller', + 'EpochManager', + 'L2GraphToken', + 'GraphCurationToken', + 'ServiceRegistry', + 'Curation', + 'SubgraphNFTDescriptor', + 'SubgraphNFT', + 'GNS', + 'Staking', + 'RewardsManager', + 'DisputeManager', + 'AllocationExchange', + 'L2GraphTokenGateway', + 'L2Reservoir', +] export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { const graphConfigPath = cliArgs.graphConfig diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index 80d7cceeb..8020fac93 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -6,18 +6,123 @@ general: contracts: Controller: calls: + - fn: "setContractProxy" + id: "0xe6876326c1291dfcbbd3864a6816d698cd591defc7aa2153d7f9c4c04016c89f" # keccak256('Curation') + contractAddress: "${{Curation.address}}" + - fn: "setContractProxy" + id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') + contractAddress: "${{GNS.address}}" + - fn: "setContractProxy" + id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') + contractAddress: "${{DisputeManager.address}}" + - fn: "setContractProxy" + id: "0xc713c3df6d14cdf946460395d09af88993ee2b948b1a808161494e32c5f67063" # keccak256('EpochManager') + contractAddress: "${{EpochManager.address}}" + - fn: "setContractProxy" + id: "0x966f1e8d8d8014e05f6ec4a57138da9be1f7c5a7f802928a18072f7c53180761" # keccak256('RewardsManager') + contractAddress: "${{RewardsManager.address}}" + - fn: "setContractProxy" + id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') + contractAddress: "${{Staking.address}}" - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{L2GraphToken.address}}" - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L2Reservoir.address}}" + ServiceRegistry: + proxy: true + init: + controller: "${{Controller.address}}" + EpochManager: + proxy: true + init: + controller: "${{Controller.address}}" + lengthInBlocks: 1108 # 4 hours (in 13 second blocks) L2GraphToken: proxy: true init: owner: *governor initialSupply: "0" + Curation: + proxy: true + init: + controller: "${{Controller.address}}" + bondingCurve: "${{BancorFormula.address}}" + curationTokenMaster: "${{GraphCurationToken.address}}" + reserveRatio: 500000 # 50% (parts per million) + curationTaxPercentage: 10000 # 1% (parts per million) + minimumCurationDeposit: "1000000000000000000" # 1 GRT + DisputeManager: + proxy: true + init: + controller: "${{Controller.address}}" + arbitrator: *arbitrator + minimumDeposit: "10000000000000000000000" # 10,000 GRT (in wei) + fishermanRewardPercentage: 500000 # 50% (parts per million) + idxSlashingPercentage: 25000 # 2.5% (parts per million) + qrySlashingPercentage: 5000 # 0.5% (parts per million) + GNS: + proxy: true + init: + controller: "${{Controller.address}}" + bondingCurve: "${{BancorFormula.address}}" + subgraphNFT: "${{SubgraphNFT.address}}" + calls: + - fn: "approveAll" + SubgraphNFT: + init: + governor: "${{Env.deployer}}" + calls: + - fn: "setTokenDescriptor" + tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" + - fn: "setMinter" + minter: "${{GNS.address}}" + Staking: + proxy: true + init: + controller: "${{Controller.address}}" + minimumIndexerStake: "100000000000000000000000" # 100,000 GRT (in wei) + thawingPeriod: 6646 # 10 days (in blocks) + protocolPercentage: 10000 # 1% (parts per million) + curationPercentage: 100000 # 10% (parts per million) + channelDisputeEpochs: 2 # (in epochs) + maxAllocationEpochs: 6 # Based on epoch length this is 28 days (in epochs) + delegationUnbondingPeriod: 6 # Based on epoch length this is 28 days (in epochs) + delegationRatio: 16 # 16x (delegated stake to indexer stake multiplier) + rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator + rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + calls: + - fn: "setDelegationTaxPercentage" + delegationTaxPercentage: 5000 # 0.5% (parts per million) + - fn: "setSlasher" + slasher: "${{DisputeManager.address}}" + allowed: true + - fn: "setAssetHolder" + assetHolder: "${{AllocationExchange.address}}" + allowed: true + RewardsManager: + proxy: true + init: + controller: "${{Controller.address}}" + AllocationExchange: + init: + graphToken: "${{GraphToken.address}}" + staking: "${{Staking.address}}" + governor: *governor + authority: *authority + calls: + - fn: "approveAll" L2GraphTokenGateway: proxy: true init: controller: "${{Controller.address}}" + L2Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "approveRewardsManager" diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index 55da96fbb..4a6908a27 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -30,6 +30,9 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L1GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L1Reservoir.address}}" ServiceRegistry: proxy: true init: @@ -44,7 +47,7 @@ contracts: initialSupply: "10000000000000000000000000000" # in wei calls: - fn: "addMinter" - minter: "${{RewardsManager.address}}" + minter: "${{L1Reservoir.address}}" Curation: proxy: true init: @@ -106,9 +109,6 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - calls: - - fn: "setIssuanceRate" - _issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 AllocationExchange: init: graphToken: "${{GraphToken.address}}" @@ -125,3 +125,11 @@ contracts: proxy: true init: controller: "${{Controller.address}}" + L1Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + dripInterval: 50400 + calls: + - fn: "approveRewardsManager" + - fn: "initialSnapshot" diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index 561d5244d..ae220a9e0 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -183,5 +183,6 @@ contract Managed { _syncContract("Staking"); _syncContract("GraphToken"); _syncContract("GraphTokenGateway"); + _syncContract("Reservoir"); } } diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol new file mode 100644 index 000000000..6bdd9ca67 --- /dev/null +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../reservoir/IReservoir.sol"; +import "../../reservoir/Reservoir.sol"; +import "./L2ReservoirStorage.sol"; + +/** + * @title L2 Rewards Reservoir + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 2. + * It receives tokens for rewards from L1, and provides functions to compute accumulated and new + * total rewards at a particular block number. + */ +contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { + using SafeMath for uint256; + + event DripReceived(uint256 _normalizedTokenSupply); + event NextDripNonceUpdated(uint256 _nonce); + + /** + * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. + */ + modifier onlyL2Gateway() { + require(msg.sender == _resolveContract(keccak256("GraphTokenGateway")), "ONLY_GATEWAY"); + _; + } + + /** + * @dev Initialize this contract. + * The contract will be paused. + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + } + + /** + * @dev Update the next drip nonce + * To be used only as a backup option if the two layers get out of sync. + * @param _nonce Expected value for the nonce of the next drip message + */ + function setNextDripNonce(uint256 _nonce) external onlyGovernor { + nextDripNonce = _nonce; + emit NextDripNonceUpdated(_nonce); + } + + /** + * @dev Get new total rewards accumulated since the last drip. + * This is deltaR = p * r ^ (blocknum - t0) - p, where: + * - p is the normalized token supply snapshot at t0 + * - t0 is the last drip block, i.e. lastRewardsUpdateBlock + * - r is the issuanceRate + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on L2 since the last drip + */ + function getNewRewards(uint256 blocknum) + public + view + override(Reservoir, IReservoir) + returns (uint256 deltaRewards) + { + uint256 t0 = lastRewardsUpdateBlock; + if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { + return 0; + } + deltaRewards = normalizedTokenSupplyCache + .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(normalizedTokenSupplyCache); + } + + /** + * @dev Receive dripped tokens from L1. + * This function can only be called by the gateway, as it is + * meant to be a callhook when receiving tokens from L1. It + * updates the normalizedTokenSupplyCache and issuanceRate, + * and snapshots the accumulated rewards. If issuanceRate changes, + * it also triggers a snapshot of rewards per signal on the RewardsManager. + * @param _normalizedTokenSupply Snapshot of total GRT supply multiplied by L2 rewards fraction + * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 + * @param _nonce Incrementing nonce to ensure messages are received in order + */ + function receiveDrip( + uint256 _normalizedTokenSupply, + uint256 _issuanceRate, + uint256 _nonce + ) external override onlyL2Gateway { + require(_nonce == nextDripNonce, "INVALID_NONCE"); + nextDripNonce = nextDripNonce.add(1); + if (_issuanceRate != issuanceRate) { + rewardsManager().updateAccRewardsPerSignal(); + snapshotAccumulatedRewards(); + issuanceRate = _issuanceRate; + emit IssuanceRateUpdated(_issuanceRate); + } else { + snapshotAccumulatedRewards(); + } + normalizedTokenSupplyCache = _normalizedTokenSupply; + emit DripReceived(normalizedTokenSupplyCache); + } + + /** + * @dev Snapshot accumulated rewards on this layer + * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. + */ + function snapshotAccumulatedRewards() internal { + accumulatedLayerRewards = getAccumulatedRewards(block.number); + lastRewardsUpdateBlock = block.number; + } +} diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol new file mode 100644 index 000000000..c6ad73e1c --- /dev/null +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @dev Storage variables for the L2Reservoir + */ +contract L2ReservoirV1Storage { + // Snapshot of total GRT supply multiplied by L2 rewards fraction, received from L1 + uint256 public normalizedTokenSupplyCache; + // Expected nonce value for the next drip hook + uint256 public nextDripNonce; +} diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol new file mode 100644 index 000000000..dfc64f14f --- /dev/null +++ b/contracts/reservoir/IReservoir.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @title Interface for the Rewards Reservoir + * @dev This is the shared interface between L1 and L2, for the contracts + * that hold rewards on each layers and provide functions to compute + * accumulated and new total rewards. + */ +interface IReservoir { + // Emitted when the issuance rate is updated + event IssuanceRateUpdated(uint256 _newValue); + + /** + * @dev Approve the RewardsManager to manage the reservoir's token funds + */ + function approveRewardsManager() external; + + /** + * @dev Get accumulated total rewards on this layer at a particular block + * @param blocknum Block number at which to calculate rewards + * @return totalRewards Accumulated total rewards on this layer + */ + function getAccumulatedRewards(uint256 blocknum) external view returns (uint256 totalRewards); + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on this layer since the last drip + */ + function getNewRewards(uint256 blocknum) external view returns (uint256 deltaRewards); +} + +/** + * @title Interface for the L2 Rewards Reservoir + * @dev This exposes a specific function for the L2Reservoir that is called + * as a callhook from L1 to L2, so that state can be updated when dripped rewards + * are bridged between layers. + */ +interface IL2Reservoir is IReservoir { + /** + * @dev Receive dripped tokens from L1. + * This function can only be called by the gateway, as it is + * meant to be a callhook when receiving tokens from L1. It + * updates the normalizedTokenSupplyCache and issuanceRate, + * and snapshots the accumulated rewards. If issuanceRate changes, + * it also triggers a snapshot of rewards per signal on the RewardsManager. + * @param _normalizedTokenSupply Snapshot of total GRT supply multiplied by L2 rewards fraction + * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 + * @param _nonce Incrementing nonce to ensure messages are received in order + */ + function receiveDrip( + uint256 _normalizedTokenSupply, + uint256 _issuanceRate, + uint256 _nonce + ) external; +} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol new file mode 100644 index 000000000..3b020648d --- /dev/null +++ b/contracts/reservoir/L1Reservoir.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../arbitrum/ITokenGateway.sol"; + +import "./IReservoir.sol"; +import "./Reservoir.sol"; +import "./L1ReservoirStorage.sol"; + +/** + * @title L1 Rewards Reservoir + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 1. + * It provides a function to periodically drip rewards, and functions to compute accumulated and new + * total rewards at a particular block number. + */ +contract L1Reservoir is L1ReservoirV1Storage, Reservoir { + using SafeMath for uint256; + + // Emitted when the initial supply snapshot is taken after contract deployment + event InitialSnapshotTaken( + uint256 _blockNumber, + uint256 _tokenSupplyCache, + uint256 _mintedPendingRewards + ); + // Emitted when an issuance rate update is staged, to be applied on the next drip + event IssuanceRateStaged(uint256 _newValue); + // Emitted when an L2 rewards fraction update is staged, to be applied on the next drip + event L2RewardsFractionStaged(uint256 _newValue); + // Emitted when the L2 rewards fraction is updated (during a drip) + event L2RewardsFractionUpdated(uint256 _newValue); + // Emitted when the drip interval is updated + event DripIntervalUpdated(uint256 _newValue); + // Emitted when new rewards are dripped and potentially sent to L2 + event RewardsDripped(uint256 _totalMinted, uint256 _sentToL2, uint256 _nextDeadline); + // Emitted when the address for the L2Reservoir is updated + event L2ReservoirAddressUpdated(address _l2ReservoirAddress); + + /** + * @dev Initialize this contract. + * The contract will be paused. + * @param _controller Address of the Controller that manages this contract + * @param _dripInterval Drip interval, i.e. time period for which rewards are minted each time we drip + */ + function initialize(address _controller, uint256 _dripInterval) external onlyImpl { + Managed._initialize(_controller); + dripInterval = _dripInterval; + } + + /** + * @dev Sets the drip interval. + * This is the time in the future (in blocks) for which drip() will mint rewards. + * Keep in mind that changing this value will require manually re-adjusting + * the reservoir's token balance, because the first call to drip might produce + * more or less tokens than needed. + * @param _dripInterval The new interval in blocks for which drip() will mint rewards + */ + function setDripInterval(uint256 _dripInterval) external onlyGovernor { + require(_dripInterval > 0, "Drip interval must be > 0"); + dripInterval = _dripInterval; + emit DripIntervalUpdated(_dripInterval); + } + + /** + * @dev Sets the issuance rate. + * The issuance rate is defined as a relative increase of the total supply per block, plus 1. + * This means that it needs to be greater than 1.0, any number under 1.0 is not + * allowed and an issuance rate of 1.0 means no issuance. + * To accommodate a high precision the issuance rate is expressed in wei, i.e. fixed point at 1e18. + * @param _issuanceRate Issuance rate expressed in wei / fixed point at 1e18 + */ + function setIssuanceRate(uint256 _issuanceRate) external onlyGovernor { + require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); + nextIssuanceRate = _issuanceRate; + emit IssuanceRateStaged(_issuanceRate); + } + + /** + * @dev Sets the L2 rewards fraction. + * This is the portion of the indexer rewards that are sent to L2. + * The value is in fixed point at 1e18 and must be less than 1. + * @param _l2RewardsFraction Fraction of rewards to send to L2, in wei / fixed point at 1e18 + */ + function setL2RewardsFraction(uint256 _l2RewardsFraction) external onlyGovernor { + require(_l2RewardsFraction <= TOKEN_DECIMALS, "L2 Rewards fraction must be <= 1"); + nextL2RewardsFraction = _l2RewardsFraction; + emit L2RewardsFractionStaged(_l2RewardsFraction); + } + + /** + * @dev Sets the L2 Reservoir address + * This is the address on L2 to which we send tokens for rewards. + * @param _l2ReservoirAddress New address for the L2Reservoir on L2 + */ + function setL2ReservoirAddress(address _l2ReservoirAddress) external onlyGovernor { + l2ReservoirAddress = _l2ReservoirAddress; + emit L2ReservoirAddressUpdated(_l2ReservoirAddress); + } + + /** + * @dev Computes the initial snapshot for token supply and mints any pending rewards + * This will initialize the tokenSupplyCache to the current GRT supply, after which + * we will keep an internal accounting only using newly minted rewards. This function + * will also mint any pending rewards to cover up to the current block for open allocations, + * to be computed off-chain. + * @param pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function + */ + function initialSnapshot(uint256 pendingRewards) external onlyGovernor { + lastRewardsUpdateBlock = block.number; + IGraphToken grt = graphToken(); + grt.mint(address(this), pendingRewards); + tokenSupplyCache = grt.totalSupply(); + emit InitialSnapshotTaken(block.number, tokenSupplyCache, pendingRewards); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2 + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * @param l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + */ + function drip( + uint256 l2MaxGas, + uint256 l2GasPriceBid, + uint256 l2MaxSubmissionCost + ) external payable notPaused { + uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); + uint256 mintedRewardsActual = getNewGlobalRewards(block.number); + // eps = (signed int) mintedRewardsTotal - mintedRewardsActual + + if (nextIssuanceRate != issuanceRate) { + rewardsManager().updateAccRewardsPerSignal(); + snapshotAccumulatedRewards(mintedRewardsActual); // This updates lastRewardsUpdateBlock + issuanceRate = nextIssuanceRate; + emit IssuanceRateUpdated(issuanceRate); + } else { + snapshotAccumulatedRewards(mintedRewardsActual); + } + + rewardsMintedUntilBlock = block.number.add(dripInterval); + // n = deltaR(t1, t0) + uint256 newRewardsToDistribute = getNewGlobalRewards(rewardsMintedUntilBlock); + // N = n - eps + uint256 tokensToMint = newRewardsToDistribute.add(mintedRewardsActual).sub( + mintedRewardsTotal + ); + + if (tokensToMint > 0) { + graphToken().mint(address(this), tokensToMint); + } + + uint256 tokensToSendToL2 = 0; + if (l2RewardsFraction != nextL2RewardsFraction) { + tokensToSendToL2 = nextL2RewardsFraction.mul(newRewardsToDistribute).div( + TOKEN_DECIMALS + ); + if (mintedRewardsTotal > mintedRewardsActual) { + // eps > 0, i.e. t < t1_old + // Note this can fail if the old l2RewardsFraction is larger + // than the new, in which case we just have to wait until enough time has passed + // so that eps is small enough. + tokensToSendToL2 = tokensToSendToL2.sub( + l2RewardsFraction.mul(mintedRewardsTotal.sub(mintedRewardsActual)).div( + TOKEN_DECIMALS + ) + ); + } else { + tokensToSendToL2 = tokensToSendToL2.add( + l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( + TOKEN_DECIMALS + ) + ); + } + l2RewardsFraction = nextL2RewardsFraction; + emit L2RewardsFractionUpdated(l2RewardsFraction); + _sendNewTokensAndStateToL2( + tokensToSendToL2, + l2MaxGas, + l2GasPriceBid, + l2MaxSubmissionCost + ); + } else if (l2RewardsFraction > 0) { + tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(TOKEN_DECIMALS); + _sendNewTokensAndStateToL2( + tokensToSendToL2, + l2MaxGas, + l2GasPriceBid, + l2MaxSubmissionCost + ); + } else { + // Avoid locking funds in this contract if we don't need to + // send a message to L2. + require(msg.value == 0, "No eth value needed"); + } + emit RewardsDripped(tokensToMint, tokensToSendToL2, rewardsMintedUntilBlock); + } + + /** + * @dev Snapshot accumulated rewards on this layer + * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. + * We also update the tokenSupplyCache by adding the new total rewards on both layers. + * @param globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block + */ + function snapshotAccumulatedRewards(uint256 globalDelta) internal { + tokenSupplyCache = tokenSupplyCache + globalDelta; + // Reimplementation of getAccumulatedRewards but reusing the globalDelta calculated above, + // to save gas + accumulatedLayerRewards = + accumulatedLayerRewards + + globalDelta.mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div(TOKEN_DECIMALS); + lastRewardsUpdateBlock = block.number; + } + + /** + * @dev Send new tokens and a message with state to L2 + * This function will use the L1GraphTokenGateway to send tokens + * to L2, and will also encode a callhook to update state on the L2Reservoir. + * @param nTokens Number of tokens to send to L2 + * @param maxGas Max gas for the L2 retryable ticket execution + * @param gasPriceBid Gas price for the L2 retryable ticket execution + * @param maxSubmissionCost Max submission price for the L2 retryable ticket + */ + function _sendNewTokensAndStateToL2( + uint256 nTokens, + uint256 maxGas, + uint256 gasPriceBid, + uint256 maxSubmissionCost + ) internal { + uint256 normalizedSupply = l2RewardsFraction.mul(tokenSupplyCache).div(TOKEN_DECIMALS); + bytes memory extraData = abi.encodeWithSelector( + IL2Reservoir.receiveDrip.selector, + normalizedSupply, + issuanceRate, + nextDripNonce + ); + nextDripNonce = nextDripNonce.add(1); + bytes memory data = abi.encode(maxSubmissionCost, extraData); + IGraphToken grt = graphToken(); + ITokenGateway gateway = ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); + grt.approve(address(gateway), nTokens); + gateway.outboundTransfer{ value: msg.value }( + address(grt), + l2ReservoirAddress, + nTokens, + maxGas, + gasPriceBid, + data + ); + } + + /** + * @dev Get new total rewards on both layers at a particular block, since the last drip event + * This is deltaR = p * r ^ (blocknum - t0) - p, where: + * - p is the total token supply snapshot at t0 + * - t0 is the last drip block, i.e. lastRewardsUpdateBlock + * - r is the issuanceRate + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on both layers since the last drip + */ + function getNewGlobalRewards(uint256 blocknum) public view returns (uint256 deltaRewards) { + uint256 t0 = lastRewardsUpdateBlock; + if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { + return 0; + } + deltaRewards = tokenSupplyCache + .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(tokenSupplyCache); + } + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event + * This is deltaR_L1 = (1-lambda) * deltaR, where: + * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) + * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on Layer 1 since the last drip + */ + function getNewRewards(uint256 blocknum) public view override returns (uint256 deltaRewards) { + deltaRewards = getNewGlobalRewards(blocknum).mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div( + TOKEN_DECIMALS + ); + } +} diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol new file mode 100644 index 000000000..f8254d59f --- /dev/null +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @dev Storage variables for the L1Reservoir + */ +contract L1ReservoirV1Storage { + // Fraction of total rewards to be sent by L2, expressed in fixed point at 1e18 + uint256 public l2RewardsFraction; + // New fraction of total rewards to be sent by L2, to be applied on the next drip + uint256 public nextL2RewardsFraction; + // Address for the L2Reservoir to which we send rewards + address public l2ReservoirAddress; + // Block until the minted supplies should last before another drip is needed + uint256 public rewardsMintedUntilBlock; + // Snapshot of initial token supply plus accumulated global rewards + uint256 public tokenSupplyCache; + // New issuance rate to be applied on the next drip + uint256 public nextIssuanceRate; + // Interval for rewards drip, in blocks + uint256 public dripInterval; + // Auto-incrementing nonce that will be used when sending rewards to L2, to ensure ordering + uint256 public nextDripNonce; +} diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol new file mode 100644 index 000000000..ba19de015 --- /dev/null +++ b/contracts/reservoir/Reservoir.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../upgrades/GraphUpgradeable.sol"; + +import "./ReservoirStorage.sol"; +import "./IReservoir.sol"; + +/** + * @title Rewards Reservoir base contract + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 1 or Layer 2. + * It provides functions to compute accumulated and new total rewards at a particular block number. + * This base contract provides functionality that is common to L1 and L2, to be extended on each layer. + */ +abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir { + using SafeMath for uint256; + + uint256 private constant MAX_UINT256 = 2**256 - 1; + uint256 internal constant TOKEN_DECIMALS = 1e18; + uint256 internal constant MIN_ISSUANCE_RATE = 1e18; + + /** + * @dev Approve the RewardsManager to manage the reservoir's token funds + */ + function approveRewardsManager() external override onlyGovernor { + graphToken().approve(address(rewardsManager()), MAX_UINT256); + } + + /** + * @dev Get accumulated total rewards on this layer at a particular block + * @param blocknum Block number at which to calculate rewards + * @return totalRewards Accumulated total rewards on this layer + */ + function getAccumulatedRewards(uint256 blocknum) + public + view + override + returns (uint256 totalRewards) + { + // R(t) = R(t0) + (DeltaR(t, t0)) + totalRewards = accumulatedLayerRewards + getNewRewards(blocknum); + } + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event. + * Must be implemented by the reservoir on each layer. + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on this layer since the last drip + */ + function getNewRewards(uint256 blocknum) + public + view + virtual + override + returns (uint256 deltaRewards); + + /** + * @dev Raises x to the power of n with scaling factor of base. + * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 + * @param x Base of the exponentiation + * @param n Exponent + * @param base Scaling factor + * @return z Exponential of n with base x + */ + function _pow( + uint256 x, + uint256 n, + uint256 base + ) internal pure returns (uint256 z) { + // solhint-disable-next-line no-inline-assembly + assembly { + switch x + case 0 { + switch n + case 0 { + z := base + } + default { + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + z := base + } + default { + z := x + } + let half := div(base, 2) // for rounding. + for { + n := div(n, 2) + } n { + n := div(n, 2) + } { + let xx := mul(x, x) + if iszero(eq(div(xx, x), x)) { + revert(0, 0) + } + let xxRound := add(xx, half) + if lt(xxRound, xx) { + revert(0, 0) + } + x := div(xxRound, base) + if mod(n, 2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { + revert(0, 0) + } + let zxRound := add(zx, half) + if lt(zxRound, zx) { + revert(0, 0) + } + z := div(zxRound, base) + } + } + } + } + } +} diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol new file mode 100644 index 000000000..18f1cf9d4 --- /dev/null +++ b/contracts/reservoir/ReservoirStorage.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "./IReservoir.sol"; +import "../governance/Managed.sol"; + +/** + * @dev Base storage variables for the Reservoir on both layers + */ +contract ReservoirV1Storage is Managed { + // Relative increase of the total supply per block, plus 1, expressed in fixed point at 1e18. + uint256 public issuanceRate; + // Accumulated total rewards on the corresponding layer (L1 or L2) + uint256 public accumulatedLayerRewards; + // Last block at which rewards when updated, i.e. block at which the last drip happened or was received + uint256 public lastRewardsUpdateBlock; +} diff --git a/contracts/rewards/IRewardsManager.sol b/contracts/rewards/IRewardsManager.sol index dc17c8ba8..f92ca422a 100644 --- a/contracts/rewards/IRewardsManager.sol +++ b/contracts/rewards/IRewardsManager.sol @@ -15,8 +15,6 @@ interface IRewardsManager { // -- Config -- - function setIssuanceRate(uint256 _issuanceRate) external; - function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; // -- Denylist -- @@ -54,6 +52,8 @@ interface IRewardsManager { function takeRewards(address _allocationID) external returns (uint256); + function takeAndBurnRewards(address _allocationID) external; + // -- Hooks -- function onSubgraphSignalUpdate(bytes32 _subgraphDeploymentID) external returns (uint256); diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 6d8f1be74..ed832a42c 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -10,6 +10,8 @@ import "../upgrades/GraphUpgradeable.sol"; import "./RewardsManagerStorage.sol"; import "./IRewardsManager.sol"; +import "../reservoir/IReservoir.sol"; + /** * @title Rewards Manager Contract * @dev Tracks how inflationary GRT rewards should be handed out. Relies on the Curation contract @@ -27,7 +29,7 @@ import "./IRewardsManager.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; uint256 private constant TOKEN_DECIMALS = 1e18; @@ -46,9 +48,24 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa ); /** - * @dev Emitted when rewards are denied to an indexer. + * @dev Emitted when rewards are denied to an indexer (and therefore burned). */ - event RewardsDenied(address indexed indexer, address indexed allocationID, uint256 epoch); + event RewardsDenied( + address indexed indexer, + address indexed allocationID, + uint256 epoch, + uint256 amount + ); + + /** + * @dev Emitted when rewards for an indexer are burned . + */ + event RewardsBurned( + address indexed indexer, + address indexed allocationID, + uint256 epoch, + uint256 amount + ); /** * @dev Emitted when a subgraph is denied for claiming rewards. @@ -74,32 +91,6 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa // -- Config -- - /** - * @dev Sets the issuance rate. - * The issuance rate is defined as a percentage increase of the total supply per block. - * This means that it needs to be greater than 1.0, any number under 1.0 is not - * allowed and an issuance rate of 1.0 means no issuance. - * To accommodate a high precision the issuance rate is expressed in wei. - * @param _issuanceRate Issuance rate expressed in wei - */ - function setIssuanceRate(uint256 _issuanceRate) external override onlyGovernor { - _setIssuanceRate(_issuanceRate); - } - - /** - * @dev Sets the issuance rate. - * @param _issuanceRate Issuance rate - */ - function _setIssuanceRate(uint256 _issuanceRate) private { - require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); - - // Called since `issuance rate` will change - updateAccRewardsPerSignal(); - - issuanceRate = _issuanceRate; - emit ParameterUpdated("issuanceRate"); - } - /** * @dev Sets the subgraph oracle allowed to denegate distribution of rewards to subgraphs. * @param _subgraphAvailabilityOracle Address of the subgraph availability oracle @@ -187,32 +178,13 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Gets the issuance of rewards per signal since last updated. * - * Compound interest formula: `a = p(1 + r/n)^nt` - * The formula is simplified with `n = 1` as we apply the interest once every time step. - * The `r` is passed with +1 included. So for 10% instead of 0.1 it is 1.1 - * The simplified formula is `a = p * r^t` - * - * Notation: - * t: time steps are in blocks since last updated - * p: total supply of GRT tokens - * a: inflated amount of total supply for the period `t` when interest `r` is applied - * x: newly accrued rewards token for the period `t` + * The compound interest formula is applied in the Reservoir contract. + * This function will compare accumulated rewards at the current block + * with the value that was cached at accRewardsPerSignalLastBlockUpdated. * * @return newly accrued rewards per signal since last update */ function getNewRewardsPerSignal() public view override returns (uint256) { - // Calculate time steps - uint256 t = block.number.sub(accRewardsPerSignalLastBlockUpdated); - // Optimization to skip calculations if zero time steps elapsed - if (t == 0) { - return 0; - } - - // Zero issuance under a rate of 1.0 - if (issuanceRate <= MIN_ISSUANCE_RATE) { - return 0; - } - // Zero issuance if no signalled tokens IGraphToken graphToken = graphToken(); uint256 signalledTokens = graphToken.balanceOf(address(curation())); @@ -220,16 +192,14 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return 0; } - uint256 r = issuanceRate; - uint256 p = tokenSupplySnapshot; - uint256 a = p.mul(_pow(r, t, TOKEN_DECIMALS)).div(TOKEN_DECIMALS); - - // New issuance of tokens during time steps - uint256 x = a.sub(p); + uint256 accRewardsNow = reservoir().getAccumulatedRewards(block.number); // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number - return x.mul(TOKEN_DECIMALS).div(signalledTokens); + return + (accRewardsNow.sub(accRewardsOnLastSignalUpdate)).mul(TOKEN_DECIMALS).div( + signalledTokens + ); } /** @@ -312,7 +282,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; - tokenSupplySnapshot = graphToken().totalSupply(); + accRewardsOnLastSignalUpdate = reservoir().getAccumulatedRewards(block.number); return accRewardsPerSignal; } @@ -399,7 +369,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Pull rewards from the contract for a particular allocation. * This function can only be called by the Staking contract. - * This function will mint the necessary tokens to reward based on the inflation calculation. + * This function will transfer the necessary tokens to reward based on the inflation calculation. * @param _allocationID Allocation * @return Assigned rewards amount */ @@ -413,90 +383,60 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa alloc.subgraphDeploymentID ); - // Do not do rewards on denied subgraph deployments ID - if (isDenied(alloc.subgraphDeploymentID)) { - emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch); - return 0; - } - // Calculate rewards accrued by this allocation uint256 rewards = _calcRewards( alloc.tokens, alloc.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken ); - if (rewards > 0) { - // Mint directly to staking contract for the reward amount + if (!isDenied(alloc.subgraphDeploymentID)) { + // Transfer to staking contract for the reward amount // The staking contract will do bookkeeping of the reward and // assign in proportion to each stakeholder incentive - graphToken().mint(address(staking), rewards); + if (rewards > 0) { + graphToken().transferFrom(address(reservoir()), address(staking), rewards); + } + emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + } else { + if (rewards > 0) { + graphToken().burnFrom(address(reservoir()), rewards); + } + emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + return 0; } - emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); - return rewards; } /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x + * @dev Burn rewards for a particular allocation. + * This function can only be called by the Staking contract. + * This function will burn the necessary tokens to reward based on the inflation calculation. + * @param _allocationID Allocation */ - function _pow( - uint256 x, - uint256 n, - uint256 base - ) private pure returns (uint256 z) { - assembly { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } + function takeAndBurnRewards(address _allocationID) external override { + // Only Staking contract is authorized as caller + IStaking staking = staking(); + require(msg.sender == address(staking), "Caller must be the staking contract"); + + IStaking.Allocation memory alloc = staking.getAllocation(_allocationID); + uint256 accRewardsPerAllocatedToken = onSubgraphAllocationUpdate( + alloc.subgraphDeploymentID + ); + + // Calculate rewards accrued by this allocation + uint256 rewards = _calcRewards( + alloc.tokens, + alloc.accRewardsPerAllocatedToken, + accRewardsPerAllocatedToken + ); + if (rewards > 0) { + graphToken().burnFrom(address(reservoir()), rewards); + emit RewardsBurned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); } } + + function reservoir() internal view returns (IReservoir) { + return IReservoir(_resolveContract(keccak256("Reservoir"))); + } } diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index 7626992da..d8a6284e5 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -8,7 +8,7 @@ import "../governance/Managed.sol"; contract RewardsManagerV1Storage is Managed { // -- State -- - uint256 public issuanceRate; + uint256 public issuanceRateDeprecated; uint256 public accRewardsPerSignal; uint256 public accRewardsPerSignalLastBlockUpdated; @@ -29,5 +29,10 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage { contract RewardsManagerV3Storage is RewardsManagerV2Storage { // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated - uint256 public tokenSupplySnapshot; + uint256 public tokenSupplySnapshotDeprecated; +} + +contract RewardsManagerV4Storage is RewardsManagerV3Storage { + // Accumulated rewards at accRewardsPerSignalLastBlockUpdated + uint256 public accRewardsOnLastSignalUpdate; } diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index 0e582fc83..94be54420 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -1202,11 +1202,12 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } rebatePool.addToPool(alloc.collectedFees, alloc.effectiveAllocation); - // Distribute rewards if proof of indexing was presented by the indexer or operator + // Distribute rewards if proof of indexing was presented by the indexer or operator, + // otherwise the rewards will be burned from the reservoir. if (isIndexer && _poi != 0) { _distributeRewards(_allocationID, alloc.indexer); } else { - _updateRewards(alloc.subgraphDeploymentID); + _takeAndBurnRewards(_allocationID); } // Free allocated tokens from use @@ -1584,7 +1585,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } // Automatically triggers update of rewards snapshot as allocation will change - // after this call. Take rewards mint tokens for the Staking contract to distribute + // after this call. Take rewards transfers tokens for the Staking contract to distribute // between indexer and delegators uint256 totalRewards = rewardsManager.takeRewards(_allocationID); if (totalRewards == 0) { @@ -1604,6 +1605,21 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { ); } + /** + * @dev Burn rewards for the closed allocation and update the allocation state. + * @param _allocationID Allocation + */ + function _takeAndBurnRewards(address _allocationID) private { + IRewardsManager rewardsManager = rewardsManager(); + if (address(rewardsManager) == address(0)) { + return; + } + + // Automatically triggers update of rewards snapshot as allocation will change + // after this call. + rewardsManager.takeAndBurnRewards(_allocationID); + } + /** * @dev Send rewards to the appropiate destination. * @param _graphToken Graph token diff --git a/contracts/tests/ReservoirMock.sol b/contracts/tests/ReservoirMock.sol new file mode 100644 index 000000000..2ce40b3f2 --- /dev/null +++ b/contracts/tests/ReservoirMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "../reservoir/Reservoir.sol"; + +// Mock contract used for testing rewards +contract ReservoirMock is Reservoir { + function getNewRewards(uint256) public view override returns (uint256 r) {} + + /** + * @dev Raises x to the power of n with scaling factor of base. + * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 + * @param x Base of the exponentiation + * @param n Exponent + * @param base Scaling factor + * @return z Exponential of n with base x + */ + function pow( + uint256 x, + uint256 n, + uint256 base + ) public pure returns (uint256 z) { + z = _pow(x, n, base); + } +} diff --git a/contracts/tests/RewardsManagerMock.sol b/contracts/tests/RewardsManagerMock.sol deleted file mode 100644 index cbd57b2d3..000000000 --- a/contracts/tests/RewardsManagerMock.sol +++ /dev/null @@ -1,68 +0,0 @@ -pragma solidity ^0.7.6; -pragma abicoder v2; - -// Mock contract used for testing rewards -contract RewardsManagerMock { - /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x - */ - function pow( - uint256 x, - uint256 n, - uint256 base - ) public pure returns (uint256 z) { - assembly { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } - } - } -} diff --git a/contracts/token/IGraphToken.sol b/contracts/token/IGraphToken.sol index 41ca4838b..8255e18d5 100644 --- a/contracts/token/IGraphToken.sol +++ b/contracts/token/IGraphToken.sol @@ -9,6 +9,8 @@ interface IGraphToken is IERC20 { function burn(uint256 amount) external; + function burnFrom(address _from, uint256 amount) external; + function mint(address _to, uint256 _amount) external; // -- Mint Admin -- diff --git a/hardhat.config.ts b/hardhat.config.ts index 11557e15b..76391d3c7 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -62,7 +62,11 @@ const networkConfigs: NetworkConfig[] = [ { network: 'kovan', chainId: 42 }, { network: 'arbitrum-rinkeby', chainId: 421611, url: 'https://rinkeby.arbitrum.io/rpc' }, { network: 'arbitrum-one', chainId: 42161, url: 'https://arb1.arbitrum.io/rpc' }, - { network: 'arbitrum-nitro-devnet', chainId: 421612, url: 'https://nitro-devnet.arbitrum.io/rpc' }, + { + network: 'arbitrum-nitro-devnet', + chainId: 421612, + url: 'https://nitro-devnet.arbitrum.io/rpc', + }, ] function getAccountMnemonic() { diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index 6e69acf7a..69a48486f 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -7,8 +7,7 @@ import { InboxMock } from '../../build/types/InboxMock' import { OutboxMock } from '../../build/types/OutboxMock' import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' -import { NetworkFixture } from '../lib/fixtures' -import { deployContract } from '../lib/deployment' +import { NetworkFixture, ArbitrumL1Mocks, L1FixtureContracts } from '../lib/fixtures' import { getAccounts, @@ -30,6 +29,7 @@ describe('L1GraphTokenGateway', () => { let mockL2GRT: Account let mockL2Gateway: Account let pauseGuardian: Account + let mockL2Reservoir: Account let fixture: NetworkFixture let grt: GraphToken @@ -39,6 +39,9 @@ describe('L1GraphTokenGateway', () => { let inboxMock: InboxMock let outboxMock: OutboxMock + let arbitrumMocks: ArbitrumL1Mocks + let fixtureContracts: L1FixtureContracts + const senderTokens = toGRT('1000') const maxGas = toBN('1000000') const maxSubmissionCost = toBN('7') @@ -56,18 +59,26 @@ describe('L1GraphTokenGateway', () => { ) before(async function () { - ;[governor, tokenSender, l2Receiver, mockRouter, mockL2GRT, mockL2Gateway, pauseGuardian] = - await getAccounts() + ;[ + governor, + tokenSender, + l2Receiver, + mockRouter, + mockL2GRT, + mockL2Gateway, + pauseGuardian, + mockL2Reservoir, + ] = await getAccounts() fixture = new NetworkFixture() - ;({ grt, l1GraphTokenGateway, bridgeEscrow } = await fixture.load(governor.signer)) + fixtureContracts = await fixture.load(governor.signer) + ;({ grt, l1GraphTokenGateway, bridgeEscrow } = fixtureContracts) // Give some funds to the token sender await grt.connect(governor.signer).mint(tokenSender.address, senderTokens) // Deploy contracts that mock Arbitrum's bridge contracts - bridgeMock = (await deployContract('BridgeMock', governor.signer)) as unknown as BridgeMock - inboxMock = (await deployContract('InboxMock', governor.signer)) as unknown as InboxMock - outboxMock = (await deployContract('OutboxMock', governor.signer)) as unknown as OutboxMock + arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) + ;({ bridgeMock, inboxMock, outboxMock } = arbitrumMocks) }) beforeEach(async function () { @@ -362,23 +373,15 @@ describe('L1GraphTokenGateway', () => { await expect(senderBalance).eq(toGRT('990')) } before(async function () { - // First configure the Arbitrum bridge mocks - await bridgeMock.connect(governor.signer).setInbox(inboxMock.address, true) - await bridgeMock.connect(governor.signer).setOutbox(outboxMock.address, true) - await inboxMock.connect(governor.signer).setBridge(bridgeMock.address) - await outboxMock.connect(governor.signer).setBridge(bridgeMock.address) - - // Configure the gateway - await l1GraphTokenGateway - .connect(governor.signer) - .setArbitrumAddresses(inboxMock.address, mockRouter.address) - await l1GraphTokenGateway.connect(governor.signer).setL2TokenAddress(mockL2GRT.address) - await l1GraphTokenGateway - .connect(governor.signer) - .setL2CounterpartAddress(mockL2Gateway.address) - await l1GraphTokenGateway.connect(governor.signer).setEscrowAddress(bridgeEscrow.address) - await bridgeEscrow.connect(governor.signer).approveAll(l1GraphTokenGateway.address) - await l1GraphTokenGateway.connect(governor.signer).setPaused(false) + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + mockL2Reservoir.address, + ) }) describe('calculateL2TokenAddress', function () { diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index 71909d587..fcdd9d78d 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -1,11 +1,10 @@ import { expect, use } from 'chai' import { constants, ContractTransaction, Signer, utils } from 'ethers' -import hre from 'hardhat' import { L2GraphToken } from '../../build/types/L2GraphToken' import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' -import { NetworkFixture } from '../lib/fixtures' +import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' import { FakeContract, smock } from '@defi-wonderland/smock' @@ -13,25 +12,15 @@ import path from 'path' import { Artifacts } from 'hardhat/internal/artifacts' const ARTIFACTS_PATH = path.resolve('build/contracts') const artifacts = new Artifacts(ARTIFACTS_PATH) -const rewardsManagerMockAbi = artifacts.readArtifactSync('RewardsManagerMock').abi +const reservoirMockAbi = artifacts.readArtifactSync('ReservoirMock').abi use(smock.matchers) -import { getAccounts, toGRT, Account, provider, applyL1ToL2Alias, toBN } from '../lib/testHelpers' +import { getAccounts, toGRT, Account, toBN, getL2SignerFromL1 } from '../lib/testHelpers' import { Interface } from 'ethers/lib/utils' const { AddressZero } = constants -// Adapted from: -// https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/test/utils/messaging.ts#L5 -async function getL2SignerFromL1(l1Address: string): Promise { - const l2Address = applyL1ToL2Alias(l1Address) - await provider().send('hardhat_impersonateAccount', [l2Address]) - const l2Signer = await hre.ethers.getSigner(l2Address) - - return l2Signer -} - describe('L2GraphTokenGateway', () => { let me: Account let governor: Account @@ -42,16 +31,18 @@ describe('L2GraphTokenGateway', () => { let mockL1GRT: Account let mockL1Gateway: Account let pauseGuardian: Account + let mockL1Reservoir: Account let fixture: NetworkFixture let arbSysMock: FakeContract + let fixtureContracts: L2FixtureContracts let grt: L2GraphToken let l2GraphTokenGateway: L2GraphTokenGateway const senderTokens = toGRT('1000') const defaultData = '0x' - const rmmIface = new Interface(rewardsManagerMockAbi) - const notEmptyCallHookData = rmmIface.encodeFunctionData('pow', [toBN(1), toBN(2), toBN(3)]) + const mockIface = new Interface(reservoirMockAbi) + const notEmptyCallHookData = mockIface.encodeFunctionData('pow', [toBN(1), toBN(2), toBN(3)]) const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( ['bytes', 'bytes'], ['0x', notEmptyCallHookData], @@ -68,10 +59,12 @@ describe('L2GraphTokenGateway', () => { mockL1Gateway, l2Receiver, pauseGuardian, + mockL1Reservoir, ] = await getAccounts() fixture = new NetworkFixture() - ;({ grt, l2GraphTokenGateway } = await fixture.loadL2(governor.signer)) + fixtureContracts = await fixture.loadL2(governor.signer) + ;({ grt, l2GraphTokenGateway } = fixtureContracts) // Give some funds to the token sender await grt.connect(governor.signer).mint(tokenSender.address, senderTokens) @@ -297,17 +290,14 @@ describe('L2GraphTokenGateway', () => { await expect(senderBalance).eq(toGRT('990')) } before(async function () { - // Configure the L2 GRT - // Configure the gateway - await grt.connect(governor.signer).setGateway(l2GraphTokenGateway.address) - await grt.connect(governor.signer).setL1Address(mockL1GRT.address) - // Configure the gateway - await l2GraphTokenGateway.connect(governor.signer).setL2Router(mockRouter.address) - await l2GraphTokenGateway.connect(governor.signer).setL1TokenAddress(mockL1GRT.address) - await l2GraphTokenGateway - .connect(governor.signer) - .setL1CounterpartAddress(mockL1Gateway.address) - await l2GraphTokenGateway.connect(governor.signer).setPaused(false) + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + mockL1Reservoir.address, + ) }) describe('calculateL2TokenAddress', function () { @@ -431,29 +421,29 @@ describe('L2GraphTokenGateway', () => { await testValidFinalizeTransfer(defaultData) }) it('does not call any callhooks if the sender is not whitelisted', async function () { - const rewardsManagerMock = await smock.fake('RewardsManagerMock', { + const reservoirMock = await smock.fake('ReservoirMock', { address: l2Receiver.address, }) - rewardsManagerMock.pow.returns(1) + reservoirMock.pow.returns(1) await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData) - expect(rewardsManagerMock.pow).to.not.have.been.called + expect(reservoirMock.pow).to.not.have.been.called }) it('calls a callhook if the sender is whitelisted', async function () { - const rewardsManagerMock = await smock.fake('RewardsManagerMock', { + const reservoirMock = await smock.fake('ReservoirMock', { address: l2Receiver.address, }) - rewardsManagerMock.pow.returns(1) + reservoirMock.pow.returns(1) await l2GraphTokenGateway .connect(governor.signer) .addToCallhookWhitelist(tokenSender.address) await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData) - expect(rewardsManagerMock.pow).to.have.been.calledWith(toBN(1), toBN(2), toBN(3)) + expect(reservoirMock.pow).to.have.been.calledWith(toBN(1), toBN(2), toBN(3)) }) it('reverts if a callhook reverts', async function () { - const rewardsManagerMock = await smock.fake('RewardsManagerMock', { + const reservoirMock = await smock.fake('ReservoirMock', { address: l2Receiver.address, }) - rewardsManagerMock.pow.reverts() + reservoirMock.pow.reverts() await l2GraphTokenGateway .connect(governor.signer) .addToCallhookWhitelist(tokenSender.address) diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts new file mode 100644 index 000000000..0dd54f23c --- /dev/null +++ b/test/l2/l2Reservoir.test.ts @@ -0,0 +1,364 @@ +import { expect } from 'chai' +import { BigNumber, constants, ContractTransaction, utils } from 'ethers' + +import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' + +import { BigNumber as BN } from 'bignumber.js' + +import { + advanceBlocks, + getAccounts, + latestBlock, + toBN, + toGRT, + formatGRT, + Account, + RewardsTracker, + getL2SignerFromL1, +} from '../lib/testHelpers' +import { L2Reservoir } from '../../build/types/L2Reservoir' + +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { L2GraphToken } from '../../build/types/L2GraphToken' + +const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] + +const dripAmount = toBN('5851557519569225000000000') +const dripNormalizedSupply = toGRT('10004000000') +const dripIssuanceRate = toBN('1000000023206889619') + +describe('L2Reservoir', () => { + let governor: Account + let testAccount1: Account + let mockRouter: Account + let mockL1GRT: Account + let mockL1Gateway: Account + let mockL1Reservoir: Account + let fixture: NetworkFixture + + let grt: L2GraphToken + let l2Reservoir: L2Reservoir + let l2GraphTokenGateway: L2GraphTokenGateway + + let fixtureContracts: L2FixtureContracts + + let normalizedSupply: BigNumber + let dripBlock: BigNumber + + const ISSUANCE_RATE_PERIODS = toBN(4) // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block + + // Test accumulated rewards after nBlocksToAdvance, + // asking for the value at blockToQuery + const shouldGetNewRewards = async ( + initialSupply: BigNumber, + nBlocksToAdvance: BigNumber = ISSUANCE_RATE_PERIODS, + blockToQuery?: BigNumber, + expectedValue?: BigNumber, + round = true, + ) => { + // -- t0 -- + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK) + const startAccrued = await l2Reservoir.getAccumulatedRewards(await latestBlock()) + // Jump + await advanceBlocks(nBlocksToAdvance) + + // -- t1 -- + + // Contract calculation + if (!blockToQuery) { + blockToQuery = await latestBlock() + } + const contractAccrued = await l2Reservoir.getAccumulatedRewards(blockToQuery) + // Local calculation + if (expectedValue == null) { + expectedValue = await tracker.newRewards(blockToQuery) + } + + // Check + if (round) { + expect(toRound(contractAccrued.sub(startAccrued))).eq(toRound(expectedValue)) + } else { + expect(contractAccrued.sub(startAccrued)).eq(expectedValue) + } + + return expectedValue + } + + const gatewayFinalizeTransfer = async (callhookData: string): Promise => { + const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) + await testAccount1.signer.sendTransaction({ + to: await mockL1GatewayL2Alias.getAddress(), + value: utils.parseUnits('1', 'ether'), + }) + const data = utils.defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', callhookData]) + const tx = l2GraphTokenGateway + .connect(mockL1GatewayL2Alias) + .finalizeInboundTransfer( + mockL1GRT.address, + mockL1Reservoir.address, + l2Reservoir.address, + dripAmount, + data, + ) + return tx + } + + const validGatewayFinalizeTransfer = async ( + callhookData: string, + ): Promise => { + const tx = await gatewayFinalizeTransfer(callhookData) + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1Reservoir.address, l2Reservoir.address, dripAmount) + + await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Reservoir.address, dripAmount) + + // newly minted GRT + const receiverBalance = await grt.balanceOf(l2Reservoir.address) + await expect(receiverBalance).eq(dripAmount) + return tx + } + + before(async function () { + ;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir] = + await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.loadL2(governor.signer) + ;({ grt, l2Reservoir, l2GraphTokenGateway } = fixtureContracts) + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + mockL1Reservoir.address, + ) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('setNextDripNonce', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir.connect(testAccount1.signer).setNextDripNonce(toBN('10')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets the next expected drip nonce', async function () { + const tx = l2Reservoir.connect(governor.signer).setNextDripNonce(toBN('10')) + await expect(tx).emit(l2Reservoir, 'NextDripNonceUpdated').withArgs(toBN('10')) + await expect(await l2Reservoir.nextDripNonce()).to.eq(toBN('10')) + }) + }) + describe('receiveDrip', async function () { + it('rejects the call when not called by the gateway', async function () { + const tx = l2Reservoir + .connect(governor.signer) + .receiveDrip(dripNormalizedSupply, dripIssuanceRate, toBN('0')) + await expect(tx).revertedWith('ONLY_GATEWAY') + }) + it('rejects the call when received out of order', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + // Incorrect nonce + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate.add(1), + toBN('2'), + ) + const tx2 = gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(tx2).revertedWith('CALLHOOK_FAILED') // Gateway overrides revert message + }) + it('updates the normalized supply cache', async function () { + normalizedSupply = dripNormalizedSupply + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + }) + it('updates the normalized supply cache and issuance rate', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate.add(1), + toBN('1'), + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( + dripNormalizedSupply.add(1), + ) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate.add(1)) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + it('accepts subsequent calls without changing issuance rate', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate, + toBN('1'), + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( + dripNormalizedSupply.add(1), + ) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + it('accepts a different nonce set through setNextDripNonce', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + await l2Reservoir.connect(governor.signer).setNextDripNonce(toBN('2')) + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate, + toBN('2'), + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( + dripNormalizedSupply.add(1), + ) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + }) + + context('calculating rewards', async function () { + beforeEach(async function () { + // 5% minute rate (4 blocks) + normalizedSupply = dripNormalizedSupply + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + ISSUANCE_RATE_PER_BLOCK, + toBN('0'), + ) + await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + }) + + describe('getAccumulatedRewards', function () { + it('returns rewards accrued after some blocks', async function () { + await shouldGetNewRewards(normalizedSupply) + }) + it('returns zero if evaluated at the block where reservoir had the first drip', async function () { + await shouldGetNewRewards( + normalizedSupply, + ISSUANCE_RATE_PERIODS, + dripBlock, + toBN(0), + false, + ) + }) + it('returns the supply times issuance rate one block after the first drip', async function () { + const expectedVal = normalizedSupply + .mul(ISSUANCE_RATE_PER_BLOCK.sub(toGRT(1))) + .div(toGRT(1)) + await shouldGetNewRewards( + normalizedSupply, + ISSUANCE_RATE_PERIODS, + dripBlock.add(1), + expectedVal, + false, + ) + }) + it('returns the rewards for a block some time in the future', async function () { + await shouldGetNewRewards(normalizedSupply, toBN(1), dripBlock.add(10000)) + }) + }) + describe('getNewRewards', function () { + const computeDelta = function (t1: BigNumber, t0: BigNumber, lambda = toBN(0)): BigNumber { + const deltaT = new BN(t1.toString()).minus(new BN(t0.toString())) + const rate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + const supply = new BN(normalizedSupply.toString()) + return toBN(supply.times(rate.pow(deltaT)).minus(supply).precision(18).toString(10)) + .mul(toGRT('1').sub(lambda)) + .div(toGRT('1')) + } + it('computes the rewards delta between the last drip block and the current block', async function () { + const t0 = dripBlock + const t1 = t0.add(200) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('returns zero rewards if the time delta is zero', async function () { + const t0 = dripBlock + const expectedVal = toBN('0') + expect(await l2Reservoir.getNewRewards(t0)).to.eq(expectedVal) + }) + it('computes the rewards delta between a past drip block and a future block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = t0.add(100) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta between a past drip block and the current block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + }) + }) +}) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index cadbc58d0..b475a90c8 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -23,6 +23,8 @@ import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' import { L2GraphToken } from '../../build/types/L2GraphToken' import { BridgeEscrow } from '../../build/types/BridgeEscrow' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { L2Reservoir } from '../../build/types/L2Reservoir' // Disable logging for tests logger.pause() @@ -58,6 +60,7 @@ export const defaults = { }, rewards: { issuanceRate: toGRT('1.000000023206889619'), // 5% annual rate + dripInterval: toBN('50400'), // 1 week in blocks (post-Merge) }, } @@ -303,3 +306,29 @@ export async function deployL2GRT( deployer, ) as unknown as L2GraphToken } + +export async function deployL1Reservoir( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L1Reservoir', + [controller, defaults.rewards.dripInterval], + deployer, + ) as unknown as L1Reservoir +} + +export async function deployL2Reservoir( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L2Reservoir', + [controller], + deployer, + ) as unknown as L2Reservoir +} diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 9f87fd6da..635e9ff51 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -2,7 +2,64 @@ import { utils, Wallet, Signer } from 'ethers' import * as deployment from './deployment' -import { evmSnapshot, evmRevert, initNetwork } from './testHelpers' +import { evmSnapshot, evmRevert, initNetwork, toBN } from './testHelpers' +import { BridgeMock } from '../../build/types/BridgeMock' +import { InboxMock } from '../../build/types/InboxMock' +import { OutboxMock } from '../../build/types/OutboxMock' +import { deployContract } from './deployment' +import { Controller } from '../../build/types/Controller' +import { DisputeManager } from '../../build/types/DisputeManager' +import { EpochManager } from '../../build/types/EpochManager' +import { GraphToken } from '../../build/types/GraphToken' +import { Curation } from '../../build/types/Curation' +import { GNS } from '../../build/types/GNS' +import { Staking } from '../../build/types/Staking' +import { RewardsManager } from '../../build/types/RewardsManager' +import { ServiceRegistry } from '../../build/types/ServiceRegistry' +import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { L2GraphToken } from '../../build/types/L2GraphToken' +import { L2Reservoir } from '../../build/types/L2Reservoir' + +export interface L1FixtureContracts { + controller: Controller + disputeManager: DisputeManager + epochManager: EpochManager + grt: GraphToken + curation: Curation + gns: GNS + staking: Staking + rewardsManager: RewardsManager + serviceRegistry: ServiceRegistry + proxyAdmin: GraphProxyAdmin + l1GraphTokenGateway: L1GraphTokenGateway + bridgeEscrow: BridgeEscrow + l1Reservoir: L1Reservoir +} + +export interface L2FixtureContracts { + controller: Controller + disputeManager: DisputeManager + epochManager: EpochManager + grt: L2GraphToken + curation: Curation + gns: GNS + staking: Staking + rewardsManager: RewardsManager + serviceRegistry: ServiceRegistry + proxyAdmin: GraphProxyAdmin + l2GraphTokenGateway: L2GraphTokenGateway + l2Reservoir: L2Reservoir +} + +export interface ArbitrumL1Mocks { + bridgeMock: BridgeMock + inboxMock: InboxMock + outboxMock: OutboxMock +} export class NetworkFixture { lastSnapshotId: number @@ -11,11 +68,12 @@ export class NetworkFixture { this.lastSnapshotId = 0 } - async load( + async _loadLayer( deployer: Signer, slasher: Signer = Wallet.createRandom() as Signer, arbitrator: Signer = Wallet.createRandom() as Signer, - ): Promise { + isL2: boolean, + ): Promise { await initNetwork() // Roles @@ -30,7 +88,13 @@ export class NetworkFixture { controller.address, proxyAdmin, ) - const grt = await deployment.deployGRT(deployer) + let grt: GraphToken | L2GraphToken + if (isL2) { + grt = await deployment.deployL2GRT(deployer, proxyAdmin) + } else { + grt = await deployment.deployGRT(deployer) + } + const curation = await deployment.deployCuration(deployer, controller.address, proxyAdmin) const gns = await deployment.deployGNS(deployer, controller.address, proxyAdmin) const staking = await deployment.deployStaking(deployer, controller.address, proxyAdmin) @@ -51,17 +115,27 @@ export class NetworkFixture { proxyAdmin, ) - const l1GraphTokenGateway = await deployment.deployL1GraphTokenGateway( - deployer, - controller.address, - proxyAdmin, - ) - - const bridgeEscrow = await deployment.deployBridgeEscrow( - deployer, - controller.address, - proxyAdmin, - ) + let l1GraphTokenGateway: L1GraphTokenGateway + let l2GraphTokenGateway: L2GraphTokenGateway + let bridgeEscrow: BridgeEscrow + let l1Reservoir: L1Reservoir + let l2Reservoir: L2Reservoir + if (isL2) { + l2GraphTokenGateway = await deployment.deployL2GraphTokenGateway( + deployer, + controller.address, + proxyAdmin, + ) + l2Reservoir = await deployment.deployL2Reservoir(deployer, controller.address, proxyAdmin) + } else { + l1GraphTokenGateway = await deployment.deployL1GraphTokenGateway( + deployer, + controller.address, + proxyAdmin, + ) + bridgeEscrow = await deployment.deployBridgeEscrow(deployer, controller.address, proxyAdmin) + l1Reservoir = await deployment.deployL1Reservoir(deployer, controller.address, proxyAdmin) + } // Setup controller await controller.setContractProxy(utils.id('EpochManager'), epochManager.address) @@ -71,7 +145,13 @@ export class NetworkFixture { await controller.setContractProxy(utils.id('DisputeManager'), staking.address) await controller.setContractProxy(utils.id('RewardsManager'), rewardsManager.address) await controller.setContractProxy(utils.id('ServiceRegistry'), serviceRegistry.address) - await controller.setContractProxy(utils.id('GraphTokenGateway'), l1GraphTokenGateway.address) + if (isL2) { + await controller.setContractProxy(utils.id('GraphTokenGateway'), l2GraphTokenGateway.address) + await controller.setContractProxy(utils.id('Reservoir'), l2Reservoir.address) + } else { + await controller.setContractProxy(utils.id('GraphTokenGateway'), l1GraphTokenGateway.address) + await controller.setContractProxy(utils.id('Reservoir'), l1Reservoir.address) + } // Setup contracts await curation.connect(deployer).syncAllContracts() @@ -80,70 +160,167 @@ export class NetworkFixture { await disputeManager.connect(deployer).syncAllContracts() await rewardsManager.connect(deployer).syncAllContracts() await staking.connect(deployer).syncAllContracts() - await l1GraphTokenGateway.connect(deployer).syncAllContracts() - await bridgeEscrow.connect(deployer).syncAllContracts() + if (isL2) { + await l2GraphTokenGateway.connect(deployer).syncAllContracts() + await l2Reservoir.connect(deployer).syncAllContracts() + } else { + await l1GraphTokenGateway.connect(deployer).syncAllContracts() + await bridgeEscrow.connect(deployer).syncAllContracts() + await l1Reservoir.connect(deployer).syncAllContracts() + } await staking.connect(deployer).setSlasher(slasherAddress, true) - await grt.connect(deployer).addMinter(rewardsManager.address) await gns.connect(deployer).approveAll() - await rewardsManager.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) + if (isL2) { + await grt.connect(deployer).addMinter(l2GraphTokenGateway.address) + await l2Reservoir.connect(deployer).approveRewardsManager() + } else { + await grt.connect(deployer).addMinter(l1Reservoir.address) + await l1Reservoir.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) + await l1Reservoir.connect(deployer).approveRewardsManager() + await l1Reservoir.connect(deployer).initialSnapshot(toBN(0)) + } // Unpause the protocol await controller.connect(deployer).setPaused(false) - return { - controller, - disputeManager, - epochManager, - grt, - curation, - gns, - staking, - rewardsManager, - serviceRegistry, - proxyAdmin, - l1GraphTokenGateway, - bridgeEscrow, + if (isL2) { + return { + controller, + disputeManager, + epochManager, + grt: grt as L2GraphToken, + curation, + gns, + staking, + rewardsManager, + serviceRegistry, + proxyAdmin, + l2GraphTokenGateway, + l2Reservoir, + } as L2FixtureContracts + } else { + return { + controller, + disputeManager, + epochManager, + grt: grt as GraphToken, + curation, + gns, + staking, + rewardsManager, + serviceRegistry, + proxyAdmin, + l1GraphTokenGateway, + bridgeEscrow, + l1Reservoir, + } as L1FixtureContracts } } - async loadL2(deployer: Signer): Promise { - await initNetwork() - - // Deploy contracts - const proxyAdmin = await deployment.deployProxyAdmin(deployer) - const controller = await deployment.deployController(deployer) - - const grt = await deployment.deployL2GRT(deployer, proxyAdmin) + async load( + deployer: Signer, + slasher: Signer = Wallet.createRandom() as Signer, + arbitrator: Signer = Wallet.createRandom() as Signer, + ): Promise { + return this._loadLayer(deployer, slasher, arbitrator, false) as unknown as L1FixtureContracts + } - const l2GraphTokenGateway = await deployment.deployL2GraphTokenGateway( - deployer, - controller.address, - proxyAdmin, - ) + async loadL2( + deployer: Signer, + slasher: Signer = Wallet.createRandom() as Signer, + arbitrator: Signer = Wallet.createRandom() as Signer, + ): Promise { + return this._loadLayer(deployer, slasher, arbitrator, true) as unknown as L2FixtureContracts + } - // Setup controller - await controller.setContractProxy(utils.id('GraphToken'), grt.address) - await controller.setContractProxy(utils.id('GraphTokenGateway'), l2GraphTokenGateway.address) + async loadArbitrumL1Mocks(deployer: Signer): Promise { + const bridgeMock = (await deployContract('BridgeMock', deployer)) as unknown as BridgeMock + const inboxMock = (await deployContract('InboxMock', deployer)) as unknown as InboxMock + const outboxMock = (await deployContract('OutboxMock', deployer)) as unknown as OutboxMock + return { + bridgeMock, + inboxMock, + outboxMock, + } + } - // Setup contracts - await l2GraphTokenGateway.connect(deployer).syncAllContracts() - await grt.connect(deployer).addMinter(l2GraphTokenGateway.address) + async configureL1Bridge( + deployer: Signer, + arbitrumMocks: ArbitrumL1Mocks, + l1FixtureContracts: L1FixtureContracts, + mockRouterAddress: string, + mockL2GRTAddress: string, + mockL2GatewayAddress: string, + mockL2ReservoirAddress: string, + ): Promise { + // First configure the Arbitrum bridge mocks + await arbitrumMocks.bridgeMock.connect(deployer).setInbox(arbitrumMocks.inboxMock.address, true) + await arbitrumMocks.bridgeMock + .connect(deployer) + .setOutbox(arbitrumMocks.outboxMock.address, true) + await arbitrumMocks.inboxMock.connect(deployer).setBridge(arbitrumMocks.bridgeMock.address) + await arbitrumMocks.outboxMock.connect(deployer).setBridge(arbitrumMocks.bridgeMock.address) - // Unpause the protocol - await controller.connect(deployer).setPaused(false) + // Configure the gateway + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .setArbitrumAddresses(arbitrumMocks.inboxMock.address, mockRouterAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .setL2TokenAddress(mockL2GRTAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .setL2CounterpartAddress(mockL2GatewayAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .setEscrowAddress(l1FixtureContracts.bridgeEscrow.address) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .addToCallhookWhitelist(l1FixtureContracts.l1Reservoir.address) + await l1FixtureContracts.bridgeEscrow + .connect(deployer) + .approveAll(l1FixtureContracts.l1GraphTokenGateway.address) + await l1FixtureContracts.l1Reservoir + .connect(deployer) + .setL2ReservoirAddress(mockL2ReservoirAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .addToCallhookWhitelist(l1FixtureContracts.l1Reservoir.address) + await l1FixtureContracts.l1GraphTokenGateway.connect(deployer).setPaused(false) + } - return { - controller, - grt, - proxyAdmin, - l2GraphTokenGateway, - } + async configureL2Bridge( + deployer: Signer, + l2FixtureContracts: L2FixtureContracts, + mockRouterAddress: string, + mockL1GRTAddress: string, + mockL1GatewayAddress: string, + mockL1ReservoirAddress: string, + ): Promise { + // Configure the L2 GRT + // Configure the gateway + await l2FixtureContracts.grt + .connect(deployer) + .setGateway(l2FixtureContracts.l2GraphTokenGateway.address) + await l2FixtureContracts.grt.connect(deployer).setL1Address(mockL1GRTAddress) + // Configure the gateway + await l2FixtureContracts.l2GraphTokenGateway.connect(deployer).setL2Router(mockRouterAddress) + await l2FixtureContracts.l2GraphTokenGateway + .connect(deployer) + .setL1TokenAddress(mockL1GRTAddress) + await l2FixtureContracts.l2GraphTokenGateway + .connect(deployer) + .setL1CounterpartAddress(mockL1GatewayAddress) + await l2FixtureContracts.l2GraphTokenGateway + .connect(deployer) + .addToCallhookWhitelist(mockL1ReservoirAddress) + await l2FixtureContracts.l2GraphTokenGateway.connect(deployer).setPaused(false) } async setUp(): Promise { this.lastSnapshotId = await evmSnapshot() - provider().send('evm_setAutomine', [true]) + await initNetwork() } async tearDown(): Promise { diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index 59372dd68..048c0b5a4 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -3,6 +3,7 @@ import '@nomiclabs/hardhat-ethers' import '@nomiclabs/hardhat-waffle' import { providers, utils, BigNumber, Signer, Wallet } from 'ethers' import { formatUnits, getAddress } from 'ethers/lib/utils' +import { BigNumber as BN } from 'bignumber.js' import { EpochManager } from '../../build/types/EpochManager' @@ -132,3 +133,105 @@ export const applyL1ToL2Alias = (l1Address: string): string => { const mask = toBN(2).pow(160) return l2AddressAsNumber.mod(mask).toHexString() } + +// Core formula that gets accumulated rewards for a period of time +const getRewards = (p: BN, r: BN, t: BN): string => { + BN.config({ POW_PRECISION: 100 }) + return p.times(r.pow(t)).minus(p).precision(18).toString(10) +} + +// Tracks the accumulated rewards as supply changes across snapshots +// both at a global level (like the Reservoir) and per signal (like RewardsManager) +export class RewardsTracker { + totalSupply = BigNumber.from(0) + lastUpdatedBlock = BigNumber.from(0) + lastPerSignalUpdatedBlock = BigNumber.from(0) + accumulated = BigNumber.from(0) + accumulatedPerSignal = BigNumber.from(0) + accumulatedAtLastPerSignalUpdatedBlock = BigNumber.from(0) + issuanceRate = BigNumber.from(0) + + static async create( + initialSupply: BigNumber, + issuanceRate: BigNumber, + updatedBlock?: BigNumber, + ): Promise { + const lastUpdatedBlock = updatedBlock || (await latestBlock()) + const tracker = new RewardsTracker(initialSupply, issuanceRate, lastUpdatedBlock) + return tracker + } + + constructor(initialSupply: BigNumber, issuanceRate: BigNumber, updatedBlock: BigNumber) { + this.issuanceRate = issuanceRate + this.totalSupply = initialSupply + this.lastUpdatedBlock = updatedBlock + this.lastPerSignalUpdatedBlock = updatedBlock + } + + async snapshotRewards(initialSupply?: BigNumber, atBlock?: BigNumber): Promise { + const newRewards = await this.newRewards(atBlock) + this.accumulated = this.accumulated.add(newRewards) + this.totalSupply = initialSupply || this.totalSupply.add(newRewards) + this.lastUpdatedBlock = atBlock || (await latestBlock()) + return this.accumulated + } + + async snapshotPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + this.accumulatedPerSignal = await this.accRewardsPerSignal(totalSignal, atBlock) + this.accumulatedAtLastPerSignalUpdatedBlock = await this.accRewards(atBlock) + this.lastPerSignalUpdatedBlock = atBlock + return this.accumulatedPerSignal + } + + async elapsedBlocks(): Promise { + const currentBlock = await latestBlock() + return currentBlock.sub(this.lastUpdatedBlock) + } + + async newRewardsPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + const accRewards = await this.accRewards(atBlock) + const diff = accRewards.sub(this.accumulatedAtLastPerSignalUpdatedBlock) + if (totalSignal.eq(0)) { + return BigNumber.from(0) + } + return diff.mul(toGRT(1)).div(totalSignal) + } + + async accRewardsPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + return this.accumulatedPerSignal.add(await this.newRewardsPerSignal(totalSignal, atBlock)) + } + + async newRewards(atBlock?: BigNumber): Promise { + if (!atBlock) { + atBlock = await latestBlock() + } + const nBlocks = atBlock.sub(this.lastUpdatedBlock) + return this.accruedByElapsed(nBlocks) + } + + async accRewards(atBlock?: BigNumber): Promise { + if (!atBlock) { + atBlock = await latestBlock() + } + return this.accumulated.add(await this.newRewards(atBlock)) + } + + async accruedByElapsed(nBlocks: BigNumber | number): Promise { + const n = getRewards( + new BN(this.totalSupply.toString()), + new BN(this.issuanceRate.toString()).div(1e18), + new BN(nBlocks.toString()), + ) + return BigNumber.from(n) + } +} + +// Adapted from: +// https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/test/utils/messaging.ts#L5 +export async function getL2SignerFromL1(l1Address: string): Promise { + const l2Address = applyL1ToL2Alias(l1Address) + await provider().send('hardhat_impersonateAccount', [l2Address]) + const l2Signer = await hre.ethers.getSigner(l2Address) + + return l2Signer +} diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts new file mode 100644 index 000000000..7a4920159 --- /dev/null +++ b/test/reservoir/l1Reservoir.test.ts @@ -0,0 +1,750 @@ +import { expect } from 'chai' +import { BigNumber, constants, utils } from 'ethers' + +import { defaults, deployContract } from '../lib/deployment' +import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' + +import { GraphToken } from '../../build/types/GraphToken' +import { ReservoirMock } from '../../build/types/ReservoirMock' +import { BigNumber as BN } from 'bignumber.js' + +import { + advanceBlocks, + getAccounts, + latestBlock, + toBN, + toGRT, + formatGRT, + Account, + RewardsTracker, + provider, +} from '../lib/testHelpers' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' + +import path from 'path' +import { Artifacts } from 'hardhat/internal/artifacts' +import { Interface } from 'ethers/lib/utils' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +const ARTIFACTS_PATH = path.resolve('build/contracts') +const artifacts = new Artifacts(ARTIFACTS_PATH) +const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi +const l2ReservoirIface = new Interface(l2ReservoirAbi) + +const { AddressZero } = constants +const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] + +const maxGas = toBN('1000000') +const maxSubmissionCost = toBN('7') +const gasPriceBid = toBN('2') +const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) + +describe('L1Reservoir', () => { + let governor: Account + let testAccount1: Account + let mockRouter: Account + let mockL2GRT: Account + let mockL2Gateway: Account + let mockL2Reservoir: Account + let fixture: NetworkFixture + + let grt: GraphToken + let reservoirMock: ReservoirMock + let l1Reservoir: L1Reservoir + let bridgeEscrow: BridgeEscrow + let l1GraphTokenGateway: L1GraphTokenGateway + + let supplyBeforeDrip: BigNumber + let dripBlock: BigNumber + let fixtureContracts: L1FixtureContracts + let arbitrumMocks: ArbitrumL1Mocks + + const ISSUANCE_RATE_PERIODS = toBN(4) // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block + + // Test accumulated rewards after nBlocksToAdvance, + // asking for the value at blockToQuery + const shouldGetNewRewards = async ( + initialSupply: BigNumber, + nBlocksToAdvance: BigNumber = ISSUANCE_RATE_PERIODS, + blockToQuery?: BigNumber, + expectedValue?: BigNumber, + round = true, + ) => { + // -- t0 -- + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK) + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + // Jump + await advanceBlocks(nBlocksToAdvance) + + // -- t1 -- + + // Contract calculation + if (!blockToQuery) { + blockToQuery = await latestBlock() + } + const contractAccrued = await l1Reservoir.getAccumulatedRewards(blockToQuery) + // Local calculation + if (expectedValue == null) { + expectedValue = await tracker.newRewards(blockToQuery) + } + + // Check + if (round) { + expect(toRound(contractAccrued.sub(startAccrued))).eq(toRound(expectedValue)) + } else { + expect(contractAccrued.sub(startAccrued)).eq(expectedValue) + } + + return expectedValue + } + + const sequentialDoubleDrip = async ( + blocksToAdvance: BigNumber, + dripInterval = defaults.rewards.dripInterval, + ) => { + // Initial snapshot defines the first lastRewardsUpdateBlock + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + const supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + let expectedNextDeadline = dripBlock.add(dripInterval) + let expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const tx1 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + expect(await latestBlock()).eq(dripBlock) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + expect(await l1Reservoir.tokenSupplyCache()).to.eq(supplyBeforeDrip) + await expect(tx1) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + await expect(tx1).emit(grt, 'Transfer').withArgs(AddressZero, l1Reservoir.address, actualAmount) + await tracker.snapshotRewards() + + await advanceBlocks(blocksToAdvance) + + const tx2 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const newAmount = (await grt.balanceOf(l1Reservoir.address)).sub(actualAmount) + expectedNextDeadline = (await latestBlock()).add(dripInterval) + const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) + expectedMintedAmount = (await tracker.accRewards(expectedNextDeadline)).sub(actualAmount) + expect(toRound(newAmount)).to.eq(toRound(expectedMintedAmount)) + expect(toRound(await l1Reservoir.tokenSupplyCache())).to.eq(toRound(expectedSnapshottedSupply)) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(newAmount, toBN(0), expectedNextDeadline) + await expect(tx2).emit(grt, 'Transfer').withArgs(AddressZero, l1Reservoir.address, newAmount) + } + + before(async function () { + ;[governor, testAccount1, mockRouter, mockL2GRT, mockL2Gateway, mockL2Reservoir] = + await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.load(governor.signer) + ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway } = fixtureContracts) + + arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + mockL2Reservoir.address, + ) + reservoirMock = (await deployContract( + 'ReservoirMock', + governor.signer, + )) as unknown as ReservoirMock + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('configuration', function () { + describe('initial snapshot', function () { + it('rejects call if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).initialSnapshot(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('snapshots the total GRT supply', async function () { + const tx = l1Reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + const supply = await grt.totalSupply() + await expect(tx) + .emit(l1Reservoir, 'InitialSnapshotTaken') + .withArgs(await latestBlock(), supply, toGRT('0')) + expect(await grt.balanceOf(l1Reservoir.address)).to.eq(toGRT('0')) + expect(await l1Reservoir.tokenSupplyCache()).to.eq(supply) + expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + }) + it('mints pending rewards and includes them in the snapshot', async function () { + const pending = toGRT('10000000') + const tx = l1Reservoir.connect(governor.signer).initialSnapshot(pending) + const supply = await grt.totalSupply() + const expectedSupply = supply.add(pending) + await expect(tx) + .emit(l1Reservoir, 'InitialSnapshotTaken') + .withArgs(await latestBlock(), expectedSupply, pending) + expect(await grt.balanceOf(l1Reservoir.address)).to.eq(pending) + expect(await l1Reservoir.tokenSupplyCache()).to.eq(expectedSupply) + expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + }) + }) + describe('issuance rate update', function () { + it('rejects setting issuance rate if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setIssuanceRate(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting issuance rate to less than minimum allowed', async function () { + const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 + const tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).revertedWith('Issuance rate under minimum allowed') + }) + + it('should set issuance rate to minimum allowed', async function () { + const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 + const tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) + expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) + }) + + it('should set issuance rate to apply on next drip', async function () { + const newIssuanceRate = toGRT('1.00025') + let tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) + expect(await l1Reservoir.issuanceRate()).eq(0) + expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) + tx = l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + await expect(tx).emit(l1Reservoir, 'IssuanceRateUpdated').withArgs(newIssuanceRate) + expect(await l1Reservoir.issuanceRate()).eq(newIssuanceRate) + }) + }) + describe('drip interval update', function () { + it('rejects setting drip interval if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setDripInterval(toBN(40800)) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting drip interval to zero', async function () { + const tx = l1Reservoir.connect(governor.signer).setDripInterval(toBN(0)) + await expect(tx).revertedWith('Drip interval must be > 0') + }) + + it('updates the drip interval', async function () { + const newInterval = toBN(40800) + const tx = l1Reservoir.connect(governor.signer).setDripInterval(newInterval) + await expect(tx).emit(l1Reservoir, 'DripIntervalUpdated').withArgs(newInterval) + expect(await l1Reservoir.dripInterval()).eq(newInterval) + }) + }) + describe('L2 reservoir address update', function () { + it('rejects setting L2 reservoir address if unauthorized', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + .setL2ReservoirAddress(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('updates the L2 reservoir address', async function () { + const tx = l1Reservoir.connect(governor.signer).setL2ReservoirAddress(testAccount1.address) + await expect(tx) + .emit(l1Reservoir, 'L2ReservoirAddressUpdated') + .withArgs(testAccount1.address) + expect(await l1Reservoir.l2ReservoirAddress()).eq(testAccount1.address) + }) + }) + describe('L2 rewards fraction update', function () { + it('rejects setting L2 rewards fraction if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setL2RewardsFraction(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting L2 rewards fraction to more than 1', async function () { + const newValue = toGRT('1').add(1) + const tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).revertedWith('L2 Rewards fraction must be <= 1') + }) + + it('should set L2 rewards fraction to maximum allowed', async function () { + const newValue = toGRT('1') // this gets a bignumber with 1e18 + const tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) + expect(await l1Reservoir.l2RewardsFraction()).eq(0) + expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) + }) + + it('should set L2 rewards fraction to apply on next drip', async function () { + const newValue = toGRT('0.25') + let tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) + expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) + tx = l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionUpdated').withArgs(newValue) + expect(await l1Reservoir.l2RewardsFraction()).eq(newValue) + }) + }) + }) + + // TODO test that rewardsManager.updateAccRewardsPerSignal is called when + // issuanceRate or l2RewardsFraction is updated + describe('drip', function () { + it('mints rewards for the next week', async function () { + // Initial snapshot defines the first lastRewardsUpdateBlock + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const tx = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + expect(await l1Reservoir.tokenSupplyCache()).to.eq(supplyBeforeDrip) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + }) + it('has no effect if called a second time in the same block', async function () { + // Initial snapshot defines the first lastRewardsUpdateBlock + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + await provider().send('evm_setAutomine', [false]) + const tx1 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const tx2 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + await provider().send('evm_mine', []) + await provider().send('evm_setAutomine', [true]) + + const actualAmount = await grt.balanceOf(l1Reservoir.address) + expect(await latestBlock()).eq(dripBlock) // Just in case disabling automine stops working + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + await expect(tx1) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + await expect(tx1) + .emit(grt, 'Transfer') + .withArgs(AddressZero, l1Reservoir.address, actualAmount) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(toBN(0), toBN(0), expectedNextDeadline) + await expect(tx2).not.emit(grt, 'Transfer') + }) + it('prevents locking eth in the contract if l2RewardsFraction is 0', async function () { + const tx = l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + await expect(tx).revertedWith('No eth value needed') + }) + it('mints only a few more tokens if called on the next block', async function () { + await sequentialDoubleDrip(toBN(0)) + }) + it('mints the right amount of tokens if called before the drip period is over', async function () { + const dripInterval = toBN('100') + await l1Reservoir.connect(governor.signer).setDripInterval(dripInterval) + await sequentialDoubleDrip(toBN('50'), dripInterval) + }) + it('mints the right amount of tokens filling the gap if called after the drip period is over', async function () { + const dripInterval = toBN('100') + await l1Reservoir.connect(governor.signer).setDripInterval(dripInterval) + await sequentialDoubleDrip(toBN('150'), dripInterval) + }) + it('sends the specified fraction of the rewards with a callhook to L2', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + const normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('0'), + ]) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + }) + it('sends the outstanding amount if the L2 rewards fraction changes', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('0'), + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.8')) + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction + const expectedNewTotalSentToL2 = rewardsUntilSecondDripBlock + .div(2) + .add(expectedTotalRewards.sub(rewardsUntilSecondDripBlock).mul(8).div(10)) + + const tx2 = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('1'), // Incremented nonce + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx2) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) + it('sends the outstanding amount if the L2 rewards fraction stays constant', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('0'), + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction + const expectedNewTotalSentToL2 = expectedTotalRewards.div(2) + + const tx2 = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('1'), // Incremented nonce + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx2) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) + }) + + context('calculating rewards', async function () { + beforeEach(async function () { + // 5% minute rate (4 blocks) + await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + supplyBeforeDrip = await grt.totalSupply() + await l1Reservoir.drip(toBN(0), toBN(0), toBN(0)) + dripBlock = await latestBlock() + }) + + describe('getAccumulatedRewards', function () { + it('returns rewards accrued after some blocks', async function () { + await shouldGetNewRewards(supplyBeforeDrip) + }) + it('returns zero if evaluated at the block where reservoir had the first drip', async function () { + await shouldGetNewRewards( + supplyBeforeDrip, + ISSUANCE_RATE_PERIODS, + dripBlock, + toBN(0), + false, + ) + }) + it('returns the supply times issuance rate one block after the first drip', async function () { + const expectedVal = supplyBeforeDrip + .mul(ISSUANCE_RATE_PER_BLOCK.sub(toGRT(1))) + .div(toGRT(1)) + await shouldGetNewRewards( + supplyBeforeDrip, + ISSUANCE_RATE_PERIODS, + dripBlock.add(1), + expectedVal, + false, + ) + }) + it('returns the rewards for a block some time in the future', async function () { + await shouldGetNewRewards(supplyBeforeDrip, toBN(1), dripBlock.add(10000)) + }) + }) + describe('getNewRewards', function () { + const computeDelta = function (t1: BigNumber, t0: BigNumber, lambda = toBN(0)): BigNumber { + const deltaT = new BN(t1.toString()).minus(new BN(t0.toString())) + const rate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + const supply = new BN(supplyBeforeDrip.toString()) + return toBN(supply.times(rate.pow(deltaT)).minus(supply).precision(18).toString(10)) + .mul(toGRT('1').sub(lambda)) + .div(toGRT('1')) + } + it('computes the rewards delta between the last drip block and the current block', async function () { + const t0 = dripBlock + const t1 = t0.add(200) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('returns zero rewards if the time delta is zero', async function () { + const t0 = dripBlock + const expectedVal = toBN('0') + expect(await l1Reservoir.getNewRewards(t0)).to.eq(expectedVal) + }) + it('computes the rewards delta between a past drip block and a future block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = t0.add(100) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta between a past drip block and the current block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta considering the L2 rewards fraction', async function () { + const lambda = toGRT('0.32') + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) + await l1Reservoir.drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + supplyBeforeDrip = await l1Reservoir.tokenSupplyCache() // Has been updated accordingly + dripBlock = await latestBlock() + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + + const expectedVal = computeDelta(t1, t0, lambda) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + }) + }) + + describe('pow', function () { + it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { + const baseRatio = toGRT('0.000000004641377923') // 1% annual rate + const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks + const powPrecision = 14 // Compare up to this amount of significant digits + BN.config({ POW_PRECISION: 100 }) + for (let i = 0; i < 50; i = i + 4) { + const r = baseRatio.mul(i * 4).add(toGRT('1')) + const h = await reservoirMock.pow(r, timePeriods, toGRT('1')) + console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) + expect(new BN(h.toString()).precision(powPrecision).toString(10)).to.eq( + new BN(r.toString()) + .div(1e18) + .pow(timePeriods) + .times(1e18) + .precision(powPrecision) + .toString(10), + ) + } + }) + }) +}) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 2c9021d76..1cbeca0e2 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -1,15 +1,12 @@ import { expect } from 'chai' -import { constants, BigNumber } from 'ethers' -import { BigNumber as BN } from 'bignumber.js' +import { constants, BigNumber, ContractReceipt } from 'ethers' -import { deployContract } from '../lib/deployment' import { NetworkFixture } from '../lib/fixtures' import { Curation } from '../../build/types/Curation' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' import { RewardsManager } from '../../build/types/RewardsManager' -import { RewardsManagerMock } from '../../build/types/RewardsManagerMock' import { Staking } from '../../build/types/Staking' import { @@ -24,7 +21,10 @@ import { Account, advanceToNextEpoch, provider, + RewardsTracker, } from '../lib/testHelpers' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { LogDescription } from 'ethers/lib/utils' const MAX_PPM = 1000000 @@ -48,7 +48,10 @@ describe('Rewards', () => { let epochManager: EpochManager let staking: Staking let rewardsManager: RewardsManager - let rewardsManagerMock: RewardsManagerMock + let l1Reservoir: L1Reservoir + + let supplyBeforeDrip: BigNumber + let dripBlock: BigNumber // Derive some channel keys for each indexer used to sign attestations const channelKey1 = deriveChannelKey() @@ -62,64 +65,18 @@ describe('Rewards', () => { const metadata = HashZero - const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 5% rewards - const ISSUANCE_RATE_PER_BLOCK = toBN('1012272234429039270') // % increase every block - - // Core formula that gets accumulated rewards per signal for a period of time - const getRewardsPerSignal = (p: BN, r: BN, t: BN, s: BN): string => { - if (s.eq(0)) { - return '0' - } - return p.times(r.pow(t)).minus(p).div(s).toPrecision(18).toString() - } - - // Tracks the accumulated rewards as totalSignalled or supply changes across snapshots - class RewardsTracker { - totalSupply = BigNumber.from(0) - totalSignalled = BigNumber.from(0) - lastUpdatedBlock = BigNumber.from(0) - accumulated = BigNumber.from(0) - - static async create() { - const tracker = new RewardsTracker() - await tracker.snapshot() - return tracker - } - - async snapshot() { - this.accumulated = this.accumulated.add(await this.accrued()) - this.totalSupply = await grt.totalSupply() - this.totalSignalled = await grt.balanceOf(curation.address) - this.lastUpdatedBlock = await latestBlock() - return this - } - - async elapsedBlocks() { - const currentBlock = await latestBlock() - return currentBlock.sub(this.lastUpdatedBlock) - } - - async accrued() { - const nBlocks = await this.elapsedBlocks() - return this.accruedByElapsed(nBlocks) - } - - async accruedByElapsed(nBlocks: BigNumber | number) { - const n = getRewardsPerSignal( - new BN(this.totalSupply.toString()), - new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18), - new BN(nBlocks.toString()), - new BN(this.totalSignalled.toString()), - ) - return toGRT(n) - } - } + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block // Test accumulated rewards per signal - const shouldGetNewRewardsPerSignal = async (nBlocks = ISSUANCE_RATE_PERIODS) => { + const shouldGetNewRewardsPerSignal = async ( + initialSupply: BigNumber, + nBlocks = ISSUANCE_RATE_PERIODS, + dripBlock?: BigNumber, + ) => { // -- t0 -- - const tracker = await RewardsTracker.create() - + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK, dripBlock) + tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) // Jump await advanceBlocks(nBlocks) @@ -128,28 +85,36 @@ describe('Rewards', () => { // Contract calculation const contractAccrued = await rewardsManager.getNewRewardsPerSignal() // Local calculation - const expectedAccrued = await tracker.accrued() + const expectedAccrued = await tracker.newRewardsPerSignal(await grt.balanceOf(curation.address)) // Check - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) return expectedAccrued } + const findRewardsManagerEvents = (receipt: ContractReceipt): Array => { + return receipt.logs + .map((l) => { + try { + return rewardsManager.interface.parseLog(l) + } catch { + return null + } + }) + .filter((l) => !!l) + } + before(async function () { ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle] = await getAccounts() fixture = new NetworkFixture() - ;({ grt, curation, epochManager, staking, rewardsManager } = await fixture.load( + ;({ grt, curation, epochManager, staking, rewardsManager, l1Reservoir } = await fixture.load( governor.signer, )) - rewardsManagerMock = (await deployContract( - 'RewardsManagerMock', - governor.signer, - )) as unknown as RewardsManagerMock - // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + // await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + // await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { @@ -157,6 +122,8 @@ describe('Rewards', () => { await grt.connect(wallet.signer).approve(staking.address, toGRT('1000000')) await grt.connect(wallet.signer).approve(curation.address, toGRT('1000000')) } + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() }) beforeEach(async function () { @@ -168,32 +135,6 @@ describe('Rewards', () => { }) describe('configuration', function () { - describe('issuance rate update', function () { - it('reject set issuance rate if unauthorized', async function () { - const tx = rewardsManager.connect(indexer1.signer).setIssuanceRate(toGRT('1.025')) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - - it('reject set issuance rate to less than minimum allowed', async function () { - const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 - const tx = rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - await expect(tx).revertedWith('Issuance rate under minimum allowed') - }) - - it('should set issuance rate to minimum allowed', async function () { - const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) - }) - - it('should set issuance rate', async function () { - const newIssuanceRate = toGRT('1.025') - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) - expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await latestBlock()) - }) - }) - describe('subgraph availability service', function () { it('reject set subgraph oracle if unauthorized', async function () { const tx = rewardsManager @@ -243,9 +184,77 @@ describe('Rewards', () => { }) context('issuing rewards', async function () { + interface DelegationParameters { + indexingRewardCut: BigNumber + queryFeeCut: BigNumber + cooldownBlocks: number + } + + async function setupIndexerAllocation() { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + async function setupIndexerAllocationWithDelegation( + tokensToDelegate: BigNumber, + delegationParams: DelegationParameters, + ) { + const tokensToAllocate = toGRT('12500') + + // Transfer some funds from the curator, I don't want to mint new tokens + await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate) + await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate) + + // Stake and set delegation parameters + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .setDelegationParameters( + delegationParams.indexingRewardCut, + delegationParams.queryFeeCut, + delegationParams.cooldownBlocks, + ) + + // Delegate + await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + beforeEach(async function () { // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + dripBlock = await latestBlock() }) describe('getNewRewardsPerSignal', function () { @@ -262,7 +271,7 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, tokensToSignal, 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) }) it('accrued per signal when signalled tokens w/ many subgraphs', async function () { @@ -270,78 +279,112 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) // Update total signalled await curation.connect(curator2.signer).mint(subgraphDeploymentID2, toGRT('250'), 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) }) }) describe('updateAccRewardsPerSignal', function () { it('update the accumulated rewards per signal state', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) - // Snapshot - const tracker = await RewardsTracker.create() + // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, + // so we snapshot using the previous value + tracker.snapshotPerSignal(prevSignal) // Update await rewardsManager.updateAccRewardsPerSignal() + tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + const contractAccrued = await rewardsManager.accRewardsPerSignal() // Check - const expectedAccrued = await tracker.accrued() - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + const blockNum = await latestBlock() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + blockNum, + ) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) }) it('update the accumulated rewards per signal state after many blocks', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) - // Snapshot - const tracker = await RewardsTracker.create() + // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, + // so we snapshot using the previous value + tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.updateAccRewardsPerSignal() + tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) const contractAccrued = await rewardsManager.accRewardsPerSignal() - // Check - const expectedAccrued = await tracker.accrued() - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + const blockNum = await latestBlock() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + blockNum.add(0), + ) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) }) }) describe('getAccRewardsForSubgraph', function () { it('accrued for each subgraph', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + let prevSignal = await grt.balanceOf(curation.address) // Curator1 - Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - const tracker1 = await RewardsTracker.create() + const sg1Snapshot = await tracker.snapshotPerSignal(prevSignal) // Curator2 - Update total signalled const signalled2 = toGRT('500') + prevSignal = await grt.balanceOf(curation.address) await curation.connect(curator2.signer).mint(subgraphDeploymentID2, signalled2, 0) - - // Snapshot - const tracker2 = await RewardsTracker.create() - await tracker1.snapshot() + const sg2Snapshot = await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) - // Snapshot - await tracker1.snapshot() - await tracker2.snapshot() - // Calculate rewards - const rewardsPerSignal1 = await tracker1.accumulated - const rewardsPerSignal2 = await tracker2.accumulated - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) - const expectedRewardsSG2 = rewardsPerSignal2.mul(signalled2).div(WeiPerEther) + const rewardsPerSignal = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + ) + const expectedRewardsSG1 = rewardsPerSignal + .sub(sg1Snapshot) + .mul(signalled1) + .div(WeiPerEther) + const expectedRewardsSG2 = rewardsPerSignal + .sub(sg2Snapshot) + .mul(signalled2) + .div(WeiPerEther) // Get rewards from contract const contractRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph( @@ -359,27 +402,35 @@ describe('Rewards', () => { describe('onSubgraphSignalUpdate', function () { it('update the accumulated rewards for subgraph state', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) // Snapshot - const tracker1 = await RewardsTracker.create() + await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.onSubgraphSignalUpdate(subgraphDeploymentID1) - + const snapshot = await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) // Check const contractRewardsSG1 = (await rewardsManager.subgraphs(subgraphDeploymentID1)) .accRewardsForSubgraph - const rewardsPerSignal1 = await tracker1.accrued() - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) + const expectedRewardsSG1 = snapshot.mul(signalled1).div(WeiPerEther) expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1)) const contractAccrued = await rewardsManager.accRewardsPerSignal() - const expectedAccrued = await tracker1.accrued() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + ) expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) const contractBlockUpdated = await rewardsManager.accRewardsPerSignalLastBlockUpdated() @@ -430,11 +481,14 @@ describe('Rewards', () => { it('update the accumulated rewards for allocated tokens state', async function () { // Update total signalled const signalled1 = toGRT('1500') + // block = dripBlock await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // block = dripBlock + 1 // Allocate const tokensToAllocate = toGRT('12500') await staking.connect(indexer1.signer).stake(tokensToAllocate) + // block = dripBlock + 2 await staking .connect(indexer1.signer) .allocateFrom( @@ -445,19 +499,28 @@ describe('Rewards', () => { metadata, await channelKey1.generateProof(indexer1.address), ) - + // block = dripBlock + 3 // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) - - // Prepare expected results - // NOTE: calculated the expected result manually as the above code has 1 off block difference - // replace with a RewardsManagerMock - const expectedSubgraphRewards = toGRT('891695470') - const expectedRewardsAT = toGRT('51571') + // block = dripBlock + 7 // Update await rewardsManager.onSubgraphAllocationUpdate(subgraphDeploymentID1) + // block = dripBlock + 8 + // Prepare expected results + // Expected total rewards: + // DeltaR_end = supplyBeforeDrip * r ^ 8 - supplyBeforeDrip + // DeltaR_end = 10004000000 GRT * (1000122722344290393 / 1e18)^8 - 10004000000 GRT = 9825934.397 + // The signal was minted at dripBlock + 1, so: + // DeltaR_start = supplyBeforeDrip * r ^ 1 - supplyBeforeDrip = 1227714.332 + + // And they all go to this subgraph, so subgraph rewards = DeltaR_end - DeltaR_start = 8598220.065 + const expectedSubgraphRewards = toGRT('8598220') + + // The allocation happened at dripBlock + 3, so rewards per allocated token are: + // ((supplyBeforeDrip * r ^ 8 - supplyBeforeDrip) - (supplyBeforeDrip * r ^ 3 - supplyBeforeDrip)) / 12500 = 491.387 + const expectedRewardsAT = toGRT('491') // Check on demand results saved const subgraph = await rewardsManager.subgraphs(subgraphDeploymentID1) const contractSubgraphRewards = await rewardsManager.getAccRewardsForSubgraph( @@ -507,87 +570,85 @@ describe('Rewards', () => { }) }) - describe('takeRewards', function () { - interface DelegationParameters { - indexingRewardCut: BigNumber - queryFeeCut: BigNumber - cooldownBlocks: number - } - - async function setupIndexerAllocation() { - // Setup + describe('takeAndBurnRewards', function () { + it('should burn rewards on closed allocation with POI zero', async function () { + // Align with the epoch boundary + // dripBlock (81) await epochManager.setEpochLength(10) + // dripBlock + 1 + await advanceToNextEpoch(epochManager) + // dripBlock + 4 + // Setup + await setupIndexerAllocation() + // dripBlock + 7 + // Jump + await advanceToNextEpoch(epochManager) + // dripBlock + 14 - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - } - - async function setupIndexerAllocationWithDelegation( - tokensToDelegate: BigNumber, - delegationParams: DelegationParameters, - ) { - const tokensToAllocate = toGRT('12500') + // Before state + const beforeTokenSupply = await grt.totalSupply() + const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) + const beforeStakingBalance = await grt.balanceOf(staking.address) - // Setup - await epochManager.setEpochLength(10) + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens + // The first snapshot is after allocating, that is 7 blocks after dripBlock: + // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 + // The final snapshot is when we close the allocation, that happens 8 blocks later: + // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 + // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. + const expectedIndexingRewards = toGRT('9834378') - // Transfer some funds from the curator, I don't want to mint new tokens - await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate) - await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate) + // Close allocation with POI zero, which should burn the rewards + const tx = await staking.connect(indexer1.signer).closeAllocation(allocationID1, HashZero) + const receipt = await tx.wait() - // Stake and set delegation parameters - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .setDelegationParameters( - delegationParams.indexingRewardCut, - delegationParams.queryFeeCut, - delegationParams.cooldownBlocks, - ) + const log = findRewardsManagerEvents(receipt)[0] + const event = log.args + expect(log.name).eq('RewardsBurned') + expect(event.indexer).eq(indexer1.address) + expect(event.allocationID).eq(allocationID1) + expect(event.epoch).eq(await epochManager.currentEpoch()) + expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) - // Delegate - await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate) + // After state + const afterTokenSupply = await grt.totalSupply() + const afterIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const afterIndexer1Balance = await grt.balanceOf(indexer1.address) + const afterStakingBalance = await grt.balanceOf(staking.address) - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Check that rewards are NOT put into indexer stake + const expectedIndexerStake = beforeIndexer1Stake - // Allocate - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - } + // Check stake should NOT have increased with the rewards staked + expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) + // Check indexer balance remains the same + expect(afterIndexer1Balance).eq(beforeIndexer1Balance) + // Check indexing rewards are kept in the staking contract + expect(toRound(afterStakingBalance)).eq(toRound(beforeStakingBalance)) + // Check that tokens have been burned + // We divide by 10 to accept numeric errors up to 10 GRT + expect(toRound(afterTokenSupply.div(10))).eq( + toRound(beforeTokenSupply.sub(expectedIndexingRewards).div(10)), + ) + }) + }) + describe('takeRewards', function () { it('should distribute rewards on closed allocation and stake', async function () { // Align with the epoch boundary + // dripBlock (81) + await epochManager.setEpochLength(10) + // dripBlock + 1 await advanceToNextEpoch(epochManager) + // dripBlock + 4 // Setup await setupIndexerAllocation() - + // dripBlock + 7 // Jump await advanceToNextEpoch(epochManager) + // dripBlock + 14 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -597,19 +658,20 @@ describe('Rewards', () => { // All the rewards in this subgraph go to this allocation. // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 + // The first snapshot is after allocating, that is 7 blocks after dripBlock: + // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 + // The final snapshot is when we close the allocation, that happens 8 blocks later: + // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + const expectedIndexingRewards = toGRT('9834378') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() - const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + + const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) @@ -623,7 +685,7 @@ describe('Rewards', () => { // Check that rewards are put into indexer stake const expectedIndexerStake = beforeIndexer1Stake.add(expectedIndexingRewards) - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + // Check stake should have increased with the rewards staked expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) // Check indexer balance remains the same @@ -632,14 +694,15 @@ describe('Rewards', () => { expect(toRound(afterStakingBalance)).eq( toRound(beforeStakingBalance.add(expectedIndexingRewards)), ) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) it('should distribute rewards on closed allocation and send to destination', async function () { const destinationAddress = randomHexBytes(20) await staking.connect(indexer1.signer).setRewardsDestination(destinationAddress) + await epochManager.setEpochLength(10) // Align with the epoch boundary await advanceToNextEpoch(epochManager) // Setup @@ -656,19 +719,19 @@ describe('Rewards', () => { // All the rewards in this subgraph go to this allocation. // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 + // The first snapshot is after allocating, that is 7 blocks after dripBlock: + // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 + // The final snapshot is when we close the allocation, that happens 8 blocks later: + // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + const expectedIndexingRewards = toGRT('9834378') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() - const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) @@ -682,7 +745,7 @@ describe('Rewards', () => { // Check that rewards are properly assigned const expectedIndexerStake = beforeIndexer1Stake - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + // Check stake should not have changed expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) // Check indexing rewards are received by the rewards destination @@ -691,8 +754,8 @@ describe('Rewards', () => { ) // Check indexing rewards were not sent to the staking contract expect(afterStakingBalance).eq(beforeStakingBalance) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) it('should distribute rewards on closed allocation w/delegators', async function () { @@ -703,14 +766,18 @@ describe('Rewards', () => { cooldownBlocks: 5, } const tokensToDelegate = toGRT('2000') - + // dripBlock (81) + await epochManager.setEpochLength(10) + // dripBlock + 1 // Align with the epoch boundary await advanceToNextEpoch(epochManager) + // dripBlock + 4 // Setup the allocation and delegators await setupIndexerAllocationWithDelegation(tokensToDelegate, delegationParams) - + // dripBlock + 11 // Jump await advanceToNextEpoch(epochManager) + // dripBlock + 14 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -730,12 +797,12 @@ describe('Rewards', () => { // All the rewards in this subgraph go to this allocation. // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 14500 = 8466.995 + // The first snapshot is after allocating, that is 11 blocks after dripBlock: + // startRewardsPerToken = (10004000000 * 1.01227 ^ 11 - 10004000000) / 14500 = 931.94 // The final snapshot is when we close the allocation, that happens 4 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 4 - 10004000000) / 14500 = 34496.55 + // endRewardsPerToken = (10004000000 * 1.01227 ^ 15 - 10004000000) / 14500 = 1271.14 // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. - const expectedIndexingRewards = toGRT('377428566.77') + const expectedIndexingRewards = toGRT('4918396') // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards) @@ -745,101 +812,151 @@ describe('Rewards', () => { // Check const expectedIndexerStake = beforeIndexer1Stake.add(indexerRewards) const expectedDelegatorsPoolTokens = beforeDelegationPool.tokens.add(delegatorsRewards) - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) expect(toRound(afterDelegationPool.tokens)).eq(toRound(expectedDelegatorsPoolTokens)) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) - it('should deny rewards if subgraph on denylist', async function () { + it('should deny and burn rewards if subgraph on denylist', async function () { // Setup + await epochManager.setEpochLength(10) await rewardsManager .connect(governor.signer) .setSubgraphAvailabilityOracle(governor.address) await rewardsManager.connect(governor.signer).setDenied(subgraphDeploymentID1, true) + await advanceToNextEpoch(epochManager) await setupIndexerAllocation() // Jump await advanceToNextEpoch(epochManager) + // This is the same amount as in the test above + // (see 'should distribute rewards on closed allocation and stake') + const expectedIndexingRewards = toGRT('9834378') + const supplyBefore = await grt.totalSupply() // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'RewardsDenied') - .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch()) + await expect(tx).emit(rewardsManager, 'RewardsDenied') + const receipt = await (await tx).wait() + const logs = findRewardsManagerEvents(receipt) + expect(logs.length).to.eq(1) + expect(logs[0].name).to.eq('RewardsDenied') + const ev = logs[0].args + expect(ev.indexer).to.eq(indexer1.address) + expect(ev.allocationID).to.eq(allocationID1) + expect(ev.epoch).to.eq(await epochManager.currentEpoch()) + expect(toRound(ev.amount)).to.eq(toRound(expectedIndexingRewards)) + // Check that the rewards were burned + // We divide by 10 to accept numeric errors up to 10 GRT + expect(toRound((await grt.totalSupply()).div(10))).to.eq( + toRound(supplyBefore.sub(expectedIndexingRewards).div(10)), + ) }) }) - }) - describe('pow', function () { - it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { - const baseRatio = toGRT('0.000000004641377923') // 1% annual rate - const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks - for (let i = 0; i < 50; i = i + 4) { - const r = baseRatio.mul(i * 4).add(toGRT('1')) - const h = await rewardsManagerMock.pow(r, timePeriods, toGRT('1')) - console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) - } - }) - }) + describe('edge scenarios', function () { + it('close allocation on a subgraph that no longer have signal', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - describe('edge scenarios', function () { - it('close allocation on a subgraph that no longer have signal', async function () { - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, + // Jump + await advanceToNextEpoch(epochManager) + + // Remove all signal from the subgraph + const curatorShares = await curation.getCuratorSignal( + curator1.address, subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), ) + await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) - // Jump - await advanceToNextEpoch(epochManager) + // Close allocation. At this point rewards should be collected for that indexer + await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + }) + }) - // Remove all signal from the subgraph - const curatorShares = await curation.getCuratorSignal(curator1.address, subgraphDeploymentID1) - await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) + describe('multiple allocations', function () { + it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { + // If rewards are not monotonically increasing, this can trigger + // a subtraction overflow error as seen in mainnet tx: + // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab + await advanceToNextEpoch(epochManager) - // Close allocation. At this point rewards should be collected for that indexer - await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - }) - }) + // Setup + await epochManager.setEpochLength(10) - describe('multiple allocations', function () { - it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { - // If rewards are not monotonically increasing, this can trigger - // a subtraction overflow error as seen in mainnet tx: - // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab - await advanceToNextEpoch(epochManager) + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - // Setup - await epochManager.setEpochLength(10) + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Allocate simultaneously, burning in the middle + const tokensToAlloc = toGRT('5000') + await provider().send('evm_setAutomine', [false]) + const tx1 = await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) + const tx3 = await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) - // Stake - const tokensToStake = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToStake) + await provider().send('evm_mine', []) + await provider().send('evm_setAutomine', [true]) - // Allocate simultaneously, burning in the middle - const tokensToAlloc = toGRT('5000') - await provider().send('evm_setAutomine', [false]) - const tx1 = await staking - .connect(indexer1.signer) - .allocateFrom( + await expect(tx1).emit(staking, 'AllocationCreated') + await expect(tx2).emit(grt, 'Transfer') + await expect(tx3).emit(staking, 'AllocationCreated') + }) + it('two simultanous-similar allocations should get same amount of rewards', async function () { + await advanceToNextEpoch(epochManager) + + // Setup + await epochManager.setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) + + // Allocate simultaneously + const tokensToAlloc = toGRT('5000') + const tx1 = await staking.populateTransaction.allocateFrom( indexer1.address, subgraphDeploymentID1, tokensToAlloc, @@ -847,10 +964,7 @@ describe('Rewards', () => { metadata, await channelKey1.generateProof(indexer1.address), ) - const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) - const tx3 = await staking - .connect(indexer1.signer) - .allocateFrom( + const tx2 = await staking.populateTransaction.allocateFrom( indexer1.address, subgraphDeploymentID1, tokensToAlloc, @@ -858,61 +972,31 @@ describe('Rewards', () => { metadata, await channelKey2.generateProof(indexer1.address), ) + await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) - await provider().send('evm_mine', []) - await provider().send('evm_setAutomine', [true]) - - await expect(tx1).emit(staking, 'AllocationCreated') - await expect(tx2).emit(grt, 'Transfer') - await expect(tx3).emit(staking, 'AllocationCreated') - }) - it('two simultanous-similar allocations should get same amount of rewards', async function () { - await advanceToNextEpoch(epochManager) - - // Setup - await epochManager.setEpochLength(10) - - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Jump + await advanceToNextEpoch(epochManager) - // Stake - const tokensToStake = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToStake) - - // Allocate simultaneously - const tokensToAlloc = toGRT('5000') - const tx1 = await staking.populateTransaction.allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAlloc, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - const tx2 = await staking.populateTransaction.allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAlloc, - allocationID2, - metadata, - await channelKey2.generateProof(indexer1.address), - ) - await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) - - // Jump - await advanceToNextEpoch(epochManager) - - // Close allocations simultaneously - const tx3 = await staking.populateTransaction.closeAllocation(allocationID1, randomHexBytes()) - const tx4 = await staking.populateTransaction.closeAllocation(allocationID2, randomHexBytes()) - const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) - - // Both allocations should receive the same amount of rewards - const receipt = await tx5.wait() - const event1 = rewardsManager.interface.parseLog(receipt.logs[1]).args - const event2 = rewardsManager.interface.parseLog(receipt.logs[5]).args - expect(event1.amount).eq(event2.amount) + // Close allocations simultaneously + const tx3 = await staking.populateTransaction.closeAllocation( + allocationID1, + randomHexBytes(), + ) + const tx4 = await staking.populateTransaction.closeAllocation( + allocationID2, + randomHexBytes(), + ) + const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) + + // Both allocations should receive the same amount of rewards + const receipt = await tx5.wait() + const rewardsMgrEvents = findRewardsManagerEvents(receipt) + expect(rewardsMgrEvents.length).to.eq(2) + const event1 = rewardsMgrEvents[0].args + const event2 = rewardsMgrEvents[1].args + expect(event1.amount).to.not.eq(toBN(0)) + expect(event1.amount).to.eq(event2.amount) + }) }) }) }) From 57dbbb83a13366f11296c5331b3538c083adece6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 11 Jul 2022 21:04:51 +0200 Subject: [PATCH 10/78] fix: document potential drip reverts if issuance rate is updated [L-01] --- contracts/reservoir/L1Reservoir.sol | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 3b020648d..b1058c8f3 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -125,6 +125,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. * The call value must be equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. * @param l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 @@ -151,9 +154,15 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // n = deltaR(t1, t0) uint256 newRewardsToDistribute = getNewGlobalRewards(rewardsMintedUntilBlock); // N = n - eps - uint256 tokensToMint = newRewardsToDistribute.add(mintedRewardsActual).sub( - mintedRewardsTotal - ); + uint256 tokensToMint; + { + uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual); + require( + newRewardsPlusMintedActual >= mintedRewardsTotal, + "Would mint negative tokens, wait before calling again" + ); + tokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); + } if (tokensToMint > 0) { graphToken().mint(address(this), tokensToMint); From b5967ac866aef2b0bf4094b5db76de0ed1d03446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 11 Jul 2022 21:14:25 +0200 Subject: [PATCH 11/78] fix: document drip revert when l2RewardsFraction changed [L-02] --- contracts/reservoir/L1Reservoir.sol | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index b1058c8f3..a4c9a5937 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -127,7 +127,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * only be nonzero if l2RewardsFraction is nonzero. * Calling this function can revert if the issuance rate has recently been reduced, and the existing * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait - * until the drip amount becomes positive before calling the function again. + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. * @param l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 @@ -177,12 +179,17 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // eps > 0, i.e. t < t1_old // Note this can fail if the old l2RewardsFraction is larger // than the new, in which case we just have to wait until enough time has passed - // so that eps is small enough. - tokensToSendToL2 = tokensToSendToL2.sub( - l2RewardsFraction.mul(mintedRewardsTotal.sub(mintedRewardsActual)).div( - TOKEN_DECIMALS - ) + // so that eps is small enough. This also applies to the case where the new + // l2RewardsFraction is zero, since we still need to send one last message + // with the new values to the L2Reservoir. + uint256 l2OffsetAmount = l2RewardsFraction + .mul(mintedRewardsTotal.sub(mintedRewardsActual)) + .div(TOKEN_DECIMALS); + require( + tokensToSendToL2 > l2OffsetAmount, + "Negative amount would be sent to L2, wait before calling again" ); + tokensToSendToL2 = tokensToSendToL2.sub(l2OffsetAmount); } else { tokensToSendToL2 = tokensToSendToL2.add( l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( From d5fa0a8bbd28d06abd2b3f48bf860d73ff17a7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 11 Jul 2022 21:40:36 +0200 Subject: [PATCH 12/78] fix: rename variables related to supply to issuanceBase to make it clear they're decoupled [L-03] --- contracts/l2/reservoir/L2Reservoir.sol | 18 +++++------ contracts/l2/reservoir/L2ReservoirStorage.sol | 2 -- contracts/reservoir/IReservoir.sol | 6 ++-- contracts/reservoir/L1Reservoir.sol | 18 +++++------ contracts/reservoir/L1ReservoirStorage.sol | 2 -- contracts/reservoir/ReservoirStorage.sol | 2 ++ test/l2/l2Reservoir.test.ts | 22 +++++-------- test/reservoir/l1Reservoir.test.ts | 32 +++++++++---------- 8 files changed, 47 insertions(+), 55 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 6bdd9ca67..8948f9ee1 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -18,7 +18,7 @@ import "./L2ReservoirStorage.sol"; contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { using SafeMath for uint256; - event DripReceived(uint256 _normalizedTokenSupply); + event DripReceived(uint256 _issuanceBase); event NextDripNonceUpdated(uint256 _nonce); /** @@ -51,7 +51,7 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { /** * @dev Get new total rewards accumulated since the last drip. * This is deltaR = p * r ^ (blocknum - t0) - p, where: - * - p is the normalized token supply snapshot at t0 + * - p is the issuance base at t0 (normalized by the L2 rewards fraction) * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate * @param blocknum Block number at which to calculate rewards @@ -67,25 +67,25 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { return 0; } - deltaRewards = normalizedTokenSupplyCache + deltaRewards = issuanceBase .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) .div(TOKEN_DECIMALS) - .sub(normalizedTokenSupplyCache); + .sub(issuanceBase); } /** * @dev Receive dripped tokens from L1. * This function can only be called by the gateway, as it is * meant to be a callhook when receiving tokens from L1. It - * updates the normalizedTokenSupplyCache and issuanceRate, + * updates the issuanceBase and issuanceRate, * and snapshots the accumulated rewards. If issuanceRate changes, * it also triggers a snapshot of rewards per signal on the RewardsManager. - * @param _normalizedTokenSupply Snapshot of total GRT supply multiplied by L2 rewards fraction + * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order */ function receiveDrip( - uint256 _normalizedTokenSupply, + uint256 _issuanceBase, uint256 _issuanceRate, uint256 _nonce ) external override onlyL2Gateway { @@ -99,8 +99,8 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { } else { snapshotAccumulatedRewards(); } - normalizedTokenSupplyCache = _normalizedTokenSupply; - emit DripReceived(normalizedTokenSupplyCache); + issuanceBase = _issuanceBase; + emit DripReceived(issuanceBase); } /** diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol index c6ad73e1c..ee7880343 100644 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -6,8 +6,6 @@ pragma solidity ^0.7.6; * @dev Storage variables for the L2Reservoir */ contract L2ReservoirV1Storage { - // Snapshot of total GRT supply multiplied by L2 rewards fraction, received from L1 - uint256 public normalizedTokenSupplyCache; // Expected nonce value for the next drip hook uint256 public nextDripNonce; } diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index dfc64f14f..996f3fc2f 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -43,15 +43,15 @@ interface IL2Reservoir is IReservoir { * @dev Receive dripped tokens from L1. * This function can only be called by the gateway, as it is * meant to be a callhook when receiving tokens from L1. It - * updates the normalizedTokenSupplyCache and issuanceRate, + * updates the issuanceBase and issuanceRate, * and snapshots the accumulated rewards. If issuanceRate changes, * it also triggers a snapshot of rewards per signal on the RewardsManager. - * @param _normalizedTokenSupply Snapshot of total GRT supply multiplied by L2 rewards fraction + * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order */ function receiveDrip( - uint256 _normalizedTokenSupply, + uint256 _issuanceBase, uint256 _issuanceRate, uint256 _nonce ) external; diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index a4c9a5937..33d4c965f 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -23,7 +23,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // Emitted when the initial supply snapshot is taken after contract deployment event InitialSnapshotTaken( uint256 _blockNumber, - uint256 _tokenSupplyCache, + uint256 _issuanceBase, uint256 _mintedPendingRewards ); // Emitted when an issuance rate update is staged, to be applied on the next drip @@ -102,7 +102,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { /** * @dev Computes the initial snapshot for token supply and mints any pending rewards - * This will initialize the tokenSupplyCache to the current GRT supply, after which + * This will initialize the issuanceBase to the current GRT supply, after which * we will keep an internal accounting only using newly minted rewards. This function * will also mint any pending rewards to cover up to the current block for open allocations, * to be computed off-chain. @@ -112,8 +112,8 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { lastRewardsUpdateBlock = block.number; IGraphToken grt = graphToken(); grt.mint(address(this), pendingRewards); - tokenSupplyCache = grt.totalSupply(); - emit InitialSnapshotTaken(block.number, tokenSupplyCache, pendingRewards); + issuanceBase = grt.totalSupply(); + emit InitialSnapshotTaken(block.number, issuanceBase, pendingRewards); } /** @@ -224,11 +224,11 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { /** * @dev Snapshot accumulated rewards on this layer * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. - * We also update the tokenSupplyCache by adding the new total rewards on both layers. + * We also update the issuanceBase by adding the new total rewards on both layers. * @param globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block */ function snapshotAccumulatedRewards(uint256 globalDelta) internal { - tokenSupplyCache = tokenSupplyCache + globalDelta; + issuanceBase = issuanceBase + globalDelta; // Reimplementation of getAccumulatedRewards but reusing the globalDelta calculated above, // to save gas accumulatedLayerRewards = @@ -252,7 +252,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { uint256 gasPriceBid, uint256 maxSubmissionCost ) internal { - uint256 normalizedSupply = l2RewardsFraction.mul(tokenSupplyCache).div(TOKEN_DECIMALS); + uint256 normalizedSupply = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); bytes memory extraData = abi.encodeWithSelector( IL2Reservoir.receiveDrip.selector, normalizedSupply, @@ -288,10 +288,10 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { return 0; } - deltaRewards = tokenSupplyCache + deltaRewards = issuanceBase .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) .div(TOKEN_DECIMALS) - .sub(tokenSupplyCache); + .sub(issuanceBase); } /** diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol index f8254d59f..90821c809 100644 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -14,8 +14,6 @@ contract L1ReservoirV1Storage { address public l2ReservoirAddress; // Block until the minted supplies should last before another drip is needed uint256 public rewardsMintedUntilBlock; - // Snapshot of initial token supply plus accumulated global rewards - uint256 public tokenSupplyCache; // New issuance rate to be applied on the next drip uint256 public nextIssuanceRate; // Interval for rewards drip, in blocks diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol index 18f1cf9d4..8e3e2591d 100644 --- a/contracts/reservoir/ReservoirStorage.sol +++ b/contracts/reservoir/ReservoirStorage.sol @@ -15,4 +15,6 @@ contract ReservoirV1Storage is Managed { uint256 public accumulatedLayerRewards; // Last block at which rewards when updated, i.e. block at which the last drip happened or was received uint256 public lastRewardsUpdateBlock; + // Base value for token issuance, set initially to GRT supply and afterwards using accumulated rewards to update + uint256 public issuanceBase; } diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index 0dd54f23c..ebfe3f52e 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -172,7 +172,7 @@ describe('L2Reservoir', () => { ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) @@ -195,7 +195,7 @@ describe('L2Reservoir', () => { ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) }) @@ -208,7 +208,7 @@ describe('L2Reservoir', () => { ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) @@ -219,9 +219,7 @@ describe('L2Reservoir', () => { ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( - dripNormalizedSupply.add(1), - ) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate.add(1)) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) @@ -235,7 +233,7 @@ describe('L2Reservoir', () => { ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) @@ -246,9 +244,7 @@ describe('L2Reservoir', () => { ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( - dripNormalizedSupply.add(1), - ) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) @@ -262,7 +258,7 @@ describe('L2Reservoir', () => { ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) @@ -274,9 +270,7 @@ describe('L2Reservoir', () => { ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( - dripNormalizedSupply.add(1), - ) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 7a4920159..07a55aa61 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -121,7 +121,7 @@ describe('L1Reservoir', () => { const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(await latestBlock()).eq(dripBlock) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) - expect(await l1Reservoir.tokenSupplyCache()).to.eq(supplyBeforeDrip) + expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) await expect(tx1) .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount, toBN(0), expectedNextDeadline) @@ -136,7 +136,7 @@ describe('L1Reservoir', () => { const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) expectedMintedAmount = (await tracker.accRewards(expectedNextDeadline)).sub(actualAmount) expect(toRound(newAmount)).to.eq(toRound(expectedMintedAmount)) - expect(toRound(await l1Reservoir.tokenSupplyCache())).to.eq(toRound(expectedSnapshottedSupply)) + expect(toRound(await l1Reservoir.issuanceBase())).to.eq(toRound(expectedSnapshottedSupply)) await expect(tx2) .emit(l1Reservoir, 'RewardsDripped') .withArgs(newAmount, toBN(0), expectedNextDeadline) @@ -189,7 +189,7 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'InitialSnapshotTaken') .withArgs(await latestBlock(), supply, toGRT('0')) expect(await grt.balanceOf(l1Reservoir.address)).to.eq(toGRT('0')) - expect(await l1Reservoir.tokenSupplyCache()).to.eq(supply) + expect(await l1Reservoir.issuanceBase()).to.eq(supply) expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) }) it('mints pending rewards and includes them in the snapshot', async function () { @@ -201,7 +201,7 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'InitialSnapshotTaken') .withArgs(await latestBlock(), expectedSupply, pending) expect(await grt.balanceOf(l1Reservoir.address)).to.eq(pending) - expect(await l1Reservoir.tokenSupplyCache()).to.eq(expectedSupply) + expect(await l1Reservoir.issuanceBase()).to.eq(expectedSupply) expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) }) }) @@ -324,7 +324,7 @@ describe('L1Reservoir', () => { const tx = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) - expect(await l1Reservoir.tokenSupplyCache()).to.eq(supplyBeforeDrip) + expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) await expect(tx) .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount, toBN(0), expectedNextDeadline) @@ -413,12 +413,12 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - const normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + const l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) const issuanceRate = await l1Reservoir.issuanceRate() const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('0'), ]) @@ -463,12 +463,12 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - let normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) const issuanceRate = await l1Reservoir.issuanceRate() let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('0'), ]) @@ -510,11 +510,11 @@ describe('L1Reservoir', () => { toRound(expectedNewMintedAmount), ) expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) - normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('1'), // Incremented nonce ]) @@ -566,12 +566,12 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - let normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) const issuanceRate = await l1Reservoir.issuanceRate() let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('0'), ]) @@ -610,11 +610,11 @@ describe('L1Reservoir', () => { toRound(expectedNewMintedAmount), ) expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) - normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('1'), // Incremented nonce ]) @@ -714,7 +714,7 @@ describe('L1Reservoir', () => { const lambda = toGRT('0.32') await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) await l1Reservoir.drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) - supplyBeforeDrip = await l1Reservoir.tokenSupplyCache() // Has been updated accordingly + supplyBeforeDrip = await l1Reservoir.issuanceBase() // Has been updated accordingly dripBlock = await latestBlock() await advanceBlocks(20) const t0 = dripBlock From 494df531d05b4996c0dac70be236b6d22dedbc79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 11 Jul 2022 21:47:05 +0200 Subject: [PATCH 13/78] fix: use issuanceBase check to prevent calling initialSnapshot twice [L-04] --- contracts/reservoir/L1Reservoir.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 33d4c965f..1990d4abf 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -105,10 +105,11 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * This will initialize the issuanceBase to the current GRT supply, after which * we will keep an internal accounting only using newly minted rewards. This function * will also mint any pending rewards to cover up to the current block for open allocations, - * to be computed off-chain. + * to be computed off-chain. Can only be called once as it checks that the issuanceBase is zero. * @param pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function */ function initialSnapshot(uint256 pendingRewards) external onlyGovernor { + require(issuanceBase == 0, "Cannot call this function more than once"); lastRewardsUpdateBlock = block.number; IGraphToken grt = graphToken(); grt.mint(address(this), pendingRewards); From a15bc1d583eb6ae54e30ad084721ea2937db4d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 12 Jul 2022 14:14:47 +0200 Subject: [PATCH 14/78] test: fix tests after not allowing initialSnapshot to be called twice --- test/lib/fixtures.ts | 1 - test/reservoir/l1Reservoir.test.ts | 55 ++++++++----- test/rewards/rewards.test.ts | 124 +++++++++++++++++------------ 3 files changed, 106 insertions(+), 74 deletions(-) diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 635e9ff51..725a793ba 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -178,7 +178,6 @@ export class NetworkFixture { await grt.connect(deployer).addMinter(l1Reservoir.address) await l1Reservoir.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) await l1Reservoir.connect(deployer).approveRewardsManager() - await l1Reservoir.connect(deployer).initialSnapshot(toBN(0)) } // Unpause the protocol diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 07a55aa61..afc6233ca 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { BigNumber, constants, utils } from 'ethers' -import { defaults, deployContract } from '../lib/deployment' +import { defaults, deployContract, deployL1Reservoir } from '../lib/deployment' import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' import { GraphToken } from '../../build/types/GraphToken' @@ -26,6 +26,9 @@ import path from 'path' import { Artifacts } from 'hardhat/internal/artifacts' import { Interface } from 'ethers/lib/utils' import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +import { SubgraphDeploymentID } from '@graphprotocol/common-ts' +import { Controller } from '../../build/types/Controller' +import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' const ARTIFACTS_PATH = path.resolve('build/contracts') const artifacts = new Artifacts(ARTIFACTS_PATH) const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi @@ -53,6 +56,8 @@ describe('L1Reservoir', () => { let l1Reservoir: L1Reservoir let bridgeEscrow: BridgeEscrow let l1GraphTokenGateway: L1GraphTokenGateway + let controller: Controller + let proxyAdmin: GraphProxyAdmin let supplyBeforeDrip: BigNumber let dripBlock: BigNumber @@ -103,8 +108,6 @@ describe('L1Reservoir', () => { blocksToAdvance: BigNumber, dripInterval = defaults.rewards.dripInterval, ) => { - // Initial snapshot defines the first lastRewardsUpdateBlock - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) const supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -149,8 +152,10 @@ describe('L1Reservoir', () => { fixture = new NetworkFixture() fixtureContracts = await fixture.load(governor.signer) - ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway } = fixtureContracts) + ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway, controller, proxyAdmin } = + fixtureContracts) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) await fixture.configureL1Bridge( governor.signer, @@ -177,32 +182,45 @@ describe('L1Reservoir', () => { describe('configuration', function () { describe('initial snapshot', function () { + let reservoir: L1Reservoir + beforeEach(async function () { + // Deploy a new reservoir to avoid issues with initialSnapshot being called twice + reservoir = await deployL1Reservoir(governor.signer, controller.address, proxyAdmin) + await grt.connect(governor.signer).addMinter(reservoir.address) + }) + it('rejects call if unauthorized', async function () { - const tx = l1Reservoir.connect(testAccount1.signer).initialSnapshot(toGRT('1.025')) + const tx = reservoir.connect(testAccount1.signer).initialSnapshot(toGRT('1.025')) await expect(tx).revertedWith('Caller must be Controller governor') }) it('snapshots the total GRT supply', async function () { - const tx = l1Reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + const tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) const supply = await grt.totalSupply() await expect(tx) - .emit(l1Reservoir, 'InitialSnapshotTaken') + .emit(reservoir, 'InitialSnapshotTaken') .withArgs(await latestBlock(), supply, toGRT('0')) - expect(await grt.balanceOf(l1Reservoir.address)).to.eq(toGRT('0')) - expect(await l1Reservoir.issuanceBase()).to.eq(supply) - expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + expect(await grt.balanceOf(reservoir.address)).to.eq(toGRT('0')) + expect(await reservoir.issuanceBase()).to.eq(supply) + expect(await reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) }) it('mints pending rewards and includes them in the snapshot', async function () { const pending = toGRT('10000000') - const tx = l1Reservoir.connect(governor.signer).initialSnapshot(pending) + const tx = reservoir.connect(governor.signer).initialSnapshot(pending) const supply = await grt.totalSupply() const expectedSupply = supply.add(pending) await expect(tx) - .emit(l1Reservoir, 'InitialSnapshotTaken') + .emit(reservoir, 'InitialSnapshotTaken') .withArgs(await latestBlock(), expectedSupply, pending) - expect(await grt.balanceOf(l1Reservoir.address)).to.eq(pending) - expect(await l1Reservoir.issuanceBase()).to.eq(expectedSupply) - expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + expect(await grt.balanceOf(reservoir.address)).to.eq(pending) + expect(await reservoir.issuanceBase()).to.eq(expectedSupply) + expect(await reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + }) + it('cannot be called more than once', async function () { + let tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + await expect(tx).emit(reservoir, 'InitialSnapshotTaken') + tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + await expect(tx).revertedWith('Cannot call this function more than once') }) }) describe('issuance rate update', function () { @@ -307,8 +325,6 @@ describe('L1Reservoir', () => { // issuanceRate or l2RewardsFraction is updated describe('drip', function () { it('mints rewards for the next week', async function () { - // Initial snapshot defines the first lastRewardsUpdateBlock - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -330,8 +346,6 @@ describe('L1Reservoir', () => { .withArgs(actualAmount, toBN(0), expectedNextDeadline) }) it('has no effect if called a second time in the same block', async function () { - // Initial snapshot defines the first lastRewardsUpdateBlock - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -385,7 +399,6 @@ describe('L1Reservoir', () => { }) it('sends the specified fraction of the rewards with a callhook to L2', async function () { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -435,7 +448,6 @@ describe('L1Reservoir', () => { }) it('sends the outstanding amount if the L2 rewards fraction changes', async function () { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -538,7 +550,6 @@ describe('L1Reservoir', () => { }) it('sends the outstanding amount if the L2 rewards fraction stays constant', async function () { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 1cbeca0e2..b95b0464e 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -9,6 +9,8 @@ import { GraphToken } from '../../build/types/GraphToken' import { RewardsManager } from '../../build/types/RewardsManager' import { Staking } from '../../build/types/Staking' +import { BigNumber as BN } from 'bignumber.js' + import { advanceBlocks, deriveChannelKey, @@ -112,10 +114,6 @@ describe('Rewards', () => { governor.signer, )) - // 5% minute rate (4 blocks) - // await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - // await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) - // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { await grt.connect(governor.signer).mint(wallet.address, toGRT('1000000')) @@ -250,6 +248,31 @@ describe('Rewards', () => { ) } + function calculatedExpectedRewards( + firstSnapshotBlocks: BN, + lastSnapshotBlocks: BN, + allocatedTokens: BN, + ): BigNumber { + const issuanceBase = new BN(10004000000) + const issuanceRate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (issuanceBase * issuanceRate^nBlocks - issuanceBase) / allocatedTokens + // The first snapshot is after allocating, that is lastSnapshotBlocks blocks after dripBlock: + const startRewardsPerToken = issuanceBase + .times(issuanceRate.pow(firstSnapshotBlocks)) + .minus(issuanceBase) + .div(allocatedTokens) + // The final snapshot is when we close the allocation, that happens 8 blocks later: + const endRewardsPerToken = issuanceBase + .times(issuanceRate.pow(lastSnapshotBlocks)) + .minus(issuanceBase) + .div(allocatedTokens) + // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * allocatedTokens. + return toGRT( + endRewardsPerToken.minus(startRewardsPerToken).times(allocatedTokens).toPrecision(18), + ) + } + beforeEach(async function () { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) @@ -573,17 +596,15 @@ describe('Rewards', () => { describe('takeAndBurnRewards', function () { it('should burn rewards on closed allocation with POI zero', async function () { // Align with the epoch boundary - // dripBlock (81) await epochManager.setEpochLength(10) - // dripBlock + 1 await advanceToNextEpoch(epochManager) - // dripBlock + 4 + // Setup await setupIndexerAllocation() - // dripBlock + 7 + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + // Jump await advanceToNextEpoch(epochManager) - // dripBlock + 14 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -591,19 +612,18 @@ describe('Rewards', () => { const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) const beforeStakingBalance = await grt.balanceOf(staking.address) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 7 blocks after dripBlock: - // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 - // The final snapshot is when we close the allocation, that happens 8 blocks later: - // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('9834378') - // Close allocation with POI zero, which should burn the rewards const tx = await staking.connect(indexer1.signer).closeAllocation(allocationID1, HashZero) const receipt = await tx.wait() + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) + const log = findRewardsManagerEvents(receipt)[0] const event = log.args expect(log.name).eq('RewardsBurned') @@ -645,6 +665,7 @@ describe('Rewards', () => { // dripBlock + 4 // Setup await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // dripBlock + 7 // Jump await advanceToNextEpoch(epochManager) @@ -656,21 +677,19 @@ describe('Rewards', () => { const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) const beforeStakingBalance = await grt.balanceOf(staking.address) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 7 blocks after dripBlock: - // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 - // The final snapshot is when we close the allocation, that happens 8 blocks later: - // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('9834378') - // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) + const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) @@ -707,6 +726,7 @@ describe('Rewards', () => { await advanceToNextEpoch(epochManager) // Setup await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) @@ -717,24 +737,22 @@ describe('Rewards', () => { const beforeDestinationBalance = await grt.balanceOf(destinationAddress) const beforeStakingBalance = await grt.balanceOf(staking.address) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 7 blocks after dripBlock: - // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 - // The final snapshot is when we close the allocation, that happens 8 blocks later: - // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('9834378') - // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) + + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) // After state @@ -766,15 +784,15 @@ describe('Rewards', () => { cooldownBlocks: 5, } const tokensToDelegate = toGRT('2000') - // dripBlock (81) await epochManager.setEpochLength(10) - // dripBlock + 1 + // Align with the epoch boundary await advanceToNextEpoch(epochManager) - // dripBlock + 4 + // Setup the allocation and delegators await setupIndexerAllocationWithDelegation(tokensToDelegate, delegationParams) - // dripBlock + 11 + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + // Jump await advanceToNextEpoch(epochManager) // dripBlock + 14 @@ -786,6 +804,7 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // After state const afterTokenSupply = await grt.totalSupply() @@ -795,14 +814,12 @@ describe('Rewards', () => { // Check that rewards are put into indexer stake (only indexer cut) // Check that rewards are put into delegators pool accordingly - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 11 blocks after dripBlock: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 11 - 10004000000) / 14500 = 931.94 - // The final snapshot is when we close the allocation, that happens 4 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 15 - 10004000000) / 14500 = 1271.14 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. - const expectedIndexingRewards = toGRT('4918396') + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(14500), + ) + // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards) @@ -827,17 +844,16 @@ describe('Rewards', () => { await rewardsManager.connect(governor.signer).setDenied(subgraphDeploymentID1, true) await advanceToNextEpoch(epochManager) await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) - // This is the same amount as in the test above - // (see 'should distribute rewards on closed allocation and stake') - const expectedIndexingRewards = toGRT('9834378') const supplyBefore = await grt.totalSupply() // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) await expect(tx).emit(rewardsManager, 'RewardsDenied') + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) const receipt = await (await tx).wait() const logs = findRewardsManagerEvents(receipt) expect(logs.length).to.eq(1) @@ -846,6 +862,12 @@ describe('Rewards', () => { expect(ev.indexer).to.eq(indexer1.address) expect(ev.allocationID).to.eq(allocationID1) expect(ev.epoch).to.eq(await epochManager.currentEpoch()) + + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) expect(toRound(ev.amount)).to.eq(toRound(expectedIndexingRewards)) // Check that the rewards were burned // We divide by 10 to accept numeric errors up to 10 GRT From 29702d6e87fe6e0a526fb55dd79d7f318b29d240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 12 Jul 2022 18:30:06 +0200 Subject: [PATCH 15/78] fix: hardcode L2 initial supply to 0 [L-05] --- config/graph.arbitrum-one.yml | 1 - contracts/l2/token/L2GraphToken.sol | 7 ++++--- test/lib/deployment.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index 80d7cceeb..c9303d410 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -16,7 +16,6 @@ contracts: proxy: true init: owner: *governor - initialSupply: "0" L2GraphTokenGateway: proxy: true init: diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index 2307385c5..d58014e5e 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -239,11 +239,12 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { /** * @dev L2 Graph Token Contract initializer. * @param owner Governance address that owns this contract - * @param _initialSupply Initial supply of GRT */ - function initialize(address owner, uint256 _initialSupply) external onlyImpl { + function initialize(address owner) external onlyImpl { require(owner != address(0), "Owner must be set"); - GraphTokenUpgradeable._initialize(owner, _initialSupply); + // Initial supply hard coded to 0 as tokens are only supposed + // to be minted through the bridge. + GraphTokenUpgradeable._initialize(owner, 0); } /** diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index fe5eed768..eac7530e7 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -299,7 +299,7 @@ export async function deployL2GRT( return network.deployContractWithProxy( proxyAdmin, 'L2GraphToken', - [await deployer.getAddress(), toBN('0')], + [await deployer.getAddress()], deployer, ) as unknown as L2GraphToken } From 68cc8d3e69ed244adc251eb5618be83b650a3322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 12 Jul 2022 18:51:27 +0200 Subject: [PATCH 16/78] fix: add more input validation [L-07] --- contracts/gateway/L1GraphTokenGateway.sol | 9 +++++++++ contracts/l2/gateway/L2GraphTokenGateway.sol | 4 ++++ contracts/l2/token/L2GraphToken.sol | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index 5e1304b08..1ffb88d44 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -3,6 +3,7 @@ pragma solidity ^0.7.6; pragma abicoder v2; +import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; import "../arbitrum/L1ArbitrumMessenger.sol"; @@ -96,6 +97,8 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param _l1Router Address of the Gateway Router */ function setArbitrumAddresses(address _inbox, address _l1Router) external onlyGovernor { + require(_inbox != address(0), "INVALID_INBOX"); + require(_l1Router != address(0), "INVALID_L1_ROUTER"); inbox = _inbox; l1Router = _l1Router; emit ArbitrumAddressesSet(_inbox, _l1Router); @@ -106,6 +109,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param _l2GRT Address of the GRT contract on L2 */ function setL2TokenAddress(address _l2GRT) external onlyGovernor { + require(_l2GRT != address(0), "INVALID_L2_GRT"); l2GRT = _l2GRT; emit L2TokenAddressSet(_l2GRT); } @@ -115,6 +119,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param _l2Counterpart Address of the corresponding L2GraphTokenGateway on Arbitrum */ function setL2CounterpartAddress(address _l2Counterpart) external onlyGovernor { + require(_l2Counterpart != address(0), "INVALID_L2_COUNTERPART"); l2Counterpart = _l2Counterpart; emit L2CounterpartAddressSet(_l2Counterpart); } @@ -124,6 +129,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param _escrow Address of the BridgeEscrow */ function setEscrowAddress(address _escrow) external onlyGovernor { + require(_escrow != address(0) && Address.isContract(_escrow), "INVALID_ESCROW"); escrow = _escrow; emit EscrowAddressSet(_escrow); } @@ -134,6 +140,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param newWhitelisted Address to add to the whitelist */ function addToCallhookWhitelist(address newWhitelisted) external onlyGovernor { + require(newWhitelisted != address(0), "INVALID_ADDRESS"); callhookWhitelist[newWhitelisted] = true; emit AddedToCallhookWhitelist(newWhitelisted); } @@ -144,6 +151,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param notWhitelisted Address to remove from the whitelist */ function removeFromCallhookWhitelist(address notWhitelisted) external onlyGovernor { + require(notWhitelisted != address(0), "INVALID_ADDRESS"); callhookWhitelist[notWhitelisted] = false; emit RemovedFromCallhookWhitelist(notWhitelisted); } @@ -172,6 +180,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { IGraphToken token = graphToken(); require(_l1Token == address(token), "TOKEN_NOT_GRT"); require(_amount > 0, "INVALID_ZERO_AMOUNT"); + require(_to != address(0), "INVALID_DESTINATION"); // nested scopes to avoid stack too deep errors address from; diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index b639e2374..7148b9470 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -93,6 +93,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @param _l2Router Address of the L2 Router (provided by Arbitrum) */ function setL2Router(address _l2Router) external onlyGovernor { + require(_l2Router != address(0), "INVALID_L2_ROUTER"); l2Router = _l2Router; emit L2RouterSet(_l2Router); } @@ -102,6 +103,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @param _l1GRT L1 address of the Graph Token contract */ function setL1TokenAddress(address _l1GRT) external onlyGovernor { + require(_l1GRT != address(0), "INVALID_L1_GRT"); l1GRT = _l1GRT; emit L1TokenAddressSet(_l1GRT); } @@ -111,6 +113,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @param _l1Counterpart Address of the L1GraphTokenGateway on L1 */ function setL1CounterpartAddress(address _l1Counterpart) external onlyGovernor { + require(_l1Counterpart != address(0), "INVALID_L1_COUNTERPART"); l1Counterpart = _l1Counterpart; emit L1CounterpartAddressSet(_l1Counterpart); } @@ -159,6 +162,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); require(_amount > 0, "INVALID_ZERO_AMOUNT"); require(msg.value == 0, "INVALID_NONZERO_VALUE"); + require(_to != address(0), "INVALID_DESTINATION"); OutboundCalldata memory s; diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index d58014e5e..a0adcfd48 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -138,6 +138,7 @@ contract GraphTokenUpgradeable is * @param _account Address of the minter */ function addMinter(address _account) external onlyGovernor { + require(_account != address(0), "INVALID_MINTER"); _addMinter(_account); } @@ -146,6 +147,7 @@ contract GraphTokenUpgradeable is * @param _account Address of the minter */ function removeMinter(address _account) external onlyGovernor { + require(_minters[_account], "NOT_A_MINTER"); _removeMinter(_account); } @@ -153,6 +155,7 @@ contract GraphTokenUpgradeable is * @dev Renounce to be a minter. */ function renounceMinter() external { + require(_minters[msg.sender], "NOT_A_MINTER"); _removeMinter(msg.sender); } @@ -251,6 +254,7 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { * @dev Sets the address of the L2 gateway allowed to mint tokens */ function setGateway(address gw) external onlyGovernor { + require(gw != address(0), "INVALID_GATEWAY"); gateway = gw; emit GatewaySet(gateway); } @@ -259,6 +263,7 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { * @dev Sets the address of the counterpart token on L1 */ function setL1Address(address addr) external onlyGovernor { + require(addr != address(0), "INVALID_L1_ADDRESS"); l1Address = addr; emit L1AddressSet(addr); } From 06275b755dcbf4d192887080ad4cc405bd13001e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 12 Jul 2022 19:13:53 +0200 Subject: [PATCH 17/78] fix: remove redundant callhook whitelist on L2 [L-08] --- contracts/l2/gateway/L2GraphTokenGateway.sol | 29 +--------- test/l2/l2GraphTokenGateway.test.ts | 59 +------------------- 2 files changed, 3 insertions(+), 85 deletions(-) diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 7148b9470..c32b2965a 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -30,8 +30,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { address public l1Counterpart; // Address of the Arbitrum Gateway Router on L2 address public l2Router; - // Addresses in L1 that are whitelisted to have callhooks on transfers - mapping(address => bool) public callhookWhitelist; + // Calldata included in an outbound transfer, stored as a structure for convenience and stack depth struct OutboundCalldata { address from; @@ -62,10 +61,6 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { event L1TokenAddressSet(address _l1GRT); // Emitted when the address of the counterpart gateway on L1 has been updated event L1CounterpartAddressSet(address _l1Counterpart); - // Emitted when an address is added to the callhook whitelist - event AddedToCallhookWhitelist(address newWhitelisted); - // Emitted when an address is removed from the callhook whitelist - event RemovedFromCallhookWhitelist(address notWhitelisted); // Emitted when a callhook call failed event CallhookFailed(address destination); @@ -118,26 +113,6 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { emit L1CounterpartAddressSet(_l1Counterpart); } - /** - * @dev Adds an L1 address to the callhook whitelist. - * This address will be allowed to include callhooks when transferring tokens. - * @param newWhitelisted Address to add to the whitelist - */ - function addToCallhookWhitelist(address newWhitelisted) external onlyGovernor { - callhookWhitelist[newWhitelisted] = true; - emit AddedToCallhookWhitelist(newWhitelisted); - } - - /** - * @dev Removes an L1 address from the callhook whitelist. - * This address will no longer be allowed to include callhooks when transferring tokens. - * @param notWhitelisted Address to remove from the whitelist - */ - function removeFromCallhookWhitelist(address notWhitelisted) external onlyGovernor { - callhookWhitelist[notWhitelisted] = false; - emit RemovedFromCallhookWhitelist(notWhitelisted); - } - /** * @notice Burns L2 tokens and initiates a transfer to L1. * The tokens will be available on L1 only after the wait period (7 days) is over, @@ -241,7 +216,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount); - if (_data.length > 0 && callhookWhitelist[_from] == true) { + if (_data.length > 0) { bytes memory callhookData; { bytes memory gatewayData; diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index 36b63a33a..b727cf84d 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -165,48 +165,6 @@ describe('L2GraphTokenGateway', () => { expect(await l2GraphTokenGateway.l1Counterpart()).eq(mockL1Gateway.address) }) }) - describe('addToCallhookWhitelist', function () { - it('is not callable by addreses that are not the governor', async function () { - const tx = l2GraphTokenGateway - .connect(tokenSender.signer) - .addToCallhookWhitelist(tokenSender.address) - await expect(tx).revertedWith('Caller must be Controller governor') - expect(await l2GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) - }) - it('adds an address to the callhook whitelist', async function () { - const tx = l2GraphTokenGateway - .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) - await expect(tx) - .emit(l2GraphTokenGateway, 'AddedToCallhookWhitelist') - .withArgs(tokenSender.address) - expect(await l2GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) - }) - }) - describe('removeFromCallhookWhitelist', function () { - it('is not callable by addreses that are not the governor', async function () { - await l2GraphTokenGateway - .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) - const tx = l2GraphTokenGateway - .connect(tokenSender.signer) - .removeFromCallhookWhitelist(tokenSender.address) - await expect(tx).revertedWith('Caller must be Controller governor') - expect(await l2GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) - }) - it('removes an address from the callhook whitelist', async function () { - await l2GraphTokenGateway - .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) - const tx = l2GraphTokenGateway - .connect(governor.signer) - .removeFromCallhookWhitelist(tokenSender.address) - await expect(tx) - .emit(l2GraphTokenGateway, 'RemovedFromCallhookWhitelist') - .withArgs(tokenSender.address) - expect(await l2GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) - }) - }) describe('Pausable behavior', () => { it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { let tx = l2GraphTokenGateway.connect(tokenSender.signer).setPaused(false) @@ -426,22 +384,11 @@ describe('L2GraphTokenGateway', () => { it('mints and sends tokens when called by the aliased gateway', async function () { await testValidFinalizeTransfer(defaultData) }) - it('does not call any callhooks if the sender is not whitelisted', async function () { - const rewardsManagerMock = await smock.fake('RewardsManagerMock', { - address: l2Receiver.address, - }) - rewardsManagerMock.pow.returns(1) - await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData) - expect(rewardsManagerMock.pow).to.not.have.been.called - }) it('calls a callhook if the sender is whitelisted', async function () { const rewardsManagerMock = await smock.fake('RewardsManagerMock', { address: l2Receiver.address, }) rewardsManagerMock.pow.returns(1) - await l2GraphTokenGateway - .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData) expect(rewardsManagerMock.pow).to.have.been.calledWith(toBN(1), toBN(2), toBN(3)) }) @@ -450,9 +397,6 @@ describe('L2GraphTokenGateway', () => { address: l2Receiver.address, }) rewardsManagerMock.pow.reverts() - await l2GraphTokenGateway - .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) await me.signer.sendTransaction({ to: await mockL1GatewayL2Alias.getAddress(), @@ -467,8 +411,7 @@ describe('L2GraphTokenGateway', () => { toGRT('10'), defaultDataWithNotEmptyCallHookData, ) - await expect(tx) - .revertedWith('CALLHOOK_FAILED') + await expect(tx).revertedWith('CALLHOOK_FAILED') }) }) }) From 395d8588d1782a3882fee54a12c820a9afa547c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 12 Jul 2022 19:24:55 +0200 Subject: [PATCH 18/78] fix: allow bigger msg.value than needed [L-09] --- contracts/gateway/L1GraphTokenGateway.sol | 2 +- test/gateway/l1GraphTokenGateway.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index 1ffb88d44..52130d6d9 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -202,7 +202,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { // if a user does not desire immediate redemption they should provide // a msg.value of AT LEAST maxSubmissionCost uint256 expectedEth = maxSubmissionCost + (_maxGas * _gasPriceBid); - require(msg.value == expectedEth, "WRONG_ETH_VALUE"); + require(msg.value >= expectedEth, "WRONG_ETH_VALUE"); } outboundCalldata = getOutboundCalldata(_l1Token, from, _to, _amount, extraData); } diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index 6e69acf7a..bf679cfbd 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -433,7 +433,7 @@ describe('L1GraphTokenGateway', () => { gasPriceBid, defaultData, { - value: defaultEthValue.add(1), + value: defaultEthValue.sub(1), }, ) await expect(tx).revertedWith('WRONG_ETH_VALUE') From 2f41f81b5ccaa25400d893cae50c6a8193fedf1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 12:33:19 +0200 Subject: [PATCH 19/78] fix: document the need for drip after a param update [L-06] --- contracts/reservoir/L1Reservoir.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 1990d4abf..7a46e12dc 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -70,6 +70,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * This means that it needs to be greater than 1.0, any number under 1.0 is not * allowed and an issuance rate of 1.0 means no issuance. * To accommodate a high precision the issuance rate is expressed in wei, i.e. fixed point at 1e18. + * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, + * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure + * good synchronization between L1 and L2. * @param _issuanceRate Issuance rate expressed in wei / fixed point at 1e18 */ function setIssuanceRate(uint256 _issuanceRate) external onlyGovernor { @@ -82,6 +85,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @dev Sets the L2 rewards fraction. * This is the portion of the indexer rewards that are sent to L2. * The value is in fixed point at 1e18 and must be less than 1. + * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, + * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure + * good synchronization between L1 and L2. * @param _l2RewardsFraction Fraction of rewards to send to L2, in wei / fixed point at 1e18 */ function setL2RewardsFraction(uint256 _l2RewardsFraction) external onlyGovernor { From e8544e83a09ae06cf5946a123aa51a243ac1dd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 12 Jul 2022 18:55:20 +0200 Subject: [PATCH 20/78] fix: validate L2Reservoir address on L1Reservoir [L-07] --- contracts/reservoir/L1Reservoir.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 7a46e12dc..35127f367 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -102,6 +102,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _l2ReservoirAddress New address for the L2Reservoir on L2 */ function setL2ReservoirAddress(address _l2ReservoirAddress) external onlyGovernor { + require(_l2ReservoirAddress != address(0), "INVALID_L2_RESERVOIR"); l2ReservoirAddress = _l2ReservoirAddress; emit L2ReservoirAddressUpdated(_l2ReservoirAddress); } From 0b516439f7da02afc769c9487bb16afed8cbb8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 12:54:52 +0200 Subject: [PATCH 21/78] fix: rename normalizedSupply to l2IssuanceBase in L2 message to avoid confusion [L-10] --- contracts/reservoir/L1Reservoir.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 35127f367..98b4f7d4e 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -260,10 +260,10 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { uint256 gasPriceBid, uint256 maxSubmissionCost ) internal { - uint256 normalizedSupply = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); + uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); bytes memory extraData = abi.encodeWithSelector( IL2Reservoir.receiveDrip.selector, - normalizedSupply, + l2IssuanceBase, issuanceRate, nextDripNonce ); From f561a34aead61a1ceee0014860f892d460285e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 13:21:35 +0200 Subject: [PATCH 22/78] fix: remove silent failure if rewardsManager is not set [L-12] --- contracts/staking/Staking.sol | 9 --------- 1 file changed, 9 deletions(-) diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index 94be54420..f7052dcb1 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -1568,9 +1568,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _updateRewards(bytes32 _subgraphDeploymentID) private returns (uint256) { IRewardsManager rewardsManager = rewardsManager(); - if (address(rewardsManager) == address(0)) { - return 0; - } return rewardsManager.onSubgraphAllocationUpdate(_subgraphDeploymentID); } @@ -1580,9 +1577,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _distributeRewards(address _allocationID, address _indexer) private { IRewardsManager rewardsManager = rewardsManager(); - if (address(rewardsManager) == address(0)) { - return; - } // Automatically triggers update of rewards snapshot as allocation will change // after this call. Take rewards transfers tokens for the Staking contract to distribute @@ -1611,9 +1605,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _takeAndBurnRewards(address _allocationID) private { IRewardsManager rewardsManager = rewardsManager(); - if (address(rewardsManager) == address(0)) { - return; - } // Automatically triggers update of rewards snapshot as allocation will change // after this call. From 061ab3f6fcdad9229de5d3a1f323c473fa0ccef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 13:35:32 +0200 Subject: [PATCH 23/78] fix: document non-expiring permits on L2GraphToken [M-02] --- contracts/l2/token/L2GraphToken.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index a0adcfd48..037fda157 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -101,7 +101,7 @@ contract GraphTokenUpgradeable is * @param _owner Address of the token holder * @param _spender Address of the approved spender * @param _value Amount of tokens to approve the spender - * @param _deadline Expiration time of the signed permit + * @param _deadline Expiration time of the signed permit (if zero, the permit will never expire, so use with caution) * @param _v Signature version * @param _r Signature r value * @param _s Signature s value From f2641f1fe179d15988b7a9b89a6fbe966933539f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 15:01:36 +0200 Subject: [PATCH 24/78] test: remove unneeded L2 callhook whitelist entry --- test/lib/fixtures.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 725a793ba..54dc8bf3f 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -311,9 +311,6 @@ export class NetworkFixture { await l2FixtureContracts.l2GraphTokenGateway .connect(deployer) .setL1CounterpartAddress(mockL1GatewayAddress) - await l2FixtureContracts.l2GraphTokenGateway - .connect(deployer) - .addToCallhookWhitelist(mockL1ReservoirAddress) await l2FixtureContracts.l2GraphTokenGateway.connect(deployer).setPaused(false) } From 2582b109b38f78d42e5c8e8951605732ac19f4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 13:52:32 +0200 Subject: [PATCH 25/78] fix: document the need for providing the gateways with allowance [N-01] --- contracts/gateway/L1GraphTokenGateway.sol | 1 + contracts/l2/gateway/L2GraphTokenGateway.sol | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index 52130d6d9..1218d5fac 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -160,6 +160,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox. * The tokens are escrowed by the gateway until they are withdrawn back to L1. * The ticket must be redeemed on L2 to receive tokens at the specified address. + * Note that the caller must previously allow the gateway to spend the specified amount of GRT. * @dev maxGas and gasPriceBid must be set using Arbitrum's NodeInterface.estimateRetryableTicket method. * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) * @param _to Recipient address on L2 diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index c32b2965a..0d363c0f2 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -117,6 +117,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @notice Burns L2 tokens and initiates a transfer to L1. * The tokens will be available on L1 only after the wait period (7 days) is over, * and will require an Outbox.executeTransaction to finalize. + * Note that the caller must previously allow the gateway to spend the specified amount of GRT. * @dev no additional callhook data is allowed. The two unused params are needed * for compatibility with Arbitrum's gateway router. * The function is payable for ITokenGateway compatibility, but msg.value must be zero. From bb7f2e7eabd40135d10daaad61601f2884a65b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 13:57:47 +0200 Subject: [PATCH 26/78] fix: remove unused event in L2GraphTokenGateway [N-02] --- contracts/l2/gateway/L2GraphTokenGateway.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 0d363c0f2..1d901dd85 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -61,8 +61,6 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { event L1TokenAddressSet(address _l1GRT); // Emitted when the address of the counterpart gateway on L1 has been updated event L1CounterpartAddressSet(address _l1Counterpart); - // Emitted when a callhook call failed - event CallhookFailed(address destination); /** * @dev Checks that the sender is the L2 alias of the counterpart From 0ea0d303f85887cafbcf3920648b06914a9265c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 14:03:22 +0200 Subject: [PATCH 27/78] fix: move nonce change to reduce gas on reverted permit call [N-03] --- contracts/l2/token/L2GraphToken.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index 037fda157..72cffee1f 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -124,12 +124,12 @@ contract GraphTokenUpgradeable is ) ) ); - nonces[_owner] = nonces[_owner].add(1); address recoveredAddress = ECDSA.recover(digest, abi.encodePacked(_r, _s, _v)); require(_owner == recoveredAddress, "GRT: invalid permit"); require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); + nonces[_owner] = nonces[_owner].add(1); _approve(_owner, _spender, _value); } From 31e07c42989bba6427678443dbb940a02b717d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 14:57:55 +0200 Subject: [PATCH 28/78] fix: general code improvements [N-05] --- contracts/gateway/BridgeEscrow.sol | 12 +-- contracts/gateway/GraphTokenGateway.sol | 6 +- contracts/gateway/L1GraphTokenGateway.sol | 92 ++++++++-------- contracts/l2/gateway/L2GraphTokenGateway.sol | 60 +++++------ contracts/l2/reservoir/L2Reservoir.sol | 8 +- contracts/l2/token/L2GraphToken.sol | 98 ++++++++--------- contracts/reservoir/IReservoir.sol | 10 +- contracts/reservoir/L1Reservoir.sol | 104 +++++++++---------- contracts/reservoir/Reservoir.sol | 60 +++++------ contracts/rewards/RewardsManager.sol | 3 +- contracts/tests/arbitrum/BridgeMock.sol | 48 ++++----- contracts/tests/arbitrum/InboxMock.sol | 82 +++++++-------- contracts/tests/arbitrum/OutboxMock.sol | 58 +++++------ 13 files changed, 320 insertions(+), 321 deletions(-) diff --git a/contracts/gateway/BridgeEscrow.sol b/contracts/gateway/BridgeEscrow.sol index 42cea78b8..2d161e222 100644 --- a/contracts/gateway/BridgeEscrow.sol +++ b/contracts/gateway/BridgeEscrow.sol @@ -25,18 +25,18 @@ contract BridgeEscrow is GraphUpgradeable, Managed { /** * @dev Approve a spender (i.e. a bridge that manages the GRT funds held by the escrow) - * @param spender Address of the spender that will be approved + * @param _spender Address of the spender that will be approved */ - function approveAll(address spender) external onlyGovernor { - graphToken().approve(spender, MAX_UINT256); + function approveAll(address _spender) external onlyGovernor { + graphToken().approve(_spender, MAX_UINT256); } /** * @dev Revoke a spender (i.e. a bridge that will no longer manage the GRT funds held by the escrow) - * @param spender Address of the spender that will be revoked + * @param _spender Address of the spender that will be revoked */ - function revokeAll(address spender) external onlyGovernor { + function revokeAll(address _spender) external onlyGovernor { IGraphToken grt = graphToken(); - grt.decreaseAllowance(spender, grt.allowance(address(this), spender)); + grt.decreaseAllowance(_spender, grt.allowance(address(this), _spender)); } } diff --git a/contracts/gateway/GraphTokenGateway.sol b/contracts/gateway/GraphTokenGateway.sol index 6cd33a432..00e8441f5 100644 --- a/contracts/gateway/GraphTokenGateway.sol +++ b/contracts/gateway/GraphTokenGateway.sol @@ -42,10 +42,10 @@ abstract contract GraphTokenGateway is GraphUpgradeable, Pausable, Managed, ITok /** * @notice Change the paused state of the contract - * @param newPaused New value for the pause state (true means the transfers will be paused) + * @param _newPaused New value for the pause state (true means the transfers will be paused) */ - function setPaused(bool newPaused) external onlyGovernorOrGuardian { - _setPaused(newPaused); + function setPaused(bool _newPaused) external onlyGovernorOrGuardian { + _setPaused(_newPaused); } /** diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index 1218d5fac..52a86bdf1 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -36,30 +36,30 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { // Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 event DepositInitiated( - address _l1Token, - address indexed _from, - address indexed _to, - uint256 indexed _sequenceNumber, - uint256 _amount + address l1Token, + address indexed from, + address indexed to, + uint256 indexed sequenceNumber, + uint256 amount ); // Emitted when an incoming transfer is finalized, i.e tokens are withdrawn from L2 to L1 event WithdrawalFinalized( - address _l1Token, - address indexed _from, - address indexed _to, - uint256 indexed _exitNum, - uint256 _amount + address l1Token, + address indexed from, + address indexed to, + uint256 indexed exitNum, + uint256 amount ); // Emitted when the Arbitrum Inbox and Gateway Router addresses have been updated - event ArbitrumAddressesSet(address _inbox, address _l1Router); + event ArbitrumAddressesSet(address inbox, address l1Router); // Emitted when the L2 GRT address has been updated - event L2TokenAddressSet(address _l2GRT); + event L2TokenAddressSet(address l2GRT); // Emitted when the counterpart L2GraphTokenGateway address has been updated - event L2CounterpartAddressSet(address _l2Counterpart); + event L2CounterpartAddressSet(address l2Counterpart); // Emitted when the escrow address has been updated - event EscrowAddressSet(address _escrow); + event EscrowAddressSet(address escrow); // Emitted when an address is added to the callhook whitelist event AddedToCallhookWhitelist(address newWhitelisted); // Emitted when an address is removed from the callhook whitelist @@ -137,23 +137,23 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { /** * @dev Adds an address to the callhook whitelist. * This address will be allowed to include callhooks when transferring tokens. - * @param newWhitelisted Address to add to the whitelist + * @param _newWhitelisted Address to add to the whitelist */ - function addToCallhookWhitelist(address newWhitelisted) external onlyGovernor { - require(newWhitelisted != address(0), "INVALID_ADDRESS"); - callhookWhitelist[newWhitelisted] = true; - emit AddedToCallhookWhitelist(newWhitelisted); + function addToCallhookWhitelist(address _newWhitelisted) external onlyGovernor { + require(_newWhitelisted != address(0), "INVALID_ADDRESS"); + callhookWhitelist[_newWhitelisted] = true; + emit AddedToCallhookWhitelist(_newWhitelisted); } /** * @dev Removes an address from the callhook whitelist. * This address will no longer be allowed to include callhooks when transferring tokens. - * @param notWhitelisted Address to remove from the whitelist + * @param _notWhitelisted Address to remove from the whitelist */ - function removeFromCallhookWhitelist(address notWhitelisted) external onlyGovernor { - require(notWhitelisted != address(0), "INVALID_ADDRESS"); - callhookWhitelist[notWhitelisted] = false; - emit RemovedFromCallhookWhitelist(notWhitelisted); + function removeFromCallhookWhitelist(address _notWhitelisted) external onlyGovernor { + require(_notWhitelisted != address(0), "INVALID_ADDRESS"); + callhookWhitelist[_notWhitelisted] = false; + emit RemovedFromCallhookWhitelist(_notWhitelisted); } /** @@ -264,12 +264,12 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @notice decodes calldata required for migration of tokens * @dev data must include maxSubmissionCost, extraData can be left empty. When the router * sends an outbound message, data also contains the from address. - * @param data encoded callhook data + * @param _data encoded callhook data * @return from sender of the tx * @return maxSubmissionCost base ether value required to keep retyrable ticket alive * @return extraData any other data sent to L2 */ - function parseOutboundData(bytes memory data) + function parseOutboundData(bytes memory _data) private view returns ( @@ -280,10 +280,10 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { { if (msg.sender == l1Router) { // Data encoded by the Gateway Router includes the sender address - (from, extraData) = abi.decode(data, (address, bytes)); + (from, extraData) = abi.decode(_data, (address, bytes)); } else { from = msg.sender; - extraData = data; + extraData = _data; } // User-encoded data contains the max retryable ticket submission cost // and additional L2 calldata @@ -294,41 +294,41 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @notice Creates calldata required to create a retryable ticket * @dev encodes the target function with its params which * will be called on L2 when the retryable ticket is redeemed - * @param l1Token Address of the Graph token contract on L1 - * @param from Address on L1 from which we're transferring tokens - * @param to Address on L2 to which we're transferring tokens - * @param amount Amount of GRT to transfer - * @param data Additional call data for the L2 transaction, which must be empty + * @param _l1Token Address of the Graph token contract on L1 + * @param _from Address on L1 from which we're transferring tokens + * @param _to Address on L2 to which we're transferring tokens + * @param _amount Amount of GRT to transfer + * @param _data Additional call data for the L2 transaction, which must be empty * @return outboundCalldata Encoded calldata (including function selector) for the L2 transaction */ function getOutboundCalldata( - address l1Token, - address from, - address to, - uint256 amount, - bytes memory data + address _l1Token, + address _from, + address _to, + uint256 _amount, + bytes memory _data ) public pure returns (bytes memory outboundCalldata) { bytes memory emptyBytes; outboundCalldata = abi.encodeWithSelector( ITokenGateway.finalizeInboundTransfer.selector, - l1Token, - from, - to, - amount, - abi.encode(emptyBytes, data) + _l1Token, + _from, + _to, + _amount, + abi.encode(emptyBytes, _data) ); } /** * @notice Calculate the L2 address of a bridged token * @dev In our case, this would only work for GRT. - * @param l1ERC20 address of L1 GRT contract + * @param _l1ERC20 address of L1 GRT contract * @return L2 address of the bridged GRT token */ - function calculateL2TokenAddress(address l1ERC20) external view override returns (address) { + function calculateL2TokenAddress(address _l1ERC20) external view override returns (address) { IGraphToken token = graphToken(); - if (l1ERC20 != address(token)) { + if (_l1ERC20 != address(token)) { return address(0); } return l2GRT; diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 1d901dd85..3df59bb61 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -40,27 +40,27 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { // Emitted when an incoming transfer is finalized, i.e. tokens were deposited from L1 to L2 event DepositFinalized( address indexed l1Token, - address indexed _from, - address indexed _to, - uint256 _amount + address indexed from, + address indexed to, + uint256 amount ); // Emitted when an outbound transfer is initiated, i.e. tokens are being withdrawn from L2 back to L1 event WithdrawalInitiated( address l1Token, - address indexed _from, - address indexed _to, - uint256 indexed _l2ToL1Id, - uint256 _exitNum, - uint256 _amount + address indexed from, + address indexed to, + uint256 indexed l2ToL1Id, + uint256 exitNum, + uint256 amount ); // Emitted when the Arbitrum Gateway Router address on L2 has been updated - event L2RouterSet(address _l2Router); + event L2RouterSet(address l2Router); // Emitted when the L1 Graph Token address has been updated - event L1TokenAddressSet(address _l1GRT); + event L1TokenAddressSet(address l1GRT); // Emitted when the address of the counterpart gateway on L1 has been updated - event L1CounterpartAddressSet(address _l1Counterpart); + event L1CounterpartAddressSet(address l1Counterpart); /** * @dev Checks that the sender is the L2 alias of the counterpart @@ -239,46 +239,46 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @notice Creates calldata required to send tx to L1 * @dev encodes the target function with its params which * will be called on L1 when the message is received on L1 - * @param token Address of the token on L1 - * @param from Address of the token sender on L2 - * @param to Address to which we're sending tokens on L1 - * @param amount Amount of GRT to transfer - * @param data Additional calldata for the transaction + * @param _token Address of the token on L1 + * @param _from Address of the token sender on L2 + * @param _to Address to which we're sending tokens on L1 + * @param _amount Amount of GRT to transfer + * @param _data Additional calldata for the transaction */ function getOutboundCalldata( - address token, - address from, - address to, - uint256 amount, - bytes memory data + address _token, + address _from, + address _to, + uint256 _amount, + bytes memory _data ) public pure returns (bytes memory outboundCalldata) { outboundCalldata = abi.encodeWithSelector( ITokenGateway.finalizeInboundTransfer.selector, - token, - from, - to, - amount, - abi.encode(0, data) // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 + _token, + _from, + _to, + _amount, + abi.encode(0, _data) // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 ); } /** * @notice Decodes calldata required for migration of tokens * @dev extraData can be left empty - * @param data Encoded callhook data + * @param _data Encoded callhook data * @return from Sender of the tx * @return extraData Any other data sent to L1 */ - function parseOutboundData(bytes memory data) + function parseOutboundData(bytes memory _data) private view returns (address from, bytes memory extraData) { if (msg.sender == l2Router) { - (from, extraData) = abi.decode(data, (address, bytes)); + (from, extraData) = abi.decode(_data, (address, bytes)); } else { from = msg.sender; - extraData = data; + extraData = _data; } } diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 8948f9ee1..5619b7abd 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -54,21 +54,21 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * - p is the issuance base at t0 (normalized by the L2 rewards fraction) * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on L2 since the last drip */ - function getNewRewards(uint256 blocknum) + function getNewRewards(uint256 _blocknum) public view override(Reservoir, IReservoir) returns (uint256 deltaRewards) { uint256 t0 = lastRewardsUpdateBlock; - if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { + if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { return 0; } deltaRewards = issuanceBase - .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) .div(TOKEN_DECIMALS) .sub(issuanceBase); } diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index 72cffee1f..e20cd0e29 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -69,33 +69,6 @@ contract GraphTokenUpgradeable is _; } - /** - * @dev Graph Token Contract initializer. - * @param _initialSupply Initial supply of GRT - */ - function _initialize(address owner, uint256 _initialSupply) internal { - __ERC20_init("Graph Token", "GRT"); - Governed._initialize(owner); - - // The Governor has the initial supply of tokens - _mint(owner, _initialSupply); - - // The Governor is the default minter - _addMinter(owner); - - // EIP-712 domain separator - DOMAIN_SEPARATOR = keccak256( - abi.encode( - DOMAIN_TYPE_HASH, - DOMAIN_NAME_HASH, - DOMAIN_VERSION_HASH, - _getChainID(), - address(this), - DOMAIN_SALT - ) - ); - } - /** * @dev Approve token allowance by validating a message signed by the holder. * @param _owner Address of the token holder @@ -125,7 +98,7 @@ contract GraphTokenUpgradeable is ) ); - address recoveredAddress = ECDSA.recover(digest, abi.encodePacked(_r, _s, _v)); + address recoveredAddress = ECDSA.recover(digest, _v, _r, _s); require(_owner == recoveredAddress, "GRT: invalid permit"); require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); @@ -177,6 +150,33 @@ contract GraphTokenUpgradeable is return _minters[_account]; } + /** + * @dev Graph Token Contract initializer. + * @param _initialSupply Initial supply of GRT + */ + function _initialize(address _owner, uint256 _initialSupply) internal { + __ERC20_init("Graph Token", "GRT"); + Governed._initialize(_owner); + + // The Governor has the initial supply of tokens + _mint(_owner, _initialSupply); + + // The Governor is the default minter + _addMinter(_owner); + + // EIP-712 domain separator + DOMAIN_SEPARATOR = keccak256( + abi.encode( + DOMAIN_TYPE_HASH, + DOMAIN_NAME_HASH, + DOMAIN_VERSION_HASH, + _getChainID(), + address(this), + DOMAIN_SALT + ) + ); + } + /** * @dev Add a new minter. * @param _account Address of the minter @@ -241,50 +241,50 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { /** * @dev L2 Graph Token Contract initializer. - * @param owner Governance address that owns this contract + * @param _owner Governance address that owns this contract */ - function initialize(address owner) external onlyImpl { - require(owner != address(0), "Owner must be set"); + function initialize(address _owner) external onlyImpl { + require(_owner != address(0), "Owner must be set"); // Initial supply hard coded to 0 as tokens are only supposed // to be minted through the bridge. - GraphTokenUpgradeable._initialize(owner, 0); + GraphTokenUpgradeable._initialize(_owner, 0); } /** * @dev Sets the address of the L2 gateway allowed to mint tokens */ - function setGateway(address gw) external onlyGovernor { - require(gw != address(0), "INVALID_GATEWAY"); - gateway = gw; + function setGateway(address _gw) external onlyGovernor { + require(_gw != address(0), "INVALID_GATEWAY"); + gateway = _gw; emit GatewaySet(gateway); } /** * @dev Sets the address of the counterpart token on L1 */ - function setL1Address(address addr) external onlyGovernor { - require(addr != address(0), "INVALID_L1_ADDRESS"); - l1Address = addr; - emit L1AddressSet(addr); + function setL1Address(address _addr) external onlyGovernor { + require(_addr != address(0), "INVALID_L1_ADDRESS"); + l1Address = _addr; + emit L1AddressSet(_addr); } /** * @dev Increases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L2) - * @param account Address to credit with the new tokens - * @param amount Number of tokens to mint + * @param _account Address to credit with the new tokens + * @param _amount Number of tokens to mint */ - function bridgeMint(address account, uint256 amount) external override onlyGateway { - _mint(account, amount); - emit BridgeMinted(account, amount); + function bridgeMint(address _account, uint256 _amount) external override onlyGateway { + _mint(_account, _amount); + emit BridgeMinted(_account, _amount); } /** * @dev Decreases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L1). - * @param account Address from which to extract the tokens - * @param amount Number of tokens to burn + * @param _account Address from which to extract the tokens + * @param _amount Number of tokens to burn */ - function bridgeBurn(address account, uint256 amount) external override onlyGateway { - burnFrom(account, amount); - emit BridgeBurned(account, amount); + function bridgeBurn(address _account, uint256 _amount) external override onlyGateway { + burnFrom(_account, _amount); + emit BridgeBurned(_account, _amount); } } diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index 996f3fc2f..0bc9fba21 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -10,7 +10,7 @@ pragma solidity ^0.7.6; */ interface IReservoir { // Emitted when the issuance rate is updated - event IssuanceRateUpdated(uint256 _newValue); + event IssuanceRateUpdated(uint256 newValue); /** * @dev Approve the RewardsManager to manage the reservoir's token funds @@ -19,17 +19,17 @@ interface IReservoir { /** * @dev Get accumulated total rewards on this layer at a particular block - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return totalRewards Accumulated total rewards on this layer */ - function getAccumulatedRewards(uint256 blocknum) external view returns (uint256 totalRewards); + function getAccumulatedRewards(uint256 _blocknum) external view returns (uint256 totalRewards); /** * @dev Get new total rewards on this layer at a particular block, since the last drip event - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on this layer since the last drip */ - function getNewRewards(uint256 blocknum) external view returns (uint256 deltaRewards); + function getNewRewards(uint256 _blocknum) external view returns (uint256 deltaRewards); } /** diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 98b4f7d4e..02f1973e8 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -22,22 +22,22 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // Emitted when the initial supply snapshot is taken after contract deployment event InitialSnapshotTaken( - uint256 _blockNumber, - uint256 _issuanceBase, - uint256 _mintedPendingRewards + uint256 blockNumber, + uint256 issuanceBase, + uint256 mintedPendingRewards ); // Emitted when an issuance rate update is staged, to be applied on the next drip - event IssuanceRateStaged(uint256 _newValue); + event IssuanceRateStaged(uint256 newValue); // Emitted when an L2 rewards fraction update is staged, to be applied on the next drip - event L2RewardsFractionStaged(uint256 _newValue); + event L2RewardsFractionStaged(uint256 newValue); // Emitted when the L2 rewards fraction is updated (during a drip) - event L2RewardsFractionUpdated(uint256 _newValue); + event L2RewardsFractionUpdated(uint256 newValue); // Emitted when the drip interval is updated - event DripIntervalUpdated(uint256 _newValue); + event DripIntervalUpdated(uint256 newValue); // Emitted when new rewards are dripped and potentially sent to L2 - event RewardsDripped(uint256 _totalMinted, uint256 _sentToL2, uint256 _nextDeadline); + event RewardsDripped(uint256 totalMinted, uint256 sentToL2, uint256 nextDeadline); // Emitted when the address for the L2Reservoir is updated - event L2ReservoirAddressUpdated(address _l2ReservoirAddress); + event L2ReservoirAddressUpdated(address l2ReservoirAddress); /** * @dev Initialize this contract. @@ -113,15 +113,15 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * we will keep an internal accounting only using newly minted rewards. This function * will also mint any pending rewards to cover up to the current block for open allocations, * to be computed off-chain. Can only be called once as it checks that the issuanceBase is zero. - * @param pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function + * @param _pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function */ - function initialSnapshot(uint256 pendingRewards) external onlyGovernor { + function initialSnapshot(uint256 _pendingRewards) external onlyGovernor { require(issuanceBase == 0, "Cannot call this function more than once"); lastRewardsUpdateBlock = block.number; IGraphToken grt = graphToken(); - grt.mint(address(this), pendingRewards); + grt.mint(address(this), _pendingRewards); issuanceBase = grt.totalSupply(); - emit InitialSnapshotTaken(block.number, issuanceBase, pendingRewards); + emit InitialSnapshotTaken(block.number, issuanceBase, _pendingRewards); } /** @@ -138,14 +138,14 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * until the drip amount becomes positive before calling the function again. It can also revert * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we * should send now. - * @param l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 - * @param l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 - * @param l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 */ function drip( - uint256 l2MaxGas, - uint256 l2GasPriceBid, - uint256 l2MaxSubmissionCost + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost ) external payable notPaused { uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); uint256 mintedRewardsActual = getNewGlobalRewards(block.number); @@ -209,17 +209,17 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { emit L2RewardsFractionUpdated(l2RewardsFraction); _sendNewTokensAndStateToL2( tokensToSendToL2, - l2MaxGas, - l2GasPriceBid, - l2MaxSubmissionCost + _l2MaxGas, + _l2GasPriceBid, + _l2MaxSubmissionCost ); } else if (l2RewardsFraction > 0) { tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(TOKEN_DECIMALS); _sendNewTokensAndStateToL2( tokensToSendToL2, - l2MaxGas, - l2GasPriceBid, - l2MaxSubmissionCost + _l2MaxGas, + _l2GasPriceBid, + _l2MaxSubmissionCost ); } else { // Avoid locking funds in this contract if we don't need to @@ -233,15 +233,15 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @dev Snapshot accumulated rewards on this layer * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. * We also update the issuanceBase by adding the new total rewards on both layers. - * @param globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block + * @param _globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block */ - function snapshotAccumulatedRewards(uint256 globalDelta) internal { - issuanceBase = issuanceBase + globalDelta; - // Reimplementation of getAccumulatedRewards but reusing the globalDelta calculated above, + function snapshotAccumulatedRewards(uint256 _globalDelta) internal { + issuanceBase = issuanceBase + _globalDelta; + // Reimplementation of getAccumulatedRewards but reusing the _globalDelta calculated above, // to save gas accumulatedLayerRewards = accumulatedLayerRewards + - globalDelta.mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div(TOKEN_DECIMALS); + _globalDelta.mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div(TOKEN_DECIMALS); lastRewardsUpdateBlock = block.number; } @@ -249,16 +249,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @dev Send new tokens and a message with state to L2 * This function will use the L1GraphTokenGateway to send tokens * to L2, and will also encode a callhook to update state on the L2Reservoir. - * @param nTokens Number of tokens to send to L2 - * @param maxGas Max gas for the L2 retryable ticket execution - * @param gasPriceBid Gas price for the L2 retryable ticket execution - * @param maxSubmissionCost Max submission price for the L2 retryable ticket + * @param _nTokens Number of tokens to send to L2 + * @param _maxGas Max gas for the L2 retryable ticket execution + * @param _gasPriceBid Gas price for the L2 retryable ticket execution + * @param _maxSubmissionCost Max submission price for the L2 retryable ticket */ function _sendNewTokensAndStateToL2( - uint256 nTokens, - uint256 maxGas, - uint256 gasPriceBid, - uint256 maxSubmissionCost + uint256 _nTokens, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost ) internal { uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); bytes memory extraData = abi.encodeWithSelector( @@ -268,16 +268,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { nextDripNonce ); nextDripNonce = nextDripNonce.add(1); - bytes memory data = abi.encode(maxSubmissionCost, extraData); + bytes memory data = abi.encode(_maxSubmissionCost, extraData); IGraphToken grt = graphToken(); ITokenGateway gateway = ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); - grt.approve(address(gateway), nTokens); + grt.approve(address(gateway), _nTokens); gateway.outboundTransfer{ value: msg.value }( address(grt), l2ReservoirAddress, - nTokens, - maxGas, - gasPriceBid, + _nTokens, + _maxGas, + _gasPriceBid, data ); } @@ -288,16 +288,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * - p is the total token supply snapshot at t0 * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on both layers since the last drip */ - function getNewGlobalRewards(uint256 blocknum) public view returns (uint256 deltaRewards) { + function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256 deltaRewards) { uint256 t0 = lastRewardsUpdateBlock; - if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { + if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { return 0; } deltaRewards = issuanceBase - .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) .div(TOKEN_DECIMALS) .sub(issuanceBase); } @@ -307,12 +307,12 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * This is deltaR_L1 = (1-lambda) * deltaR, where: * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on Layer 1 since the last drip */ - function getNewRewards(uint256 blocknum) public view override returns (uint256 deltaRewards) { - deltaRewards = getNewGlobalRewards(blocknum).mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div( - TOKEN_DECIMALS - ); + function getNewRewards(uint256 _blocknum) public view override returns (uint256 deltaRewards) { + deltaRewards = getNewGlobalRewards(_blocknum) + .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) + .div(TOKEN_DECIMALS); } } diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index ba19de015..84576f6d2 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -32,26 +32,26 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir /** * @dev Get accumulated total rewards on this layer at a particular block - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return totalRewards Accumulated total rewards on this layer */ - function getAccumulatedRewards(uint256 blocknum) + function getAccumulatedRewards(uint256 _blocknum) public view override returns (uint256 totalRewards) { // R(t) = R(t0) + (DeltaR(t, t0)) - totalRewards = accumulatedLayerRewards + getNewRewards(blocknum); + totalRewards = accumulatedLayerRewards + getNewRewards(_blocknum); } /** * @dev Get new total rewards on this layer at a particular block, since the last drip event. * Must be implemented by the reservoir on each layer. - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on this layer since the last drip */ - function getNewRewards(uint256 blocknum) + function getNewRewards(uint256 _blocknum) public view virtual @@ -59,63 +59,63 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir returns (uint256 deltaRewards); /** - * @dev Raises x to the power of n with scaling factor of base. + * @dev Raises _x to the power of _n with scaling factor of _base. * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x + * @param _x Base of the exponentiation + * @param _n Exponent + * @param _base Scaling factor + * @return z Exponential of _n with base _x */ function _pow( - uint256 x, - uint256 n, - uint256 base + uint256 _x, + uint256 _n, + uint256 _base ) internal pure returns (uint256 z) { // solhint-disable-next-line no-inline-assembly assembly { - switch x + switch _x case 0 { - switch n + switch _n case 0 { - z := base + z := _base } default { z := 0 } } default { - switch mod(n, 2) + switch mod(_n, 2) case 0 { - z := base + z := _base } default { - z := x + z := _x } - let half := div(base, 2) // for rounding. + let half := div(_base, 2) // for rounding. for { - n := div(n, 2) - } n { - n := div(n, 2) + _n := div(_n, 2) + } _n { + _n := div(_n, 2) } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { + let xx := mul(_x, _x) + if iszero(eq(div(xx, _x), _x)) { revert(0, 0) } let xxRound := add(xx, half) if lt(xxRound, xx) { revert(0, 0) } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { + _x := div(xxRound, _base) + if mod(_n, 2) { + let zx := mul(z, _x) + if and(iszero(iszero(_x)), iszero(eq(div(zx, _x), z))) { revert(0, 0) } let zxRound := add(zx, half) if lt(zxRound, zx) { revert(0, 0) } - z := div(zxRound, base) + z := div(zxRound, _base) } } } diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index ed832a42c..9096631e9 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -397,6 +397,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa graphToken().transferFrom(address(reservoir()), address(staking), rewards); } emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + return rewards; } else { if (rewards > 0) { graphToken().burnFrom(address(reservoir()), rewards); @@ -404,8 +405,6 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); return 0; } - - return rewards; } /** diff --git a/contracts/tests/arbitrum/BridgeMock.sol b/contracts/tests/arbitrum/BridgeMock.sol index 7f18084d6..c9313b99d 100644 --- a/contracts/tests/arbitrum/BridgeMock.sol +++ b/contracts/tests/arbitrum/BridgeMock.sol @@ -21,25 +21,25 @@ contract BridgeMock is IBridge { /** * @dev Deliver a message to the inbox. The encoded message will be * added to the inbox array, and messageIndex will be incremented. - * @param kind Type of the message - * @param sender Address that is sending the message - * @param messageDataHash keccak256 hash of the message data + * @param _kind Type of the message + * @param _sender Address that is sending the message + * @param _messageDataHash keccak256 hash of the message data * @return The next index for the inbox array */ function deliverMessageToInbox( - uint8 kind, - address sender, - bytes32 messageDataHash + uint8 _kind, + address _sender, + bytes32 _messageDataHash ) external payable override returns (uint256) { messageIndex = messageIndex + 1; - inboxAccs.push(keccak256(abi.encodePacked(inbox, kind, sender, messageDataHash))); + inboxAccs.push(keccak256(abi.encodePacked(inbox, _kind, _sender, _messageDataHash))); emit MessageDelivered( messageIndex, inboxAccs[messageIndex - 1], msg.sender, - kind, - sender, - messageDataHash + _kind, + _sender, + _messageDataHash ); return messageIndex; } @@ -47,40 +47,40 @@ contract BridgeMock is IBridge { /** * @dev Executes an L1 function call incoing from L2. This can only be called * by the Outbox. - * @param destAddr Contract to call - * @param amount ETH value to send - * @param data Calldata for the function call + * @param _destAddr Contract to call + * @param _amount ETH value to send + * @param _data Calldata for the function call */ function executeCall( - address destAddr, - uint256 amount, - bytes calldata data + address _destAddr, + uint256 _amount, + bytes calldata _data ) external override returns (bool success, bytes memory returnData) { require(outbox == msg.sender, "NOT_FROM_OUTBOX"); // solhint-disable-next-line avoid-low-level-calls - (success, returnData) = destAddr.call{ value: amount }(data); - emit BridgeCallTriggered(msg.sender, destAddr, amount, data); + (success, returnData) = _destAddr.call{ value: _amount }(_data); + emit BridgeCallTriggered(msg.sender, _destAddr, _amount, _data); } /** * @dev Set the address of the inbox. Anyone can call this, because it's a mock. * @param _inbox Address of the inbox - * @param enabled Enable the inbox (ignored) + * @param _enabled Enable the inbox (ignored) */ - function setInbox(address _inbox, bool enabled) external override { + function setInbox(address _inbox, bool _enabled) external override { inbox = _inbox; - emit InboxToggle(inbox, enabled); + emit InboxToggle(inbox, _enabled); } /** * @dev Set the address of the outbox. Anyone can call this, because it's a mock. * @param _outbox Address of the outbox - * @param enabled Enable the outbox (ignored) + * @param _enabled Enable the outbox (ignored) */ - function setOutbox(address _outbox, bool enabled) external override { + function setOutbox(address _outbox, bool _enabled) external override { outbox = _outbox; - emit OutboxToggle(outbox, enabled); + emit OutboxToggle(outbox, _enabled); } // View functions diff --git a/contracts/tests/arbitrum/InboxMock.sol b/contracts/tests/arbitrum/InboxMock.sol index 940e33bfd..9687488e1 100644 --- a/contracts/tests/arbitrum/InboxMock.sol +++ b/contracts/tests/arbitrum/InboxMock.sol @@ -21,12 +21,12 @@ contract InboxMock is IInbox { /** * @dev Send a message to L2 (by delivering it to the Bridge) - * @param messageData Encoded data to send in the message + * @param _messageData Encoded data to send in the message * @return message number returned by the inbox */ - function sendL2Message(bytes calldata messageData) external override returns (uint256) { - uint256 msgNum = deliverToBridge(L2_MSG, msg.sender, keccak256(messageData)); - emit InboxMessageDelivered(msgNum, messageData); + function sendL2Message(bytes calldata _messageData) external override returns (uint256) { + uint256 msgNum = deliverToBridge(L2_MSG, msg.sender, keccak256(_messageData)); + emit InboxMessageDelivered(msgNum, _messageData); return msgNum; } @@ -93,52 +93,52 @@ contract InboxMock is IInbox { /** * @dev Utility function that converts the address in the L1 that submitted a tx to * the inbox to the msg.sender viewed in the L2 - * @param l1Address the address in the L1 that triggered the tx to L2 + * @param _l1Address the address in the L1 that triggered the tx to L2 * @return l2Address L2 address as viewed in msg.sender */ - function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { - l2Address = address(uint160(l1Address) + OFFSET); + function applyL1ToL2Alias(address _l1Address) internal pure returns (address l2Address) { + l2Address = address(uint160(_l1Address) + OFFSET); } /** * @dev Creates a retryable ticket for an L2 transaction - * @param destAddr Address of the contract to call in L2 - * @param arbTxCallValue Callvalue to use in the L2 transaction - * @param maxSubmissionCost Max cost of submitting the ticket, in Wei - * @param submissionRefundAddress L2 address to refund for any remaining value from the submission cost - * @param valueRefundAddress L2 address to refund if the ticket times out or gets cancelled - * @param maxGas Max gas for the L2 transcation - * @param gasPriceBid Gas price bid on L2 - * @param data Encoded calldata for the L2 transaction (including function selector) + * @param _destAddr Address of the contract to call in L2 + * @param _arbTxCallValue Callvalue to use in the L2 transaction + * @param _maxSubmissionCost Max cost of submitting the ticket, in Wei + * @param _submissionRefundAddress L2 address to refund for any remaining value from the submission cost + * @param _valueRefundAddress L2 address to refund if the ticket times out or gets cancelled + * @param _maxGas Max gas for the L2 transcation + * @param _gasPriceBid Gas price bid on L2 + * @param _data Encoded calldata for the L2 transaction (including function selector) * @return message number returned by the bridge */ function createRetryableTicket( - address destAddr, - uint256 arbTxCallValue, - uint256 maxSubmissionCost, - address submissionRefundAddress, - address valueRefundAddress, - uint256 maxGas, - uint256 gasPriceBid, - bytes calldata data + address _destAddr, + uint256 _arbTxCallValue, + uint256 _maxSubmissionCost, + address _submissionRefundAddress, + address _valueRefundAddress, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data ) external payable override returns (uint256) { - submissionRefundAddress = applyL1ToL2Alias(submissionRefundAddress); - valueRefundAddress = applyL1ToL2Alias(valueRefundAddress); + _submissionRefundAddress = applyL1ToL2Alias(_submissionRefundAddress); + _valueRefundAddress = applyL1ToL2Alias(_valueRefundAddress); return _deliverMessage( L1MessageType_submitRetryableTx, msg.sender, abi.encodePacked( - uint256(uint160(bytes20(destAddr))), - arbTxCallValue, + uint256(uint160(bytes20(_destAddr))), + _arbTxCallValue, msg.value, - maxSubmissionCost, - uint256(uint160(bytes20(submissionRefundAddress))), - uint256(uint160(bytes20(valueRefundAddress))), - maxGas, - gasPriceBid, - data.length, - data + _maxSubmissionCost, + uint256(uint160(bytes20(_submissionRefundAddress))), + uint256(uint160(bytes20(_valueRefundAddress))), + _maxGas, + _gasPriceBid, + _data.length, + _data ) ); } @@ -194,16 +194,16 @@ contract InboxMock is IInbox { /** * @dev Deliver a message to the bridge - * @param kind Type of the message - * @param sender Address that is sending the message - * @param messageDataHash keccak256 hash of the encoded message data + * @param _kind Type of the message + * @param _sender Address that is sending the message + * @param _messageDataHash keccak256 hash of the encoded message data * @return Message number returned by the bridge */ function deliverToBridge( - uint8 kind, - address sender, - bytes32 messageDataHash + uint8 _kind, + address _sender, + bytes32 _messageDataHash ) internal returns (uint256) { - return bridge.deliverMessageToInbox{ value: msg.value }(kind, sender, messageDataHash); + return bridge.deliverMessageToInbox{ value: msg.value }(_kind, _sender, _messageDataHash); } } diff --git a/contracts/tests/arbitrum/OutboxMock.sol b/contracts/tests/arbitrum/OutboxMock.sol index af16bdfdb..a529a975a 100644 --- a/contracts/tests/arbitrum/OutboxMock.sol +++ b/contracts/tests/arbitrum/OutboxMock.sol @@ -94,54 +94,54 @@ contract OutboxMock is IOutbox { * @notice (Mock) Executes a messages in an Outbox entry. * @dev This mocks what has to be called when finalizing an L2 to L1 transfer. * In our mock scenario, we don't validate and execute unconditionally. - * @param batchNum Index of OutboxEntry in outboxEntries array - * @param l2Sender sender of original message (i.e., caller of ArbSys.sendTxToL1) - * @param destAddr destination address for L1 contract call - * @param l2Block l2 block number at which sendTxToL1 call was made - * @param l1Block l1 block number at which sendTxToL1 call was made - * @param l2Timestamp l2 Timestamp at which sendTxToL1 call was made - * @param amount value in L1 message in wei - * @param calldataForL1 abi-encoded L1 message data + * @param _batchNum Index of OutboxEntry in outboxEntries array + * @param _l2Sender sender of original message (i.e., caller of ArbSys.sendTxToL1) + * @param _destAddr destination address for L1 contract call + * @param _l2Block l2 block number at which sendTxToL1 call was made + * @param _l1Block l1 block number at which sendTxToL1 call was made + * @param _l2Timestamp l2 Timestamp at which sendTxToL1 call was made + * @param _amount value in L1 message in wei + * @param _calldataForL1 abi-encoded L1 message data */ function executeTransaction( - uint256 batchNum, + uint256 _batchNum, bytes32[] calldata, // proof uint256, // index - address l2Sender, - address destAddr, - uint256 l2Block, - uint256 l1Block, - uint256 l2Timestamp, - uint256 amount, - bytes calldata calldataForL1 + address _l2Sender, + address _destAddr, + uint256 _l2Block, + uint256 _l1Block, + uint256 _l2Timestamp, + uint256 _amount, + bytes calldata _calldataForL1 ) external virtual { bytes32 outputId; context = L2ToL1Context({ - sender: l2Sender, - l2Block: uint128(l2Block), - l1Block: uint128(l1Block), - timestamp: uint128(l2Timestamp), - batchNum: uint128(batchNum), + sender: _l2Sender, + l2Block: uint128(_l2Block), + l1Block: uint128(_l1Block), + timestamp: uint128(_l2Timestamp), + batchNum: uint128(_batchNum), outputId: outputId }); // set and reset vars around execution so they remain valid during call - executeBridgeCall(destAddr, amount, calldataForL1); + executeBridgeCall(_destAddr, _amount, _calldataForL1); } /** * @dev Execute an L2-to-L1 function call by calling the bridge - * @param destAddr Address of the contract to call - * @param amount Callvalue for the function call - * @param data Calldata for the function call + * @param _destAddr Address of the contract to call + * @param _amount Callvalue for the function call + * @param _data Calldata for the function call */ function executeBridgeCall( - address destAddr, - uint256 amount, - bytes memory data + address _destAddr, + uint256 _amount, + bytes memory _data ) internal { - (bool success, bytes memory returndata) = bridge.executeCall(destAddr, amount, data); + (bool success, bytes memory returndata) = bridge.executeCall(_destAddr, _amount, _data); if (!success) { if (returndata.length > 0) { // solhint-disable-next-line no-inline-assembly From 14ada453964280d5ce47b6fef20457d15283981b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 15:32:59 +0200 Subject: [PATCH 29/78] fix: add some missing parameters in docstrings [N-06] --- contracts/l2/token/L2GraphToken.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index e20cd0e29..51f434141 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -152,6 +152,7 @@ contract GraphTokenUpgradeable is /** * @dev Graph Token Contract initializer. + * @param _owner Owner of this contract, who will hold the initial supply and will be a minter * @param _initialSupply Initial supply of GRT */ function _initialize(address _owner, uint256 _initialSupply) internal { @@ -252,6 +253,7 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { /** * @dev Sets the address of the L2 gateway allowed to mint tokens + * @param _gw Address for the L2GraphTokenGateway that will be allowed to mint tokens */ function setGateway(address _gw) external onlyGovernor { require(_gw != address(0), "INVALID_GATEWAY"); @@ -261,6 +263,7 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { /** * @dev Sets the address of the counterpart token on L1 + * @param _addr Address for the GraphToken contract on L1 */ function setL1Address(address _addr) external onlyGovernor { require(_addr != address(0), "INVALID_L1_ADDRESS"); From 8742cc85f2a02eb13c512fd6bdca70d4f969cff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 15:39:35 +0200 Subject: [PATCH 30/78] fix: use internal function to consistently set dripInterval [N-07] --- contracts/reservoir/L1Reservoir.sol | 88 ++++++++++++++++------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 02f1973e8..8115212e7 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -47,7 +47,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { */ function initialize(address _controller, uint256 _dripInterval) external onlyImpl { Managed._initialize(_controller); - dripInterval = _dripInterval; + _setDripInterval(_dripInterval); } /** @@ -59,9 +59,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _dripInterval The new interval in blocks for which drip() will mint rewards */ function setDripInterval(uint256 _dripInterval) external onlyGovernor { - require(_dripInterval > 0, "Drip interval must be > 0"); - dripInterval = _dripInterval; - emit DripIntervalUpdated(_dripInterval); + _setDripInterval(_dripInterval); } /** @@ -229,6 +227,54 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { emit RewardsDripped(tokensToMint, tokensToSendToL2, rewardsMintedUntilBlock); } + /** + * @dev Get new total rewards on both layers at a particular block, since the last drip event + * This is deltaR = p * r ^ (blocknum - t0) - p, where: + * - p is the total token supply snapshot at t0 + * - t0 is the last drip block, i.e. lastRewardsUpdateBlock + * - r is the issuanceRate + * @param _blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on both layers since the last drip + */ + function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256 deltaRewards) { + uint256 t0 = lastRewardsUpdateBlock; + if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { + return 0; + } + deltaRewards = issuanceBase + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(issuanceBase); + } + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event + * This is deltaR_L1 = (1-lambda) * deltaR, where: + * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) + * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction + * @param _blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on Layer 1 since the last drip + */ + function getNewRewards(uint256 _blocknum) public view override returns (uint256 deltaRewards) { + deltaRewards = getNewGlobalRewards(_blocknum) + .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) + .div(TOKEN_DECIMALS); + } + + /** + * @dev Sets the drip interval (internal). + * This is the time in the future (in blocks) for which drip() will mint rewards. + * Keep in mind that changing this value will require manually re-adjusting + * the reservoir's token balance, because the first call to drip might produce + * more or less tokens than needed. + * @param _dripInterval The new interval in blocks for which drip() will mint rewards + */ + function _setDripInterval(uint256 _dripInterval) internal { + require(_dripInterval > 0, "Drip interval must be > 0"); + dripInterval = _dripInterval; + emit DripIntervalUpdated(_dripInterval); + } + /** * @dev Snapshot accumulated rewards on this layer * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. @@ -281,38 +327,4 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { data ); } - - /** - * @dev Get new total rewards on both layers at a particular block, since the last drip event - * This is deltaR = p * r ^ (blocknum - t0) - p, where: - * - p is the total token supply snapshot at t0 - * - t0 is the last drip block, i.e. lastRewardsUpdateBlock - * - r is the issuanceRate - * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on both layers since the last drip - */ - function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256 deltaRewards) { - uint256 t0 = lastRewardsUpdateBlock; - if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { - return 0; - } - deltaRewards = issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) - .sub(issuanceBase); - } - - /** - * @dev Get new total rewards on this layer at a particular block, since the last drip event - * This is deltaR_L1 = (1-lambda) * deltaR, where: - * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) - * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction - * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on Layer 1 since the last drip - */ - function getNewRewards(uint256 _blocknum) public view override returns (uint256 deltaRewards) { - deltaRewards = getNewGlobalRewards(_blocknum) - .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) - .div(TOKEN_DECIMALS); - } } From 3002d3e07bb7636f8e33d0520c23255da29e1084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 16:01:58 +0200 Subject: [PATCH 31/78] fix: remove named returns [N-08] --- contracts/gateway/L1GraphTokenGateway.sol | 37 +++++++++++-------- contracts/l2/gateway/L2GraphTokenGateway.sol | 39 ++++++++++---------- contracts/l2/reservoir/L2Reservoir.sol | 13 ++++--- contracts/reservoir/IReservoir.sol | 8 ++-- contracts/reservoir/L1Reservoir.sol | 24 ++++++------ contracts/reservoir/Reservoir.sol | 26 +++++-------- contracts/tests/arbitrum/BridgeMock.sol | 7 +++- contracts/tests/arbitrum/InboxMock.sol | 6 +-- 8 files changed, 83 insertions(+), 77 deletions(-) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index 52a86bdf1..d60b1cb60 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -265,19 +265,22 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @dev data must include maxSubmissionCost, extraData can be left empty. When the router * sends an outbound message, data also contains the from address. * @param _data encoded callhook data - * @return from sender of the tx - * @return maxSubmissionCost base ether value required to keep retyrable ticket alive - * @return extraData any other data sent to L2 + * @return Sender of the tx + * @return Base ether value required to keep retryable ticket alive + * @return Additional data sent to L2 */ function parseOutboundData(bytes memory _data) private view returns ( - address from, - uint256 maxSubmissionCost, - bytes memory extraData + address, + uint256, + bytes memory ) { + address from; + uint256 maxSubmissionCost; + bytes memory extraData; if (msg.sender == l1Router) { // Data encoded by the Gateway Router includes the sender address (from, extraData) = abi.decode(_data, (address, bytes)); @@ -288,6 +291,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { // User-encoded data contains the max retryable ticket submission cost // and additional L2 calldata (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); + return (from, maxSubmissionCost, extraData); } /** @@ -299,7 +303,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param _to Address on L2 to which we're transferring tokens * @param _amount Amount of GRT to transfer * @param _data Additional call data for the L2 transaction, which must be empty - * @return outboundCalldata Encoded calldata (including function selector) for the L2 transaction + * @return Encoded calldata (including function selector) for the L2 transaction */ function getOutboundCalldata( address _l1Token, @@ -307,17 +311,18 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { address _to, uint256 _amount, bytes memory _data - ) public pure returns (bytes memory outboundCalldata) { + ) public pure returns (bytes memory) { bytes memory emptyBytes; - outboundCalldata = abi.encodeWithSelector( - ITokenGateway.finalizeInboundTransfer.selector, - _l1Token, - _from, - _to, - _amount, - abi.encode(emptyBytes, _data) - ); + return + abi.encodeWithSelector( + ITokenGateway.finalizeInboundTransfer.selector, + _l1Token, + _from, + _to, + _amount, + abi.encode(emptyBytes, _data) + ); } /** diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 3df59bb61..a4471e929 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -244,6 +244,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @param _to Address to which we're sending tokens on L1 * @param _amount Amount of GRT to transfer * @param _data Additional calldata for the transaction + * @return Calldata for a transaction sent to L1 */ function getOutboundCalldata( address _token, @@ -251,35 +252,35 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { address _to, uint256 _amount, bytes memory _data - ) public pure returns (bytes memory outboundCalldata) { - outboundCalldata = abi.encodeWithSelector( - ITokenGateway.finalizeInboundTransfer.selector, - _token, - _from, - _to, - _amount, - abi.encode(0, _data) // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 - ); + ) public pure returns (bytes memory) { + return + abi.encodeWithSelector( + ITokenGateway.finalizeInboundTransfer.selector, + _token, + _from, + _to, + _amount, + abi.encode(0, _data) // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 + ); } /** * @notice Decodes calldata required for migration of tokens * @dev extraData can be left empty * @param _data Encoded callhook data - * @return from Sender of the tx - * @return extraData Any other data sent to L1 + * @return Sender of the tx + * @return Any other data sent to L1 */ - function parseOutboundData(bytes memory _data) - private - view - returns (address from, bytes memory extraData) - { + function parseOutboundData(bytes memory _data) private view returns (address, bytes memory) { + address from; + bytes memory extraData; if (msg.sender == l2Router) { (from, extraData) = abi.decode(_data, (address, bytes)); } else { from = msg.sender; extraData = _data; } + return (from, extraData); } /** @@ -287,9 +288,9 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @dev The Arbitrum bridge adds an offset to addresses when sending messages, * so we need to apply it to check any L1 address from a message in L2 * @param _l1Address The L1 address - * @return _l2Address the L2 alias of _l1Address + * @return The L2 alias of _l1Address */ - function l1ToL2Alias(address _l1Address) internal pure returns (address _l2Address) { - _l2Address = address(uint160(_l1Address) + L2_ADDRESS_OFFSET); + function l1ToL2Alias(address _l1Address) internal pure returns (address) { + return address(uint160(_l1Address) + L2_ADDRESS_OFFSET); } } diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 5619b7abd..2a0673ba2 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -55,22 +55,23 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on L2 since the last drip + * @return New total rewards on L2 since the last drip */ function getNewRewards(uint256 _blocknum) public view override(Reservoir, IReservoir) - returns (uint256 deltaRewards) + returns (uint256) { uint256 t0 = lastRewardsUpdateBlock; if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { return 0; } - deltaRewards = issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) - .sub(issuanceBase); + return + issuanceBase + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(issuanceBase); } /** diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index 0bc9fba21..de25a2b80 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -20,16 +20,16 @@ interface IReservoir { /** * @dev Get accumulated total rewards on this layer at a particular block * @param _blocknum Block number at which to calculate rewards - * @return totalRewards Accumulated total rewards on this layer + * @return Accumulated total rewards on this layer */ - function getAccumulatedRewards(uint256 _blocknum) external view returns (uint256 totalRewards); + function getAccumulatedRewards(uint256 _blocknum) external view returns (uint256); /** * @dev Get new total rewards on this layer at a particular block, since the last drip event * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on this layer since the last drip + * @return New total rewards on this layer since the last drip */ - function getNewRewards(uint256 _blocknum) external view returns (uint256 deltaRewards); + function getNewRewards(uint256 _blocknum) external view returns (uint256); } /** diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 8115212e7..b6a821dff 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -234,17 +234,18 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on both layers since the last drip + * @return New total rewards on both layers since the last drip */ - function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256 deltaRewards) { + function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256) { uint256 t0 = lastRewardsUpdateBlock; if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { return 0; } - deltaRewards = issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) - .sub(issuanceBase); + return + issuanceBase + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(issuanceBase); } /** @@ -253,12 +254,13 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on Layer 1 since the last drip + * @return New total rewards on Layer 1 since the last drip */ - function getNewRewards(uint256 _blocknum) public view override returns (uint256 deltaRewards) { - deltaRewards = getNewGlobalRewards(_blocknum) - .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) - .div(TOKEN_DECIMALS); + function getNewRewards(uint256 _blocknum) public view override returns (uint256) { + return + getNewGlobalRewards(_blocknum).mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div( + TOKEN_DECIMALS + ); } /** diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index 84576f6d2..048697bc9 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -33,30 +33,20 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir /** * @dev Get accumulated total rewards on this layer at a particular block * @param _blocknum Block number at which to calculate rewards - * @return totalRewards Accumulated total rewards on this layer + * @return Accumulated total rewards on this layer */ - function getAccumulatedRewards(uint256 _blocknum) - public - view - override - returns (uint256 totalRewards) - { + function getAccumulatedRewards(uint256 _blocknum) public view override returns (uint256) { // R(t) = R(t0) + (DeltaR(t, t0)) - totalRewards = accumulatedLayerRewards + getNewRewards(_blocknum); + return accumulatedLayerRewards + getNewRewards(_blocknum); } /** * @dev Get new total rewards on this layer at a particular block, since the last drip event. * Must be implemented by the reservoir on each layer. * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on this layer since the last drip + * @return New total rewards on this layer since the last drip */ - function getNewRewards(uint256 _blocknum) - public - view - virtual - override - returns (uint256 deltaRewards); + function getNewRewards(uint256 _blocknum) public view virtual override returns (uint256); /** * @dev Raises _x to the power of _n with scaling factor of _base. @@ -64,13 +54,14 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir * @param _x Base of the exponentiation * @param _n Exponent * @param _base Scaling factor - * @return z Exponential of _n with base _x + * @return Exponential of _n with base _x */ function _pow( uint256 _x, uint256 _n, uint256 _base - ) internal pure returns (uint256 z) { + ) internal pure returns (uint256) { + uint256 z; // solhint-disable-next-line no-inline-assembly assembly { switch _x @@ -120,5 +111,6 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir } } } + return z; } } diff --git a/contracts/tests/arbitrum/BridgeMock.sol b/contracts/tests/arbitrum/BridgeMock.sol index c9313b99d..4f2848288 100644 --- a/contracts/tests/arbitrum/BridgeMock.sol +++ b/contracts/tests/arbitrum/BridgeMock.sol @@ -50,17 +50,22 @@ contract BridgeMock is IBridge { * @param _destAddr Contract to call * @param _amount ETH value to send * @param _data Calldata for the function call + * @return True if the call was successful, false otherwise + * @return Return data from the call */ function executeCall( address _destAddr, uint256 _amount, bytes calldata _data - ) external override returns (bool success, bytes memory returnData) { + ) external override returns (bool, bytes memory) { require(outbox == msg.sender, "NOT_FROM_OUTBOX"); + bool success; + bytes memory returnData; // solhint-disable-next-line avoid-low-level-calls (success, returnData) = _destAddr.call{ value: _amount }(_data); emit BridgeCallTriggered(msg.sender, _destAddr, _amount, _data); + return (success, returnData); } /** diff --git a/contracts/tests/arbitrum/InboxMock.sol b/contracts/tests/arbitrum/InboxMock.sol index 9687488e1..4033ae3e5 100644 --- a/contracts/tests/arbitrum/InboxMock.sol +++ b/contracts/tests/arbitrum/InboxMock.sol @@ -94,10 +94,10 @@ contract InboxMock is IInbox { * @dev Utility function that converts the address in the L1 that submitted a tx to * the inbox to the msg.sender viewed in the L2 * @param _l1Address the address in the L1 that triggered the tx to L2 - * @return l2Address L2 address as viewed in msg.sender + * @return L2 address as viewed in msg.sender */ - function applyL1ToL2Alias(address _l1Address) internal pure returns (address l2Address) { - l2Address = address(uint160(_l1Address) + OFFSET); + function applyL1ToL2Alias(address _l1Address) internal pure returns (address) { + return address(uint160(_l1Address) + OFFSET); } /** From 5c1f8773be384304a603b9591dd3e88f4273b6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 16:13:51 +0200 Subject: [PATCH 32/78] fix: update some outdated docstrings and comments [N-09] --- contracts/gateway/L1GraphTokenGateway.sol | 4 ++-- contracts/l2/gateway/L2GraphTokenGateway.sol | 5 ++--- contracts/reservoir/L1Reservoir.sol | 2 +- contracts/rewards/RewardsManager.sol | 1 + 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index d60b1cb60..58d0b8913 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -31,7 +31,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { address public l2Counterpart; // Address of the BridgeEscrow contract that holds the GRT in the bridge address public escrow; - // Address of the L1 Reservoir that is the only sender allowed to send extra data + // Addresses for which this mapping is true are allowed to send callhooks in outbound transfers mapping(address => bool) public callhookWhitelist; // Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 @@ -302,7 +302,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param _from Address on L1 from which we're transferring tokens * @param _to Address on L2 to which we're transferring tokens * @param _amount Amount of GRT to transfer - * @param _data Additional call data for the L2 transaction, which must be empty + * @param _data Additional call data for the L2 transaction, which must be empty unless the caller is whitelisted * @return Encoded calldata (including function selector) for the L2 transaction */ function getOutboundCalldata( diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index a4471e929..51888a8ad 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -122,7 +122,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) * @param _to Recipient address on L1 * @param _amount Amount of tokens to burn - * @param _data Contains sender and additional data (always zero) to send to L1 + * @param _data Contains sender and additional data (always empty) to send to L1 * @return ID of the withdraw transaction */ function outboundTransfer( @@ -194,8 +194,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { /** * @notice Receives token amount from L1 and mints the equivalent tokens to the receiving address - * @dev Only accepts transactions from the L1 GRT Gateway - * data param is unused because no additional data is allowed from L1. + * @dev Only accepts transactions from the L1 GRT Gateway. * The function is payable for ITokenGateway compatibility, but msg.value must be zero. * @param _l1Token L1 Address of GRT * @param _from Address of the sender on L1 diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index b6a821dff..c3a992b99 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -82,7 +82,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { /** * @dev Sets the L2 rewards fraction. * This is the portion of the indexer rewards that are sent to L2. - * The value is in fixed point at 1e18 and must be less than 1. + * The value is in fixed point at 1e18 and must be less than or equal to 1. * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure * good synchronization between L1 and L2. diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 9096631e9..f374e2a07 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -275,6 +275,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa /** * @dev Updates the accumulated rewards per signal and save checkpoint block number. + * Also snapshots total accumulated rewards (`accRewardsOnLastSignalUpdate`). * Must be called before `issuanceRate` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() * @return Accumulated rewards per signal From bd236162e1b136895ee7c721235817984f488b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 18:46:45 +0200 Subject: [PATCH 33/78] fix: document why some variables are not set during initialization [N-10] --- contracts/gateway/L1GraphTokenGateway.sol | 8 ++++++++ contracts/l2/gateway/L2GraphTokenGateway.sol | 6 ++++++ contracts/l2/reservoir/L2Reservoir.sol | 5 ++++- contracts/l2/token/L2GraphToken.sol | 4 ++++ contracts/reservoir/L1Reservoir.sol | 8 ++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index 58d0b8913..e9bb18897 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -84,6 +84,14 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { /** * @dev Initialize this contract. * The contract will be paused. + * Note some parameters have to be set separately as they are generally + * not expected to be available at initialization time: + * - inbox and l1Router using setArbitrumAddresses + * - l2GRT using setL2TokenAddress + * - l2Counterpart using setL2CounterpartAddress + * - escrow using setEscrowAddress + * - whitelisted callhook callers using addToCallhookWhitelist + * - pauseGuardian using setPauseGuardian * @param _controller Address of the Controller that manages this contract */ function initialize(address _controller) external onlyImpl { diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 51888a8ad..17791d8ae 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -74,6 +74,12 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { /** * @dev Initialize this contract. * The contract will be paused. + * Note some parameters have to be set separately as they are generally + * not expected to be available at initialization time: + * - l2Router using setL2Router + * - l1GRT using setL1TokenAddress + * - l1Counterpart using setL1CounterpartAddress + * - pauseGuardian using setPauseGuardian * @param _controller Address of the Controller that manages this contract */ function initialize(address _controller) external onlyImpl { diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 2a0673ba2..224d7fec4 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -31,7 +31,10 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { /** * @dev Initialize this contract. - * The contract will be paused. + * The contract will be paused. Note that issuance parameters + * are not set here because they are set from L1 through the drip function. + * The RewardsManager's address might also not be available in the controller at initialization + * time, so approveRewardsManager() must be called separately. * @param _controller Address of the Controller that manages this contract */ function initialize(address _controller) external onlyImpl { diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index 51f434141..dda740c79 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -242,6 +242,10 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { /** * @dev L2 Graph Token Contract initializer. + * Note some parameters have to be set separately as they are generally + * not expected to be available at initialization time: + * - gateway using setGateway + * - l1Address using setL1Address * @param _owner Governance address that owns this contract */ function initialize(address _owner) external onlyImpl { diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index c3a992b99..55835269d 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -42,6 +42,14 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { /** * @dev Initialize this contract. * The contract will be paused. + * Note that the contract is designed to not accrue rewards until the first call + * to the drip function, that also requires the initial supply snapshot to be taken + * using initialSnapshot. For this reason, issuanceRate and l2RewardsFraction + * are not initialized here and instead need a call to setIssuanceRate and setL2RewardsFraction. + * On the other hand, the l2ReservoirAddress is not expected to be known at initialization + * time and must therefore be set using setL2ReservoirAddress. + * The RewardsManager's address might also not be available in the controller at initialization + * time, so approveRewardsManager() must be called separately as well. * @param _controller Address of the Controller that manages this contract * @param _dripInterval Drip interval, i.e. time period for which rewards are minted each time we drip */ From 0f5fc9bd113202b181e56606d5852d6650a81fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 18:57:45 +0200 Subject: [PATCH 34/78] fix: add missing getters to Managed [N-11] --- contracts/governance/Managed.sol | 18 ++++++++++++++++++ contracts/l2/gateway/L2GraphTokenGateway.sol | 2 +- contracts/l2/reservoir/L2Reservoir.sol | 2 +- contracts/reservoir/L1Reservoir.sol | 2 +- contracts/rewards/RewardsManager.sol | 4 ---- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index ae220a9e0..20a9191e5 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -9,6 +9,8 @@ import "../epochs/IEpochManager.sol"; import "../rewards/IRewardsManager.sol"; import "../staking/IStaking.sol"; import "../token/IGraphToken.sol"; +import "../arbitrum/ITokenGateway.sol"; +import "../reservoir/IReservoir.sol"; /** * @title Graph Managed contract @@ -145,6 +147,22 @@ contract Managed { return IGraphToken(_resolveContract(keccak256("GraphToken"))); } + /** + * @dev Return GraphTokenGateway (L1 or L2) interface. + * @return Graph token gateway contract registered with Controller + */ + function graphTokenGateway() internal view returns (ITokenGateway) { + return ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); + } + + /** + * @dev Return Reservoir (L1 or L2) interface. + * @return Reservoir contract registered with Controller + */ + function reservoir() internal view returns (IReservoir) { + return IReservoir(_resolveContract(keccak256("Reservoir"))); + } + /** * @dev Resolve a contract address from the cache or the Controller if not found. * @return Address of the contract diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 17791d8ae..15359cf21 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -195,7 +195,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { if (l1ERC20 != l1GRT) { return address(0); } - return Managed._resolveContract(keccak256("GraphToken")); + return address(graphToken()); } /** diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 224d7fec4..a9a410abf 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -25,7 +25,7 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. */ modifier onlyL2Gateway() { - require(msg.sender == _resolveContract(keccak256("GraphTokenGateway")), "ONLY_GATEWAY"); + require(msg.sender == address(graphTokenGateway()), "ONLY_GATEWAY"); _; } diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 55835269d..0215e07ea 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -326,7 +326,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { nextDripNonce = nextDripNonce.add(1); bytes memory data = abi.encode(_maxSubmissionCost, extraData); IGraphToken grt = graphToken(); - ITokenGateway gateway = ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); + ITokenGateway gateway = graphTokenGateway(); grt.approve(address(gateway), _nTokens); gateway.outboundTransfer{ value: msg.value }( address(grt), diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index f374e2a07..0b33b8d3b 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -435,8 +435,4 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa emit RewardsBurned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); } } - - function reservoir() internal view returns (IReservoir) { - return IReservoir(_resolveContract(keccak256("Reservoir"))); - } } From 46a8444b1574addfcd84c74316f8a6d17fdc3554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 19:06:35 +0200 Subject: [PATCH 35/78] fix: use Arbitrum's AddressAliasHelper instead of reimplementing it [N-12] --- contracts/arbitrum/AddressAliasHelper.sol | 46 ++++++++++++++++++++ contracts/l2/gateway/L2GraphTokenGateway.sol | 21 +++------ contracts/tests/arbitrum/InboxMock.sol | 17 ++------ 3 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 contracts/arbitrum/AddressAliasHelper.sol diff --git a/contracts/arbitrum/AddressAliasHelper.sol b/contracts/arbitrum/AddressAliasHelper.sol new file mode 100644 index 000000000..740b70361 --- /dev/null +++ b/contracts/arbitrum/AddressAliasHelper.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2019-2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/84e64dee6ee82adbf8ec34fd4b86c207a61d9007/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +library AddressAliasHelper { + uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); + + /// @notice Utility function that converts the address in the L1 that submitted a tx to + /// the inbox to the msg.sender viewed in the L2 + /// @param l1Address the address in the L1 that triggered the tx to L2 + /// @return l2Address L2 address as viewed in msg.sender + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + l2Address = address(uint160(l1Address) + offset); + } + + /// @notice Utility function that converts the msg.sender viewed in the L2 to the + /// address in the L1 that submitted a tx to the inbox + /// @param l2Address L2 address as viewed in msg.sender + /// @return l1Address the address in the L1 that triggered the tx to L2 + function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { + l1Address = address(uint160(l2Address) - offset); + } +} diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 15359cf21..89943d010 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -6,6 +6,7 @@ pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; import "../../arbitrum/L2ArbitrumMessenger.sol"; +import "../../arbitrum/AddressAliasHelper.sol"; import "../../gateway/GraphTokenGateway.sol"; import "../token/L2GraphToken.sol"; @@ -20,10 +21,6 @@ import "../token/L2GraphToken.sol"; contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { using SafeMath for uint256; - // Offset applied by the bridge to L1 addresses sending messages to L2 - uint160 internal constant L2_ADDRESS_OFFSET = - uint160(0x1111000000000000000000000000000000001111); - // Address of the Graph Token contract on L1 address public l1GRT; // Address of the L1GraphTokenGateway that is the counterpart of this gateway on L1 @@ -67,7 +64,10 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * gateway on L1. */ modifier onlyL1Counterpart() { - require(msg.sender == l1ToL2Alias(l1Counterpart), "ONLY_COUNTERPART_GATEWAY"); + require( + msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Counterpart), + "ONLY_COUNTERPART_GATEWAY" + ); _; } @@ -287,15 +287,4 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { } return (from, extraData); } - - /** - * @notice Converts L1 address to its L2 alias used when sending messages - * @dev The Arbitrum bridge adds an offset to addresses when sending messages, - * so we need to apply it to check any L1 address from a message in L2 - * @param _l1Address The L1 address - * @return The L2 alias of _l1Address - */ - function l1ToL2Alias(address _l1Address) internal pure returns (address) { - return address(uint160(_l1Address) + L2_ADDRESS_OFFSET); - } } diff --git a/contracts/tests/arbitrum/InboxMock.sol b/contracts/tests/arbitrum/InboxMock.sol index 4033ae3e5..b600ec3ac 100644 --- a/contracts/tests/arbitrum/InboxMock.sol +++ b/contracts/tests/arbitrum/InboxMock.sol @@ -3,14 +3,13 @@ pragma solidity ^0.7.6; import "../../arbitrum/IInbox.sol"; +import "../../arbitrum/AddressAliasHelper.sol"; /** * @title Arbitrum Inbox mock contract * @dev This contract implements (a subset of) Arbitrum's IInbox interface for testing purposes */ contract InboxMock is IInbox { - // Offset used when calculating the L2 alias of an L1 address - uint160 internal constant OFFSET = uint160(0x1111000000000000000000000000000000001111); // Type indicator for a standard L2 message uint8 internal constant L2_MSG = 3; // Type indicator for a retryable ticket message @@ -90,16 +89,6 @@ contract InboxMock is IInbox { revert("Unimplemented"); } - /** - * @dev Utility function that converts the address in the L1 that submitted a tx to - * the inbox to the msg.sender viewed in the L2 - * @param _l1Address the address in the L1 that triggered the tx to L2 - * @return L2 address as viewed in msg.sender - */ - function applyL1ToL2Alias(address _l1Address) internal pure returns (address) { - return address(uint160(_l1Address) + OFFSET); - } - /** * @dev Creates a retryable ticket for an L2 transaction * @param _destAddr Address of the contract to call in L2 @@ -122,8 +111,8 @@ contract InboxMock is IInbox { uint256 _gasPriceBid, bytes calldata _data ) external payable override returns (uint256) { - _submissionRefundAddress = applyL1ToL2Alias(_submissionRefundAddress); - _valueRefundAddress = applyL1ToL2Alias(_valueRefundAddress); + _submissionRefundAddress = AddressAliasHelper.applyL1ToL2Alias(_submissionRefundAddress); + _valueRefundAddress = AddressAliasHelper.applyL1ToL2Alias(_valueRefundAddress); return _deliverMessage( L1MessageType_submitRetryableTx, From 505a2040378d3f20c59a922c118c6df712631602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 12:45:34 +0200 Subject: [PATCH 36/78] fix: separate contracts into different files [N-13] --- contracts/l2/reservoir/IL2Reservoir.sol | 30 +++ contracts/l2/reservoir/L2Reservoir.sol | 1 + contracts/l2/token/GraphTokenUpgradeable.sol | 210 +++++++++++++++++++ contracts/l2/token/L2GraphToken.sol | 205 +----------------- contracts/reservoir/IReservoir.sol | 25 --- contracts/reservoir/L1Reservoir.sol | 2 +- 6 files changed, 243 insertions(+), 230 deletions(-) create mode 100644 contracts/l2/reservoir/IL2Reservoir.sol create mode 100644 contracts/l2/token/GraphTokenUpgradeable.sol diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol new file mode 100644 index 000000000..90b089ac9 --- /dev/null +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../reservoir/IReservoir.sol"; + +/** + * @title Interface for the L2 Rewards Reservoir + * @dev This exposes a specific function for the L2Reservoir that is called + * as a callhook from L1 to L2, so that state can be updated when dripped rewards + * are bridged between layers. + */ +interface IL2Reservoir is IReservoir { + /** + * @dev Receive dripped tokens from L1. + * This function can only be called by the gateway, as it is + * meant to be a callhook when receiving tokens from L1. It + * updates the issuanceBase and issuanceRate, + * and snapshots the accumulated rewards. If issuanceRate changes, + * it also triggers a snapshot of rewards per signal on the RewardsManager. + * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) + * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 + * @param _nonce Incrementing nonce to ensure messages are received in order + */ + function receiveDrip( + uint256 _issuanceBase, + uint256 _issuanceRate, + uint256 _nonce + ) external; +} diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index a9a410abf..67376648a 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/math/SafeMath.sol"; import "../../reservoir/IReservoir.sol"; import "../../reservoir/Reservoir.sol"; +import "./IL2Reservoir.sol"; import "./L2ReservoirStorage.sol"; /** diff --git a/contracts/l2/token/GraphTokenUpgradeable.sol b/contracts/l2/token/GraphTokenUpgradeable.sol new file mode 100644 index 000000000..f7d65e26b --- /dev/null +++ b/contracts/l2/token/GraphTokenUpgradeable.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../upgrades/GraphUpgradeable.sol"; +import "../../token/GraphToken.sol"; +import "../../governance/Governed.sol"; + +/** + * @title GraphTokenUpgradeable contract + * @dev This is the implementation of the ERC20 Graph Token. + * The implementation exposes a Permit() function to allow for a spender to send a signed message + * and approve funds to a spender following EIP2612 to make integration with other contracts easier. + * + * The token is initially owned by the deployer address that can mint tokens to create the initial + * distribution. For convenience, an initial supply can be passed in the constructor that will be + * assigned to the deployer. + * + * The governor can add contracts allowed to mint indexing rewards. + * + * Note this is an exact copy of the original GraphToken contract, but using + * initializer functions and upgradeable OpenZeppelin contracts instead of + * the original's constructor + non-upgradeable approach. + */ +contract GraphTokenUpgradeable is + GraphUpgradeable, + Governed, + ERC20Upgradeable, + ERC20BurnableUpgradeable +{ + using SafeMath for uint256; + + // -- EIP712 -- + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-domainseparator + + bytes32 private constant DOMAIN_TYPE_HASH = + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ); + bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Token"); + bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); + bytes32 private constant DOMAIN_SALT = + 0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591; // Randomly generated salt + bytes32 private constant PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + // -- State -- + + // solhint-disable-next-line var-name-mixedcase + bytes32 private DOMAIN_SEPARATOR; + mapping(address => bool) private _minters; + mapping(address => uint256) public nonces; + + // -- Events -- + + event MinterAdded(address indexed account); + event MinterRemoved(address indexed account); + + modifier onlyMinter() { + require(isMinter(msg.sender), "Only minter can call"); + _; + } + + /** + * @dev Approve token allowance by validating a message signed by the holder. + * @param _owner Address of the token holder + * @param _spender Address of the approved spender + * @param _value Amount of tokens to approve the spender + * @param _deadline Expiration time of the signed permit (if zero, the permit will never expire, so use with caution) + * @param _v Signature version + * @param _r Signature r value + * @param _s Signature s value + */ + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonces[_owner], _deadline) + ) + ) + ); + + address recoveredAddress = ECDSA.recover(digest, _v, _r, _s); + require(_owner == recoveredAddress, "GRT: invalid permit"); + require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); + + nonces[_owner] = nonces[_owner].add(1); + _approve(_owner, _spender, _value); + } + + /** + * @dev Add a new minter. + * @param _account Address of the minter + */ + function addMinter(address _account) external onlyGovernor { + require(_account != address(0), "INVALID_MINTER"); + _addMinter(_account); + } + + /** + * @dev Remove a minter. + * @param _account Address of the minter + */ + function removeMinter(address _account) external onlyGovernor { + require(_minters[_account], "NOT_A_MINTER"); + _removeMinter(_account); + } + + /** + * @dev Renounce to be a minter. + */ + function renounceMinter() external { + require(_minters[msg.sender], "NOT_A_MINTER"); + _removeMinter(msg.sender); + } + + /** + * @dev Mint new tokens. + * @param _to Address to send the newly minted tokens + * @param _amount Amount of tokens to mint + */ + function mint(address _to, uint256 _amount) external onlyMinter { + _mint(_to, _amount); + } + + /** + * @dev Return if the `_account` is a minter or not. + * @param _account Address to check + * @return True if the `_account` is minter + */ + function isMinter(address _account) public view returns (bool) { + return _minters[_account]; + } + + /** + * @dev Graph Token Contract initializer. + * @param _owner Owner of this contract, who will hold the initial supply and will be a minter + * @param _initialSupply Initial supply of GRT + */ + function _initialize(address _owner, uint256 _initialSupply) internal { + __ERC20_init("Graph Token", "GRT"); + Governed._initialize(_owner); + + // The Governor has the initial supply of tokens + _mint(_owner, _initialSupply); + + // The Governor is the default minter + _addMinter(_owner); + + // EIP-712 domain separator + DOMAIN_SEPARATOR = keccak256( + abi.encode( + DOMAIN_TYPE_HASH, + DOMAIN_NAME_HASH, + DOMAIN_VERSION_HASH, + _getChainID(), + address(this), + DOMAIN_SALT + ) + ); + } + + /** + * @dev Add a new minter. + * @param _account Address of the minter + */ + function _addMinter(address _account) private { + _minters[_account] = true; + emit MinterAdded(_account); + } + + /** + * @dev Remove a minter. + * @param _account Address of the minter + */ + function _removeMinter(address _account) private { + _minters[_account] = false; + emit MinterRemoved(_account); + } + + /** + * @dev Get the running network chain ID. + * @return The chain ID + */ + function _getChainID() private pure returns (uint256) { + uint256 id; + // solhint-disable-next-line no-inline-assembly + assembly { + id := chainid() + } + return id; + } +} diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index dda740c79..ec6ca4eb8 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -2,213 +2,10 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol"; -import "@openzeppelin/contracts/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; -import "../../upgrades/GraphUpgradeable.sol"; -import "../../token/GraphToken.sol"; +import "./GraphTokenUpgradeable.sol"; import "../../arbitrum/IArbToken.sol"; -import "../../governance/Governed.sol"; - -/** - * @title GraphTokenUpgradeable contract - * @dev This is the implementation of the ERC20 Graph Token. - * The implementation exposes a Permit() function to allow for a spender to send a signed message - * and approve funds to a spender following EIP2612 to make integration with other contracts easier. - * - * The token is initially owned by the deployer address that can mint tokens to create the initial - * distribution. For convenience, an initial supply can be passed in the constructor that will be - * assigned to the deployer. - * - * The governor can add contracts allowed to mint indexing rewards. - * - * Note this is an exact copy of the original GraphToken contract, but using - * initializer functions and upgradeable OpenZeppelin contracts instead of - * the original's constructor + non-upgradeable approach. - */ -contract GraphTokenUpgradeable is - GraphUpgradeable, - Governed, - ERC20Upgradeable, - ERC20BurnableUpgradeable -{ - using SafeMath for uint256; - - // -- EIP712 -- - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-domainseparator - - bytes32 private constant DOMAIN_TYPE_HASH = - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" - ); - bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Token"); - bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); - bytes32 private constant DOMAIN_SALT = - 0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591; // Randomly generated salt - bytes32 private constant PERMIT_TYPEHASH = - keccak256( - "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" - ); - - // -- State -- - - // solhint-disable-next-line var-name-mixedcase - bytes32 private DOMAIN_SEPARATOR; - mapping(address => bool) private _minters; - mapping(address => uint256) public nonces; - - // -- Events -- - - event MinterAdded(address indexed account); - event MinterRemoved(address indexed account); - - modifier onlyMinter() { - require(isMinter(msg.sender), "Only minter can call"); - _; - } - - /** - * @dev Approve token allowance by validating a message signed by the holder. - * @param _owner Address of the token holder - * @param _spender Address of the approved spender - * @param _value Amount of tokens to approve the spender - * @param _deadline Expiration time of the signed permit (if zero, the permit will never expire, so use with caution) - * @param _v Signature version - * @param _r Signature r value - * @param _s Signature s value - */ - function permit( - address _owner, - address _spender, - uint256 _value, - uint256 _deadline, - uint8 _v, - bytes32 _r, - bytes32 _s - ) external { - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR, - keccak256( - abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonces[_owner], _deadline) - ) - ) - ); - - address recoveredAddress = ECDSA.recover(digest, _v, _r, _s); - require(_owner == recoveredAddress, "GRT: invalid permit"); - require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); - - nonces[_owner] = nonces[_owner].add(1); - _approve(_owner, _spender, _value); - } - - /** - * @dev Add a new minter. - * @param _account Address of the minter - */ - function addMinter(address _account) external onlyGovernor { - require(_account != address(0), "INVALID_MINTER"); - _addMinter(_account); - } - - /** - * @dev Remove a minter. - * @param _account Address of the minter - */ - function removeMinter(address _account) external onlyGovernor { - require(_minters[_account], "NOT_A_MINTER"); - _removeMinter(_account); - } - - /** - * @dev Renounce to be a minter. - */ - function renounceMinter() external { - require(_minters[msg.sender], "NOT_A_MINTER"); - _removeMinter(msg.sender); - } - - /** - * @dev Mint new tokens. - * @param _to Address to send the newly minted tokens - * @param _amount Amount of tokens to mint - */ - function mint(address _to, uint256 _amount) external onlyMinter { - _mint(_to, _amount); - } - - /** - * @dev Return if the `_account` is a minter or not. - * @param _account Address to check - * @return True if the `_account` is minter - */ - function isMinter(address _account) public view returns (bool) { - return _minters[_account]; - } - - /** - * @dev Graph Token Contract initializer. - * @param _owner Owner of this contract, who will hold the initial supply and will be a minter - * @param _initialSupply Initial supply of GRT - */ - function _initialize(address _owner, uint256 _initialSupply) internal { - __ERC20_init("Graph Token", "GRT"); - Governed._initialize(_owner); - - // The Governor has the initial supply of tokens - _mint(_owner, _initialSupply); - - // The Governor is the default minter - _addMinter(_owner); - - // EIP-712 domain separator - DOMAIN_SEPARATOR = keccak256( - abi.encode( - DOMAIN_TYPE_HASH, - DOMAIN_NAME_HASH, - DOMAIN_VERSION_HASH, - _getChainID(), - address(this), - DOMAIN_SALT - ) - ); - } - - /** - * @dev Add a new minter. - * @param _account Address of the minter - */ - function _addMinter(address _account) private { - _minters[_account] = true; - emit MinterAdded(_account); - } - - /** - * @dev Remove a minter. - * @param _account Address of the minter - */ - function _removeMinter(address _account) private { - _minters[_account] = false; - emit MinterRemoved(_account); - } - - /** - * @dev Get the running network chain ID. - * @return The chain ID - */ - function _getChainID() private pure returns (uint256) { - uint256 id; - // solhint-disable-next-line no-inline-assembly - assembly { - id := chainid() - } - return id; - } -} /** * @title L2 Graph Token Contract diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index de25a2b80..2d3919147 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -31,28 +31,3 @@ interface IReservoir { */ function getNewRewards(uint256 _blocknum) external view returns (uint256); } - -/** - * @title Interface for the L2 Rewards Reservoir - * @dev This exposes a specific function for the L2Reservoir that is called - * as a callhook from L1 to L2, so that state can be updated when dripped rewards - * are bridged between layers. - */ -interface IL2Reservoir is IReservoir { - /** - * @dev Receive dripped tokens from L1. - * This function can only be called by the gateway, as it is - * meant to be a callhook when receiving tokens from L1. It - * updates the issuanceBase and issuanceRate, - * and snapshots the accumulated rewards. If issuanceRate changes, - * it also triggers a snapshot of rewards per signal on the RewardsManager. - * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) - * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 - * @param _nonce Incrementing nonce to ensure messages are received in order - */ - function receiveDrip( - uint256 _issuanceBase, - uint256 _issuanceRate, - uint256 _nonce - ) external; -} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 0215e07ea..0636b9c99 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -7,7 +7,7 @@ import "@openzeppelin/contracts/math/SafeMath.sol"; import "../arbitrum/ITokenGateway.sol"; -import "./IReservoir.sol"; +import "../l2/reservoir/IL2Reservoir.sol"; import "./Reservoir.sol"; import "./L1ReservoirStorage.sol"; From b7b2a6c6b583031c2418ec6bfa5735e0a883d405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 12:59:29 +0200 Subject: [PATCH 37/78] fix: rename some variables for clarity [N-14] --- contracts/l2/gateway/L2GraphTokenGateway.sol | 23 ++++++++++----- contracts/l2/reservoir/L2Reservoir.sol | 4 +-- contracts/reservoir/L1Reservoir.sol | 31 ++++++++++++-------- contracts/reservoir/Reservoir.sol | 2 +- contracts/rewards/RewardsManager.sol | 14 ++++----- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 89943d010..f728417bc 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -144,23 +144,32 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { require(msg.value == 0, "INVALID_NONZERO_VALUE"); require(_to != address(0), "INVALID_DESTINATION"); - OutboundCalldata memory s; + OutboundCalldata memory outboundCalldata; - (s.from, s.extraData) = parseOutboundData(_data); - require(s.extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); + (outboundCalldata.from, outboundCalldata.extraData) = parseOutboundData(_data); + require(outboundCalldata.extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); // from needs to approve this contract to burn the amount first - L2GraphToken(this.calculateL2TokenAddress(l1GRT)).bridgeBurn(s.from, _amount); + L2GraphToken(this.calculateL2TokenAddress(l1GRT)).bridgeBurn( + outboundCalldata.from, + _amount + ); uint256 id = sendTxToL1( 0, - s.from, + outboundCalldata.from, l1Counterpart, - getOutboundCalldata(_l1Token, s.from, _to, _amount, s.extraData) + getOutboundCalldata( + _l1Token, + outboundCalldata.from, + _to, + _amount, + outboundCalldata.extraData + ) ); // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 - emit WithdrawalInitiated(_l1Token, s.from, _to, id, 0, _amount); + emit WithdrawalInitiated(_l1Token, outboundCalldata.from, _to, id, 0, _amount); return abi.encode(id); } diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 67376648a..a317d02a4 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -73,8 +73,8 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { } return issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) + .mul(_pow(issuanceRate, _blocknum.sub(t0), FIXED_POINT_SCALING_FACTOR)) + .div(FIXED_POINT_SCALING_FACTOR) .sub(issuanceBase); } diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 0636b9c99..5e98f2570 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -97,7 +97,10 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _l2RewardsFraction Fraction of rewards to send to L2, in wei / fixed point at 1e18 */ function setL2RewardsFraction(uint256 _l2RewardsFraction) external onlyGovernor { - require(_l2RewardsFraction <= TOKEN_DECIMALS, "L2 Rewards fraction must be <= 1"); + require( + _l2RewardsFraction <= FIXED_POINT_SCALING_FACTOR, + "L2 Rewards fraction must be <= 1" + ); nextL2RewardsFraction = _l2RewardsFraction; emit L2RewardsFractionStaged(_l2RewardsFraction); } @@ -187,7 +190,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { uint256 tokensToSendToL2 = 0; if (l2RewardsFraction != nextL2RewardsFraction) { tokensToSendToL2 = nextL2RewardsFraction.mul(newRewardsToDistribute).div( - TOKEN_DECIMALS + FIXED_POINT_SCALING_FACTOR ); if (mintedRewardsTotal > mintedRewardsActual) { // eps > 0, i.e. t < t1_old @@ -198,7 +201,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // with the new values to the L2Reservoir. uint256 l2OffsetAmount = l2RewardsFraction .mul(mintedRewardsTotal.sub(mintedRewardsActual)) - .div(TOKEN_DECIMALS); + .div(FIXED_POINT_SCALING_FACTOR); require( tokensToSendToL2 > l2OffsetAmount, "Negative amount would be sent to L2, wait before calling again" @@ -207,7 +210,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { } else { tokensToSendToL2 = tokensToSendToL2.add( l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( - TOKEN_DECIMALS + FIXED_POINT_SCALING_FACTOR ) ); } @@ -220,7 +223,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { _l2MaxSubmissionCost ); } else if (l2RewardsFraction > 0) { - tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(TOKEN_DECIMALS); + tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(FIXED_POINT_SCALING_FACTOR); _sendNewTokensAndStateToL2( tokensToSendToL2, _l2MaxGas, @@ -251,8 +254,8 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { } return issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) + .mul(_pow(issuanceRate, _blocknum.sub(t0), FIXED_POINT_SCALING_FACTOR)) + .div(FIXED_POINT_SCALING_FACTOR) .sub(issuanceBase); } @@ -266,9 +269,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { */ function getNewRewards(uint256 _blocknum) public view override returns (uint256) { return - getNewGlobalRewards(_blocknum).mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div( - TOKEN_DECIMALS - ); + getNewGlobalRewards(_blocknum) + .mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)) + .div(FIXED_POINT_SCALING_FACTOR); } /** @@ -297,7 +300,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // to save gas accumulatedLayerRewards = accumulatedLayerRewards + - _globalDelta.mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div(TOKEN_DECIMALS); + _globalDelta.mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)).div( + FIXED_POINT_SCALING_FACTOR + ); lastRewardsUpdateBlock = block.number; } @@ -316,7 +321,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { uint256 _gasPriceBid, uint256 _maxSubmissionCost ) internal { - uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); + uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div( + FIXED_POINT_SCALING_FACTOR + ); bytes memory extraData = abi.encodeWithSelector( IL2Reservoir.receiveDrip.selector, l2IssuanceBase, diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index 048697bc9..6e15948f6 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -20,7 +20,7 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir using SafeMath for uint256; uint256 private constant MAX_UINT256 = 2**256 - 1; - uint256 internal constant TOKEN_DECIMALS = 1e18; + uint256 internal constant FIXED_POINT_SCALING_FACTOR = 1e18; uint256 internal constant MIN_ISSUANCE_RATE = 1e18; /** diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 0b33b8d3b..aed867a1c 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -32,7 +32,7 @@ import "../reservoir/IReservoir.sol"; contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; - uint256 private constant TOKEN_DECIMALS = 1e18; + uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; uint256 private constant MIN_ISSUANCE_RATE = 1e18; // -- Events -- @@ -197,7 +197,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number return - (accRewardsNow.sub(accRewardsOnLastSignalUpdate)).mul(TOKEN_DECIMALS).div( + (accRewardsNow.sub(accRewardsOnLastSignalUpdate)).mul(FIXED_POINT_SCALING_FACTOR).div( signalledTokens ); } @@ -231,7 +231,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa ? getAccRewardsPerSignal() .sub(subgraph.accRewardsPerSignalSnapshot) .mul(subgraphSignalledTokens) - .div(TOKEN_DECIMALS) + .div(FIXED_POINT_SCALING_FACTOR) : 0; return subgraph.accRewardsForSubgraph.add(newRewards); } @@ -262,9 +262,9 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa return (0, accRewardsForSubgraph); } - uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph.mul(TOKEN_DECIMALS).div( - subgraphAllocatedTokens - ); + uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph + .mul(FIXED_POINT_SCALING_FACTOR) + .div(subgraphAllocatedTokens); return ( subgraph.accRewardsPerAllocatedToken.add(newRewardsPerAllocatedToken), accRewardsForSubgraph @@ -364,7 +364,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa uint256 _endAccRewardsPerAllocatedToken ) private pure returns (uint256) { uint256 newAccrued = _endAccRewardsPerAllocatedToken.sub(_startAccRewardsPerAllocatedToken); - return newAccrued.mul(_tokens).div(TOKEN_DECIMALS); + return newAccrued.mul(_tokens).div(FIXED_POINT_SCALING_FACTOR); } /** From 1f798d89f43a4a8b0177a5beb8db3dccf3a39cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 13:29:04 +0200 Subject: [PATCH 38/78] fix: document the need to retry tickets if drip is received out-of-order [N-15] --- contracts/l2/reservoir/L2Reservoir.sol | 3 +++ contracts/reservoir/L1Reservoir.sol | 3 +++ 2 files changed, 6 insertions(+) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index a317d02a4..1bc93c912 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -85,6 +85,9 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * updates the issuanceBase and issuanceRate, * and snapshots the accumulated rewards. If issuanceRate changes, * it also triggers a snapshot of rewards per signal on the RewardsManager. + * Note that the transaction might revert if it's received out-of-order, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 5e98f2570..547b3e618 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -147,6 +147,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * until the drip amount becomes positive before calling the function again. It can also revert * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 From 012e3d2d3637bb9a719886bb4e1df6488058debd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 13:37:32 +0200 Subject: [PATCH 39/78] fix: various typos [N-16] --- contracts/arbitrum/ITokenGateway.sol | 2 +- contracts/gateway/L1GraphTokenGateway.sol | 8 ++++---- contracts/l2/gateway/L2GraphTokenGateway.sol | 2 +- contracts/l2/token/GraphTokenUpgradeable.sol | 4 ++-- contracts/reservoir/IReservoir.sol | 2 +- contracts/reservoir/L1Reservoir.sol | 6 +++--- contracts/rewards/RewardsManager.sol | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/arbitrum/ITokenGateway.sol b/contracts/arbitrum/ITokenGateway.sol index c59af47a6..977fe07f2 100644 --- a/contracts/arbitrum/ITokenGateway.sol +++ b/contracts/arbitrum/ITokenGateway.sol @@ -66,7 +66,7 @@ interface ITokenGateway { /** * @notice Calculate the address used when bridging an ERC20 token * @dev the L1 and L2 address oracles may not always be in sync. - * For example, a custom token may have been registered but not deploy or the contract self destructed. + * For example, a custom token may have been registered but not deployed or the contract self destructed. * @param l1ERC20 address of L1 token * @return L2 address of a bridged ERC20 token */ diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index e9bb18897..23b7cc12d 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -100,7 +100,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { } /** - * @dev sets the addresses for L1 contracts provided by Arbitrum + * @dev Sets the addresses for L1 contracts provided by Arbitrum * @param _inbox Address of the Inbox that is part of the Arbitrum Bridge * @param _l1Router Address of the Gateway Router */ @@ -207,7 +207,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { require(maxSubmissionCost > 0, "NO_SUBMISSION_COST"); { - // makes sure only sufficient ETH is supplied required for successful redemption on L2 + // makes sure only sufficient ETH is supplied as required for successful redemption on L2 // if a user does not desire immediate redemption they should provide // a msg.value of AT LEAST maxSubmissionCost uint256 expectedEth = maxSubmissionCost + (_maxGas * _gasPriceBid); @@ -269,8 +269,8 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { } /** - * @notice decodes calldata required for migration of tokens - * @dev data must include maxSubmissionCost, extraData can be left empty. When the router + * @notice Decodes calldata required for migration of tokens + * @dev Data must include maxSubmissionCost, extraData can be left empty. When the router * sends an outbound message, data also contains the from address. * @param _data encoded callhook data * @return Sender of the tx diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index f728417bc..f9bef2d79 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -13,7 +13,7 @@ import "../token/L2GraphToken.sol"; /** * @title L2 Graph Token Gateway Contract * @dev Provides the L2 side of the Ethereum-Arbitrum GRT bridge. Receives GRT from the L1 chain - * and mints them on the L2 side. Sending GRT back to L1 by burning them on the L2 side. + * and mints them on the L2 side. Sends GRT back to L1 by burning them on the L2 side. * Based on Offchain Labs' reference implementation and Livepeer's arbitrum-lpt-bridge * (See: https://github.com/OffchainLabs/arbitrum/tree/master/packages/arb-bridge-peripherals/contracts/tokenbridge * and https://github.com/livepeer/arbitrum-lpt-bridge) diff --git a/contracts/l2/token/GraphTokenUpgradeable.sol b/contracts/l2/token/GraphTokenUpgradeable.sol index f7d65e26b..793129eed 100644 --- a/contracts/l2/token/GraphTokenUpgradeable.sol +++ b/contracts/l2/token/GraphTokenUpgradeable.sol @@ -14,7 +14,7 @@ import "../../governance/Governed.sol"; /** * @title GraphTokenUpgradeable contract * @dev This is the implementation of the ERC20 Graph Token. - * The implementation exposes a Permit() function to allow for a spender to send a signed message + * The implementation exposes a permit() function to allow for a spender to send a signed message * and approve funds to a spender following EIP2612 to make integration with other contracts easier. * * The token is initially owned by the deployer address that can mint tokens to create the initial @@ -74,7 +74,7 @@ contract GraphTokenUpgradeable is * @param _spender Address of the approved spender * @param _value Amount of tokens to approve the spender * @param _deadline Expiration time of the signed permit (if zero, the permit will never expire, so use with caution) - * @param _v Signature version + * @param _v Signature recovery id * @param _r Signature r value * @param _s Signature s value */ diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index 2d3919147..a101952e0 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -5,7 +5,7 @@ pragma solidity ^0.7.6; /** * @title Interface for the Rewards Reservoir * @dev This is the shared interface between L1 and L2, for the contracts - * that hold rewards on each layers and provide functions to compute + * that hold rewards on each layer and provide functions to compute * accumulated and new total rewards. */ interface IReservoir { diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 547b3e618..21e3d750a 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -150,9 +150,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. - * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 - * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 - * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 */ function drip( uint256 _l2MaxGas, diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index aed867a1c..c9dcf103d 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -274,7 +274,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa // -- Updates -- /** - * @dev Updates the accumulated rewards per signal and save checkpoint block number. + * @dev Updates the accumulated rewards per signal and saves the checkpoint block number. * Also snapshots total accumulated rewards (`accRewardsOnLastSignalUpdate`). * Must be called before `issuanceRate` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() From 1e171dbfe62fa61ee52b512473138dc56abaadd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 13:45:30 +0200 Subject: [PATCH 40/78] fix: remove unneeded ERC20Upgradeable inheritance [N-17] --- contracts/l2/token/GraphTokenUpgradeable.sol | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/contracts/l2/token/GraphTokenUpgradeable.sol b/contracts/l2/token/GraphTokenUpgradeable.sol index 793129eed..0df9b8e06 100644 --- a/contracts/l2/token/GraphTokenUpgradeable.sol +++ b/contracts/l2/token/GraphTokenUpgradeable.sol @@ -2,7 +2,6 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol"; import "@openzeppelin/contracts/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -27,12 +26,7 @@ import "../../governance/Governed.sol"; * initializer functions and upgradeable OpenZeppelin contracts instead of * the original's constructor + non-upgradeable approach. */ -contract GraphTokenUpgradeable is - GraphUpgradeable, - Governed, - ERC20Upgradeable, - ERC20BurnableUpgradeable -{ +contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20BurnableUpgradeable { using SafeMath for uint256; // -- EIP712 -- From 65829afc20a4357f67e48e0860cc7df0d4ea84e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 13:51:17 +0200 Subject: [PATCH 41/78] fix: remove some unnecessary bits of code [N-19] --- contracts/gateway/L1GraphTokenGateway.sol | 10 +++++----- contracts/l2/gateway/L2GraphTokenGateway.sol | 7 ++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index 23b7cc12d..ac5b1c6bc 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -242,30 +242,30 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { /** * @notice Receives withdrawn tokens from L2 * The equivalent tokens are released from escrow and sent to the destination. - * @dev can only accept transactions coming from the L2 GRT Gateway + * @dev can only accept transactions coming from the L2 GRT Gateway. + * The last parameter is unused but kept for compatibility with Arbitrum gateways, + * and the encoded exitNum is assumed to be 0. * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) * @param _from Address of the sender * @param _to Recepient address on L1 * @param _amount Amount of tokens transferred - * @param _data Contains exitNum which is always set to 0 */ function finalizeInboundTransfer( address _l1Token, address _from, address _to, uint256 _amount, - bytes calldata _data + bytes calldata // _data, contains exitNum, unused by this contract ) external payable override notPaused onlyL2Counterpart { IGraphToken token = graphToken(); require(_l1Token == address(token), "TOKEN_NOT_GRT"); - (uint256 exitNum, ) = abi.decode(_data, (uint256, bytes)); uint256 escrowBalance = token.balanceOf(escrow); // If the bridge doesn't have enough tokens, something's very wrong! require(_amount <= escrowBalance, "BRIDGE_OUT_OF_FUNDS"); token.transferFrom(escrow, _to, _amount); - emit WithdrawalFinalized(_l1Token, _from, _to, exitNum, _amount); + emit WithdrawalFinalized(_l1Token, _from, _to, 0, _amount); } /** diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index f9bef2d79..a6c57acdd 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -150,10 +150,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { require(outboundCalldata.extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); // from needs to approve this contract to burn the amount first - L2GraphToken(this.calculateL2TokenAddress(l1GRT)).bridgeBurn( - outboundCalldata.from, - _amount - ); + L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeBurn(outboundCalldata.from, _amount); uint256 id = sendTxToL1( 0, @@ -191,7 +188,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { uint256 _amount, bytes calldata _data ) external returns (bytes memory) { - return outboundTransfer(_l1Token, _to, _amount, uint256(0), uint256(0), _data); + return outboundTransfer(_l1Token, _to, _amount, 0, 0, _data); } /** From a11501f819f24d8642b74886d596d3e3ee6e7489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 13:56:47 +0200 Subject: [PATCH 42/78] fix: replace MAX_UINT256 with type().max [N-18] [N-20] --- contracts/gateway/BridgeEscrow.sol | 4 +--- contracts/reservoir/Reservoir.sol | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/gateway/BridgeEscrow.sol b/contracts/gateway/BridgeEscrow.sol index 2d161e222..605f13a50 100644 --- a/contracts/gateway/BridgeEscrow.sol +++ b/contracts/gateway/BridgeEscrow.sol @@ -13,8 +13,6 @@ import "../token/IGraphToken.sol"; * approved as a spender. */ contract BridgeEscrow is GraphUpgradeable, Managed { - uint256 private constant MAX_UINT256 = 2**256 - 1; - /** * @dev Initialize this contract. * @param _controller Address of the Controller that manages this contract @@ -28,7 +26,7 @@ contract BridgeEscrow is GraphUpgradeable, Managed { * @param _spender Address of the spender that will be approved */ function approveAll(address _spender) external onlyGovernor { - graphToken().approve(_spender, MAX_UINT256); + graphToken().approve(_spender, type(uint256).max); } /** diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index 6e15948f6..26c5f02f2 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -19,7 +19,6 @@ import "./IReservoir.sol"; abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir { using SafeMath for uint256; - uint256 private constant MAX_UINT256 = 2**256 - 1; uint256 internal constant FIXED_POINT_SCALING_FACTOR = 1e18; uint256 internal constant MIN_ISSUANCE_RATE = 1e18; @@ -27,7 +26,7 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir * @dev Approve the RewardsManager to manage the reservoir's token funds */ function approveRewardsManager() external override onlyGovernor { - graphToken().approve(address(rewardsManager()), MAX_UINT256); + graphToken().approve(address(rewardsManager()), type(uint256).max); } /** From 960cc04584945e206ff61eb885f5eef9d412395d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 14:01:20 +0200 Subject: [PATCH 43/78] fix: remove an unused import [N-21] --- contracts/reservoir/ReservoirStorage.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol index 8e3e2591d..b46e44d35 100644 --- a/contracts/reservoir/ReservoirStorage.sol +++ b/contracts/reservoir/ReservoirStorage.sol @@ -2,7 +2,6 @@ pragma solidity ^0.7.6; -import "./IReservoir.sol"; import "../governance/Managed.sol"; /** From 3a6c3b5f8bd005d72036341bf8314106b36d26f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 14:08:02 +0200 Subject: [PATCH 44/78] fix: check before adding/removing whitelisted addresses [N-23] --- contracts/gateway/L1GraphTokenGateway.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index ac5b1c6bc..c8fb4baa1 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -149,6 +149,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { */ function addToCallhookWhitelist(address _newWhitelisted) external onlyGovernor { require(_newWhitelisted != address(0), "INVALID_ADDRESS"); + require(!callhookWhitelist[_newWhitelisted], "ALREADY_WHITELISTED"); callhookWhitelist[_newWhitelisted] = true; emit AddedToCallhookWhitelist(_newWhitelisted); } @@ -160,6 +161,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { */ function removeFromCallhookWhitelist(address _notWhitelisted) external onlyGovernor { require(_notWhitelisted != address(0), "INVALID_ADDRESS"); + require(callhookWhitelist[_notWhitelisted], "NOT_WHITELISTED"); callhookWhitelist[_notWhitelisted] = false; emit RemovedFromCallhookWhitelist(_notWhitelisted); } From 37d70a5191a0ac037ee7fb93fa73dc4f73f111b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 13:15:52 +0200 Subject: [PATCH 45/78] test: remove repeated addToCallhookWhitelist --- test/lib/fixtures.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 54dc8bf3f..5a4f1e53a 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -283,9 +283,6 @@ export class NetworkFixture { await l1FixtureContracts.l1Reservoir .connect(deployer) .setL2ReservoirAddress(mockL2ReservoirAddress) - await l1FixtureContracts.l1GraphTokenGateway - .connect(deployer) - .addToCallhookWhitelist(l1FixtureContracts.l1Reservoir.address) await l1FixtureContracts.l1GraphTokenGateway.connect(deployer).setPaused(false) } From 9e7686f13758e92e25c40cca034545450678836f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 14:05:56 +0200 Subject: [PATCH 46/78] fix: use SafeMath more consistently --- contracts/gateway/L1GraphTokenGateway.sol | 2 +- contracts/reservoir/L1Reservoir.sol | 8 ++++---- contracts/reservoir/Reservoir.sol | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index c8fb4baa1..b3a0f8981 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -212,7 +212,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { // makes sure only sufficient ETH is supplied as required for successful redemption on L2 // if a user does not desire immediate redemption they should provide // a msg.value of AT LEAST maxSubmissionCost - uint256 expectedEth = maxSubmissionCost + (_maxGas * _gasPriceBid); + uint256 expectedEth = maxSubmissionCost.add(_maxGas.mul(_gasPriceBid)); require(msg.value >= expectedEth, "WRONG_ETH_VALUE"); } outboundCalldata = getOutboundCalldata(_l1Token, from, _to, _amount, extraData); diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 21e3d750a..674e127ef 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -298,14 +298,14 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block */ function snapshotAccumulatedRewards(uint256 _globalDelta) internal { - issuanceBase = issuanceBase + _globalDelta; + issuanceBase = issuanceBase.add(_globalDelta); // Reimplementation of getAccumulatedRewards but reusing the _globalDelta calculated above, // to save gas - accumulatedLayerRewards = - accumulatedLayerRewards + + accumulatedLayerRewards = accumulatedLayerRewards.add( _globalDelta.mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)).div( FIXED_POINT_SCALING_FACTOR - ); + ) + ); lastRewardsUpdateBlock = block.number; } diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index 26c5f02f2..d2a0bf6cb 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -36,7 +36,7 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir */ function getAccumulatedRewards(uint256 _blocknum) public view override returns (uint256) { // R(t) = R(t0) + (DeltaR(t, t0)) - return accumulatedLayerRewards + getNewRewards(_blocknum); + return accumulatedLayerRewards.add(getNewRewards(_blocknum)); } /** From 8426b4c4116650dabee5085aaa636f48d2d4fadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 26 Jul 2022 17:35:31 +0200 Subject: [PATCH 47/78] fix: add more docs on the design behind callhook reverts [L-13] --- contracts/gateway/L1GraphTokenGateway.sol | 6 ++++++ contracts/l2/gateway/L2GraphTokenGateway.sol | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index b3a0f8981..19fd73be5 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -172,6 +172,12 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * The ticket must be redeemed on L2 to receive tokens at the specified address. * Note that the caller must previously allow the gateway to spend the specified amount of GRT. * @dev maxGas and gasPriceBid must be set using Arbitrum's NodeInterface.estimateRetryableTicket method. + * Also note that whitelisted senders (some protocol contracts) can include additional calldata + * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction + * can revert if the callhook reverts, potentially locking the tokens on the bridge if the callhook + * never succeeds. This requires extra care when adding contracts to the whitelist, but is necessary to ensure that + * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks + * with token transfers. * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) * @param _to Recipient address on L2 * @param _amount Amount of tokens to tranfer diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index a6c57acdd..593e0e228 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -208,6 +208,12 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { * @notice Receives token amount from L1 and mints the equivalent tokens to the receiving address * @dev Only accepts transactions from the L1 GRT Gateway. * The function is payable for ITokenGateway compatibility, but msg.value must be zero. + * Note that whitelisted senders (some protocol contracts) can include additional calldata + * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction + * can revert if the callhook reverts, potentially locking the tokens on the bridge if the callhook + * never succeeds. This requires extra care when adding contracts to the whitelist, but is necessary to ensure that + * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks + * with token transfers. * @param _l1Token L1 Address of GRT * @param _from Address of the sender on L1 * @param _to Recipient address on L2 From eb23e6a67aa581cd9c414df8bf74a4a6bf3469f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 27 Jul 2022 17:06:31 +0200 Subject: [PATCH 48/78] fix: correct some mistakes from the merge conflict resolution --- cli/commands/migrate.ts | 2 +- tasks/gre.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 81ca7c6dd..fb817cd67 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -18,7 +18,7 @@ const { EtherSymbol } = constants const { formatEther } = utils // Contracts are deployed in the order defined in this list -const allContracts = [ +let allContracts = [ 'GraphProxyAdmin', 'BancorFormula', 'Controller', diff --git a/tasks/gre.ts b/tasks/gre.ts index baced85d9..d8a13a13e 100644 --- a/tasks/gre.ts +++ b/tasks/gre.ts @@ -27,7 +27,7 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { addressBook: lazyObject(() => getAddressBook(addressBookPath, chainId)), graphConfig: lazyObject(() => readConfig(graphConfigPath, true)), contracts: lazyObject(() => - loadContracts(getAddressBook(addressBookPath, chainId), hre.ethers.provider), + loadContracts(getAddressBook(addressBookPath, chainId), chainId, hre.ethers.provider), ), } } From 7648ea24c0bb05c0616623c76c1cf8e471627033 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Thu, 14 Apr 2022 17:26:48 -0300 Subject: [PATCH 49/78] feat: add support for Arbitrum Nitro on CLI commands --- cli/commands/bridge/to-l1.ts | 33 +++--- cli/commands/bridge/to-l2.ts | 100 +++++++----------- cli/utils.ts | 54 +++++++++- contracts/arbitrum/NodeInterface.sol | 76 ------------- .../tests/arbitrum/ArbRetryableTxStub.sol | 9 -- package.json | 2 +- yarn.lock | 59 ++--------- 7 files changed, 110 insertions(+), 223 deletions(-) delete mode 100644 contracts/arbitrum/NodeInterface.sol delete mode 100644 contracts/tests/arbitrum/ArbRetryableTxStub.sol diff --git a/cli/commands/bridge/to-l1.ts b/cli/commands/bridge/to-l1.ts index ffee9dbe8..3796bfd30 100644 --- a/cli/commands/bridge/to-l1.ts +++ b/cli/commands/bridge/to-l1.ts @@ -2,7 +2,7 @@ import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' import { getAddressBook } from '../../address-book' import { getProvider, sendTransaction, toGRT } from '../../network' -import { chainIdIsL2 } from '../../utils' +import { chainIdIsL2, createNitroNetwork } from '../../utils' import { loadAddressBookContract } from '../../contracts' import { L2TransactionReceipt, @@ -13,6 +13,7 @@ import { import { L2GraphTokenGateway } from '../../../build/types/L2GraphTokenGateway' import { BigNumber } from 'ethers' import { JsonRpcProvider } from '@ethersproject/providers' +import { providers } from 'ethers' const FOURTEEN_DAYS_IN_SECONDS = 24 * 3600 * 14 @@ -46,13 +47,14 @@ const wait = (ms: number): Promise => { const waitUntilOutboxEntryCreatedWithCb = async ( msg: L2ToL1MessageWriter, + provider: providers.Provider, retryDelay: number, callback: () => void, ) => { let done = false while (!done) { - const status = await msg.status(null) - if (status == L2ToL1MessageStatus.CONFIRMED) { + const status = await msg.status(provider) + if (status == L2ToL1MessageStatus.CONFIRMED || status == L2ToL1MessageStatus.EXECUTED) { done = true } else { callback() @@ -65,6 +67,7 @@ export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Prom logger.info(`>>> Sending tokens to L1 <<<\n`) const l2Provider = getProvider(cliArgs.l2ProviderUrl) const l2ChainId = (await l2Provider.getNetwork()).chainId + createNitroNetwork(cliArgs.providerUrl) if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', @@ -100,10 +103,8 @@ export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Prom await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider)) )[0] - logger.info( - `The transaction generated an outbox message with batch number ${l2ToL1Message.batchNumber}`, - ) - logger.info(`and index in batch ${l2ToL1Message.indexInBatch}.`) + logger.info(`The transaction generated an L2 to L1 message in outbox with eth block number:`) + logger.info(l2ToL1Message.event.ethBlockNum.toString()) logger.info( `After the dispute period is finalized (in ~1 week), you can finalize this by calling`, ) @@ -119,6 +120,7 @@ export const finishSendToL1 = async ( logger.info(`>>> Finishing transaction sending tokens to L1 <<<\n`) const l2Provider = getProvider(cliArgs.l2ProviderUrl) const l2ChainId = (await l2Provider.getNetwork()).chainId + createNitroNetwork(cliArgs.providerUrl) if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', @@ -162,27 +164,22 @@ export const finishSendToL1 = async ( if (wait) { const retryDelayMs = cliArgs.retryDelaySeconds ? cliArgs.retryDelaySeconds * 1000 : 60000 logger.info('Waiting for outbox entry to be created, this can take a full week...') - await waitUntilOutboxEntryCreatedWithCb(l2ToL1Message, retryDelayMs, () => { + await waitUntilOutboxEntryCreatedWithCb(l2ToL1Message, l2Provider, retryDelayMs, () => { logger.info('Still waiting...') }) } else { - logger.info('Checking if L2 to L1 message is confirmed...') - const status = await l2ToL1Message.status(null) - if (status != L2ToL1MessageStatus.CONFIRMED) { + const status = await l2ToL1Message.status(l2Provider) + if (status == L2ToL1MessageStatus.EXECUTED) { + throw new Error('Message already executed!') + } else if (status != L2ToL1MessageStatus.CONFIRMED) { throw new Error( `Transaction is not confirmed, status is ${status} when it should be ${L2ToL1MessageStatus.CONFIRMED}. Has the dispute period passed?`, ) } } - logger.info('Getting proof to execute message') - const proofInfo = await l2ToL1Message.tryGetProof(l2Provider) - - if (await l2ToL1Message.hasExecuted(proofInfo)) { - throw new Error('Message already executed!') - } logger.info('Executing outbox transaction') - const tx = await l2ToL1Message.execute(proofInfo) + const tx = await l2ToL1Message.execute(l2Provider) const outboxExecuteReceipt = await tx.wait() logger.info('Transaction succeeded! tx hash:') logger.info(outboxExecuteReceipt.transactionHash) diff --git a/cli/commands/bridge/to-l2.ts b/cli/commands/bridge/to-l2.ts index fd9ac947d..b806adad8 100644 --- a/cli/commands/bridge/to-l2.ts +++ b/cli/commands/bridge/to-l2.ts @@ -1,69 +1,49 @@ import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' -import { getContractAt, getProvider, sendTransaction, toGRT } from '../../network' -import { BigNumber, utils } from 'ethers' +import { getProvider, sendTransaction, toGRT } from '../../network' +import { utils } from 'ethers' import { parseEther } from '@ethersproject/units' import { L1TransactionReceipt, L1ToL2MessageStatus, - getRawArbTransactionReceipt, L1ToL2MessageWriter, + L1ToL2MessageGasEstimator, } from '@arbitrum/sdk' -import { nodeInterfaceAddress, arbRetryableTxAddress, chainIdIsL2 } from '../../utils' -import { JsonRpcProvider } from '@ethersproject/providers' - -const maxSubmissionPriceIncreasePct = 400 -const maxGasIncreasePct = 50 - -const percentIncrease = (val: BigNumber, increase: number): BigNumber => { - return val.add(val.mul(increase).div(100)) -} +import { chainIdIsL2, createNitroNetwork } from '../../utils' const logAutoRedeemReason = (autoRedeemRec) => { if (autoRedeemRec == null) { logger.info(`Auto redeem was not attempted.`) return } - switch (autoRedeemRec.returnCode) { - case 1: - logger.info(`Auto redeem reverted.`) - case 2: - logger.info(`Auto redeem failed: hit congestion in the chain.`) - case 8: - logger.info(`Auto redeem failed: exceeded the tx gas limit.`) - case 10: - logger.info(`Auto redeem failed: gas provided is below minimum tx gas.`) - case 11: - logger.info(`Auto redeem failed: L2 gas price is set too low.`) - case 12: - logger.info(`Auto redeem failed: no L2 gas is provided for auto redeem.`) - default: - logger.info(`Auto redeem reverted, unknown code ${autoRedeemRec.returnCode}`) - } + logger.info(`Auto redeem reverted.`) } -const checkAndRedeemMessage = async ( - l1ToL2Message: L1ToL2MessageWriter, - l2Provider: JsonRpcProvider, -) => { +const checkAndRedeemMessage = async (l1ToL2Message: L1ToL2MessageWriter) => { + logger.info(`Waiting for status of ${l1ToL2Message.retryableCreationId}`) const res = await l1ToL2Message.waitForStatus() + logger.info('Getting auto redeem attempt') + const autoRedeemRec = await l1ToL2Message.getAutoRedeemAttempt() + const l2TxReceipt = res.status === L1ToL2MessageStatus.REDEEMED ? res.l2TxReceipt : autoRedeemRec + let l2TxHash = l2TxReceipt ? l2TxReceipt.transactionHash : 'null' if (res.status === L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2) { /** Message wasn't auto-redeemed! */ logger.warn('Funds were deposited on L2 but the retryable ticket was not redeemed') - const autoRedeemRec = await getRawArbTransactionReceipt(l2Provider, l1ToL2Message.autoRedeemId) logAutoRedeemReason(autoRedeemRec) logger.info('Attempting to redeem...') await l1ToL2Message.redeem() + l2TxHash = (await l1ToL2Message.getSuccessfulRedeem()).transactionHash } else if (res.status != L1ToL2MessageStatus.REDEEMED) { throw new Error(`Unexpected L1ToL2MessageStatus ${res.status}`) } - logger.info(`Transfer successful: ${l1ToL2Message.l2TxHash}`) + logger.info(`Transfer successful: ${l2TxHash}`) } export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { logger.info(`>>> Sending tokens to L2 <<<\n`) const l2Provider = getProvider(cliArgs.l2ProviderUrl) const l2ChainId = (await l2Provider.getNetwork()).chainId + createNitroNetwork(cliArgs.providerUrl) if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', @@ -87,36 +67,26 @@ export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise>> Redeeming pending tokens on L2 <<<\n`) const l2Provider = getProvider(cliArgs.l2ProviderUrl) const l2ChainId = (await l2Provider.getNetwork()).chainId + createNitroNetwork(cliArgs.providerUrl) if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', @@ -156,10 +127,11 @@ export const redeemSendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Pro const receipt = await l1Provider.getTransactionReceipt(cliArgs.txHash) const l1Receipt = new L1TransactionReceipt(receipt) - const l1ToL2Message = await l1Receipt.getL1ToL2Message(cli.wallet.connect(l2Provider)) + const l1ToL2Messages = await l1Receipt.getL1ToL2Messages(cli.wallet.connect(l2Provider)) + const l1ToL2Message = l1ToL2Messages[0] logger.info('Checking message status in L2...') - await checkAndRedeemMessage(l1ToL2Message, l2Provider) + await checkAndRedeemMessage(l1ToL2Message) } export const sendToL2Command = { diff --git a/cli/utils.ts b/cli/utils.ts index 035c5dba0..59332d7fa 100644 --- a/cli/utils.ts +++ b/cli/utils.ts @@ -1,3 +1,4 @@ +import { addCustomNetwork } from '@arbitrum/sdk' import { Contract, Wallet, providers } from 'ethers' import { loadArtifact } from './artifacts' @@ -13,9 +14,6 @@ export const l2ToL1ChainIdMap = Object.fromEntries( Object.entries(l1ToL2ChainIdMap).map(([k, v]) => [v, k]), ) -export const nodeInterfaceAddress = '0x00000000000000000000000000000000000000C8' -export const arbRetryableTxAddress = '0x000000000000000000000000000000000000006E' - export const contractAt = ( contractName: string, contractAddress: string, @@ -30,3 +28,53 @@ export const getProvider = (providerUrl: string, network?: number): providers.Js export const chainIdIsL2 = (chainId: number | string): boolean => { return l2ChainIds.includes(Number(chainId)) } + +export const createNitroNetwork = (l1ProviderUrl: string): void => { + addCustomNetwork({ + customL1Network: { + blockTime: 15, + chainID: 5, + explorerUrl: 'https://goerli.etherscan.io/', + isCustom: true, + name: 'Goerli', + partnerChainIDs: [421612], + rpcURL: l1ProviderUrl, + }, + customL2Network: { + chainID: 421612, + confirmPeriodBlocks: 960, + ethBridge: { + bridge: '0x9903a892da86c1e04522d63b08e5514a921e81df', + inbox: '0x1fdbbcc914e84af593884bf8e8dd6877c29035a2', + outboxes: { + '0xFDF2B11347dA17326BAF30bbcd3F4b09c4719584': 0, + }, + rollup: '0x767CfF8D8de386d7cbe91DbD39675132ba2f5967', + sequencerInbox: '0xb32f4257e05c56c53d46bbec9e85770eb52425d6', + }, + explorerUrl: 'https://nitro-devnet-explorer.arbitrum.io/', + isArbitrum: true, + isCustom: true, + name: 'ArbLocal', + partnerChainID: 5, + rpcURL: 'https://nitro-devnet.arbitrum.io/rpc', + tokenBridge: { + l1CustomGateway: '0x23D4e0D7Cb7AE7CF745E82262B17eb46535Ae819', + l1ERC20Gateway: '0x6336C4e811b2f7D17d45b6241Fd47F2E11621Ffb', + l1GatewayRouter: '0x8BDFa67ace22cE2BFb2fFebe72f0c91CDA694d4b', + l1MultiCall: '0x90863B80f274b6D2227b01f2c1de4fdCb04896E2', + l1ProxyAdmin: '0x678cC9702ebF79d741E4f815937475311A58404a', + l1Weth: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + l1WethGateway: '0x64bfF696bE6a087A81936b9a2489624015381be4', + l2CustomGateway: '0x7AC493f26EF26904E52fE46C8DaEE247b9A556B8', + l2ERC20Gateway: '0xf298434ffE691400b932f4b14B436f451F4CED76', + l2GatewayRouter: '0xC502Ded1EE1d616B43F7f20Ebde83Be1A275ca3c', + l2Multicall: '0x1068dbfcc13f3a22fcAe684943AFA43cc66fA689', + l2ProxyAdmin: '0x1F2715AaC7EeFb75ebCc478f3D9a361fa47A95DD', + l2Weth: '0x96CfA560e7332DebA750e330fb6f59E2269f40Dd', + l2WethGateway: '0xf10c7CAA33A3360f60053Bc1081980f62567505F', + }, + retryableLifetimeSeconds: 608400, + }, + }) +} diff --git a/contracts/arbitrum/NodeInterface.sol b/contracts/arbitrum/NodeInterface.sol deleted file mode 100644 index ba8a6acaa..000000000 --- a/contracts/arbitrum/NodeInterface.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * - * Originally copied from: - * https://github.com/OffchainLabs/arbitrum/tree/89e1f6234fe133253f445db44ec1612d57389c45/packages/arb-bridge-peripherals - * - * MODIFIED from Offchain Labs' implementation: - * - Changed max solidity version to <0.8.0 (pablo@edgeandnode.com) - * - */ - -pragma solidity >=0.4.21 <0.8.0; - -/** @title Interface for providing Outbox proof data - * @notice This contract doesn't exist on-chain. Instead it is a virtual interface accessible at 0x00000000000000000000000000000000000000C8 - * This is a cute trick to allow an Arbitrum node to provide data without us having to implement an additional RPC ) - */ - -interface NodeInterface { - /** - * @notice Returns the proof necessary to redeem a message - * @param batchNum index of outbox entry (i.e., outgoing messages Merkle root) in array of outbox entries - * @param index index of outgoing message in outbox entry - * @return proof Merkle proof of message inclusion in outbox entry - * @return path Merkle path to message - * @return l2Sender sender if original message (i.e., caller of ArbSys.sendTxToL1) - * @return l1Dest destination address for L1 contract call - * @return l2Block l2 block number at which sendTxToL1 call was made - * @return l1Block l1 block number at which sendTxToL1 call was made - * @return timestamp l2 Timestamp at which sendTxToL1 call was made - * @return amount value in L1 message in wei - * @return calldataForL1 abi-encoded L1 message data - */ - function lookupMessageBatchProof(uint256 batchNum, uint64 index) - external - view - returns ( - bytes32[] memory proof, - uint256 path, - address l2Sender, - address l1Dest, - uint256 l2Block, - uint256 l1Block, - uint256 timestamp, - uint256 amount, - bytes memory calldataForL1 - ); - - /** - * @notice Estimate the cost of putting a message in the L2 inbox that is reexecuted - * @param sender sender of the L1 and L2 transaction - * @param deposit amount to deposit to sender in L2 - * @param destAddr destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress maxgas x gasprice - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param maxGas Max gas deducted from user's L2 balance to cover L2 execution - * @param gasPriceBid price bid for L2 execution - * @param data ABI encoded data of L2 message - * @return gas used, and gas price to execute this transaction - */ - function estimateRetryableTicket( - address sender, - uint256 deposit, - address destAddr, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 maxGas, - uint256 gasPriceBid, - bytes calldata data - ) external pure returns (uint256, uint256); -} diff --git a/contracts/tests/arbitrum/ArbRetryableTxStub.sol b/contracts/tests/arbitrum/ArbRetryableTxStub.sol deleted file mode 100644 index 3b2a32702..000000000 --- a/contracts/tests/arbitrum/ArbRetryableTxStub.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -// This only exists so that our hardhat build gives us an ABI artifact for ArbRetryableTx - -pragma solidity ^0.7.6; - -import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; - -abstract contract ArbRetryableTxStub is ArbRetryableTx {} diff --git a/package.json b/package.json index 01acdd9fc..4655d1886 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "ethers": "^5.6.0" }, "devDependencies": { - "@arbitrum/sdk": "^1.1.2", + "@arbitrum/sdk": "^3.0.0-beta.5", "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", "@defi-wonderland/smock": "^2.0.7", diff --git a/yarn.lock b/yarn.lock index fb438e232..eed2251bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@arbitrum/sdk@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-1.1.2.tgz#3c29a4d333791781dbd61410fe635a57b595aa71" - integrity sha512-ILgdiOqezBs2loa703YtG4W10arxgrfWbaL17e7dnjp8q2yhjdWF0iTSx+Ep/mTVPhVE/9cOT/S/XuUmxdqiMQ== +"@arbitrum/sdk@^3.0.0-beta.5": + version "3.0.0-beta.5" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.0.0-beta.5.tgz#ef1c81de58db9e76defb4a1971274316a375133b" + integrity sha512-qeNdK7es4uKRFciz4zznPEnGRZaAHkrwNqUN1F4U6d4i8olhK0KMdSodx2ZjajBvVVwOo5kFsw5ocAaTvkf28g== dependencies: "@ethersproject/address" "^5.0.8" "@ethersproject/bignumber" "^5.1.1" @@ -13,9 +13,6 @@ "@typechain/ethers-v5" "9.0.0" "@types/prompts" "^2.0.14" "@types/yargs" "^17.0.9" - arb-bridge-eth "0.7.5" - arb-bridge-peripherals "1.0.6" - arbos-precompiles "1.0.2" dotenv "^10.0.0" ethers "^5.1.0" ts-node "^10.2.1" @@ -1316,17 +1313,12 @@ "@types/sinon-chai" "^3.2.3" "@types/web3" "1.0.19" -"@openzeppelin/contracts-0.8@npm:@openzeppelin/contracts@^4.3.2": - version "4.5.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.5.0.tgz#3fd75d57de172b3743cdfc1206883f56430409cc" - integrity sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA== - "@openzeppelin/contracts-upgradeable@3.4.2": version "3.4.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-3.4.2.tgz#2c2a1b0fa748235a1f495b6489349776365c51b3" integrity sha512-mDlBS17ymb2wpaLcrqRYdnBAmP1EwqhOXMvqWk2c5Q1N1pm5TkiCtXM9Xzznh4bYsQBq0aIWEkFFE2+iLSN1Tw== -"@openzeppelin/contracts@3.4.2", "@openzeppelin/contracts@^3.4.1": +"@openzeppelin/contracts@^3.4.1": version "3.4.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== @@ -1354,20 +1346,6 @@ proper-lockfile "^4.1.1" solidity-ast "^0.4.15" -"@openzeppelin/upgrades-core@^1.7.6": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.14.1.tgz#a0e1c83f9811186ac49d286e6b43ae129097422b" - integrity sha512-iKlh1mbUxyfdjdEiUFyhMkqirfas+DMUu7ED53nZbHEyhcYsm+5Fl/g0Bv6bZA+a7k8kO8+22DNEKsqaDUBc2Q== - dependencies: - bn.js "^5.1.2" - cbor "^8.0.0" - chalk "^4.1.0" - compare-versions "^4.0.0" - debug "^4.1.1" - ethereumjs-util "^7.0.3" - proper-lockfile "^4.1.1" - solidity-ast "^0.4.15" - "@resolver-engine/core@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@resolver-engine/core/-/core-0.3.3.tgz#590f77d85d45bc7ecc4e06c654f41345db6ca967" @@ -2246,30 +2224,7 @@ anymatch@~3.1.1, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -arb-bridge-eth@0.7.5: - version "0.7.5" - resolved "https://registry.yarnpkg.com/arb-bridge-eth/-/arb-bridge-eth-0.7.5.tgz#465a304b145eaad0af168e63192f714ee02cb025" - integrity sha512-OzsOqKBdU7AwmMJS1Pt27yqt/tYIteSXUIYNJYahiM7lUZk2ZsnPRnYKUc64ONDpTo/VGhO0cVXGjkrseBeJrA== - dependencies: - "@openzeppelin/contracts" "3.4.2" - "@openzeppelin/contracts-0.8" "npm:@openzeppelin/contracts@^4.3.2" - "@openzeppelin/contracts-upgradeable" "3.4.2" - hardhat "^2.6.1" - -arb-bridge-peripherals@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/arb-bridge-peripherals/-/arb-bridge-peripherals-1.0.6.tgz#91024740b96bf10fc43bf6582ee0debf93cf3d81" - integrity sha512-nrarOZ72VEo/DprRO+CPaD4dwM9EuhHqZ/pyPjTjgprLtvQ9gVewIi0O8dtM2E83JC7O3zUxHOPBtTLuJaZ/oQ== - dependencies: - "@openzeppelin/contracts" "3.4.2" - "@openzeppelin/contracts-upgradeable" "3.4.2" - arb-bridge-eth "0.7.5" - arbos-precompiles "^1.0.2" - hardhat "^2.6.1" - optionalDependencies: - "@openzeppelin/upgrades-core" "^1.7.6" - -arbos-precompiles@1.0.2, arbos-precompiles@^1.0.2: +arbos-precompiles@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/arbos-precompiles/-/arbos-precompiles-1.0.2.tgz#7bebd5963aef972cd259eb41f3116ea065013ea6" integrity sha512-1dOFYFJUN0kKoofh6buZJ8qCqTs+oLGSsGzHI0trA/Pka/TCERflCRsNVxez2lihOvK7MT/a2RA8AepKtBXdPQ== @@ -6602,7 +6557,7 @@ hardhat-tracer@^1.0.0-alpha.6: dependencies: ethers "^5.0.24" -hardhat@^2.6.1, hardhat@^2.6.4: +hardhat@^2.6.4: version "2.9.3" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.9.3.tgz#4759dc3c468c7d15f34334ca1be7d59b04e47b1e" integrity sha512-7Vw99RbYbMZ15UzegOR/nqIYIqddZXvLwJGaX5sX4G5bydILnbjmDU6g3jMKJSiArEixS3vHAEaOs5CW1JQ3hg== From 500e86b1de7c33fb7f6587585486769f36c27667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 28 Jul 2022 14:11:12 +0200 Subject: [PATCH 50/78] fix: change to the new Goerli Arb testnet --- arbitrum-addresses.json | 10 +++---- cli/commands/bridge/to-l1.ts | 6 ++--- cli/commands/bridge/to-l2.ts | 6 ++--- cli/utils.ts | 52 +----------------------------------- hardhat.config.ts | 6 ++--- 5 files changed, 15 insertions(+), 65 deletions(-) diff --git a/arbitrum-addresses.json b/arbitrum-addresses.json index 5a4655cf2..4249270ad 100644 --- a/arbitrum-addresses.json +++ b/arbitrum-addresses.json @@ -1,5 +1,5 @@ { - "source": "https://github.com/OffchainLabs/arbitrum/tree/f54baf10871ee86aedca4880796342ef9bd0b0ab/packages/", + "source": "https://developer.offchainlabs.com/docs/useful_addresses", "1": { "L1GatewayRouter": { "address": "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef" @@ -18,10 +18,10 @@ }, "5": { "L1GatewayRouter": { - "address": "0x8BDFa67ace22cE2BFb2fFebe72f0c91CDA694d4b" + "address": "0x4c7708168395aEa569453Fc36862D2ffcDaC588c" }, "IInbox": { - "address": "0x1FdBBcC914e84aF593884bf8e8Dd6877c29035A2" + "address": "0x6BEbC4925716945D46F0Ec336D5C2564F419682C" } }, "42161": { @@ -34,9 +34,9 @@ "address": "0x9413AD42910c1eA60c737dB5f58d1C504498a3cD" } }, - "421612": { + "421613": { "L2GatewayRouter": { - "address": "0xC502Ded1EE1d616B43F7f20Ebde83Be1A275ca3c" + "address": "0xE5B9d8d42d656d1DcB8065A6c012FE3780246041" } } } diff --git a/cli/commands/bridge/to-l1.ts b/cli/commands/bridge/to-l1.ts index 3796bfd30..f3673a53a 100644 --- a/cli/commands/bridge/to-l1.ts +++ b/cli/commands/bridge/to-l1.ts @@ -2,7 +2,7 @@ import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' import { getAddressBook } from '../../address-book' import { getProvider, sendTransaction, toGRT } from '../../network' -import { chainIdIsL2, createNitroNetwork } from '../../utils' +import { chainIdIsL2 } from '../../utils' import { loadAddressBookContract } from '../../contracts' import { L2TransactionReceipt, @@ -67,7 +67,7 @@ export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Prom logger.info(`>>> Sending tokens to L1 <<<\n`) const l2Provider = getProvider(cliArgs.l2ProviderUrl) const l2ChainId = (await l2Provider.getNetwork()).chainId - createNitroNetwork(cliArgs.providerUrl) + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', @@ -120,7 +120,7 @@ export const finishSendToL1 = async ( logger.info(`>>> Finishing transaction sending tokens to L1 <<<\n`) const l2Provider = getProvider(cliArgs.l2ProviderUrl) const l2ChainId = (await l2Provider.getNetwork()).chainId - createNitroNetwork(cliArgs.providerUrl) + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', diff --git a/cli/commands/bridge/to-l2.ts b/cli/commands/bridge/to-l2.ts index b806adad8..f176bc482 100644 --- a/cli/commands/bridge/to-l2.ts +++ b/cli/commands/bridge/to-l2.ts @@ -9,7 +9,7 @@ import { L1ToL2MessageWriter, L1ToL2MessageGasEstimator, } from '@arbitrum/sdk' -import { chainIdIsL2, createNitroNetwork } from '../../utils' +import { chainIdIsL2 } from '../../utils' const logAutoRedeemReason = (autoRedeemRec) => { if (autoRedeemRec == null) { @@ -43,7 +43,7 @@ export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise>> Sending tokens to L2 <<<\n`) const l2Provider = getProvider(cliArgs.l2ProviderUrl) const l2ChainId = (await l2Provider.getNetwork()).chainId - createNitroNetwork(cliArgs.providerUrl) + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', @@ -117,7 +117,7 @@ export const redeemSendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Pro logger.info(`>>> Redeeming pending tokens on L2 <<<\n`) const l2Provider = getProvider(cliArgs.l2ProviderUrl) const l2ChainId = (await l2Provider.getNetwork()).chainId - createNitroNetwork(cliArgs.providerUrl) + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', diff --git a/cli/utils.ts b/cli/utils.ts index 59332d7fa..846c5ce9d 100644 --- a/cli/utils.ts +++ b/cli/utils.ts @@ -6,7 +6,7 @@ import { loadArtifact } from './artifacts' export const l1ToL2ChainIdMap = { '1': '42161', '4': '421611', - '5': '421612', + '5': '421613', } export const l2ChainIds = Object.values(l1ToL2ChainIdMap).map(Number) @@ -28,53 +28,3 @@ export const getProvider = (providerUrl: string, network?: number): providers.Js export const chainIdIsL2 = (chainId: number | string): boolean => { return l2ChainIds.includes(Number(chainId)) } - -export const createNitroNetwork = (l1ProviderUrl: string): void => { - addCustomNetwork({ - customL1Network: { - blockTime: 15, - chainID: 5, - explorerUrl: 'https://goerli.etherscan.io/', - isCustom: true, - name: 'Goerli', - partnerChainIDs: [421612], - rpcURL: l1ProviderUrl, - }, - customL2Network: { - chainID: 421612, - confirmPeriodBlocks: 960, - ethBridge: { - bridge: '0x9903a892da86c1e04522d63b08e5514a921e81df', - inbox: '0x1fdbbcc914e84af593884bf8e8dd6877c29035a2', - outboxes: { - '0xFDF2B11347dA17326BAF30bbcd3F4b09c4719584': 0, - }, - rollup: '0x767CfF8D8de386d7cbe91DbD39675132ba2f5967', - sequencerInbox: '0xb32f4257e05c56c53d46bbec9e85770eb52425d6', - }, - explorerUrl: 'https://nitro-devnet-explorer.arbitrum.io/', - isArbitrum: true, - isCustom: true, - name: 'ArbLocal', - partnerChainID: 5, - rpcURL: 'https://nitro-devnet.arbitrum.io/rpc', - tokenBridge: { - l1CustomGateway: '0x23D4e0D7Cb7AE7CF745E82262B17eb46535Ae819', - l1ERC20Gateway: '0x6336C4e811b2f7D17d45b6241Fd47F2E11621Ffb', - l1GatewayRouter: '0x8BDFa67ace22cE2BFb2fFebe72f0c91CDA694d4b', - l1MultiCall: '0x90863B80f274b6D2227b01f2c1de4fdCb04896E2', - l1ProxyAdmin: '0x678cC9702ebF79d741E4f815937475311A58404a', - l1Weth: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - l1WethGateway: '0x64bfF696bE6a087A81936b9a2489624015381be4', - l2CustomGateway: '0x7AC493f26EF26904E52fE46C8DaEE247b9A556B8', - l2ERC20Gateway: '0xf298434ffE691400b932f4b14B436f451F4CED76', - l2GatewayRouter: '0xC502Ded1EE1d616B43F7f20Ebde83Be1A275ca3c', - l2Multicall: '0x1068dbfcc13f3a22fcAe684943AFA43cc66fA689', - l2ProxyAdmin: '0x1F2715AaC7EeFb75ebCc478f3D9a361fa47A95DD', - l2Weth: '0x96CfA560e7332DebA750e330fb6f59E2269f40Dd', - l2WethGateway: '0xf10c7CAA33A3360f60053Bc1081980f62567505F', - }, - retryableLifetimeSeconds: 608400, - }, - }) -} diff --git a/hardhat.config.ts b/hardhat.config.ts index e5fb9ad91..89cd6ec63 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -63,9 +63,9 @@ const networkConfigs: NetworkConfig[] = [ { network: 'arbitrum-rinkeby', chainId: 421611, url: 'https://rinkeby.arbitrum.io/rpc' }, { network: 'arbitrum-one', chainId: 42161, url: 'https://arb1.arbitrum.io/rpc' }, { - network: 'arbitrum-nitro-devnet', - chainId: 421612, - url: 'https://nitro-devnet.arbitrum.io/rpc', + network: 'arbitrum-goerli', + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', }, ] From 4b9491f230617a140361769b59ec01700a5added Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Sun, 5 Jun 2022 15:17:14 -0700 Subject: [PATCH 51/78] feat: keeper reward for reservoir drip through token issuance --- contracts/l2/reservoir/IL2Reservoir.sol | 11 +- contracts/l2/reservoir/L2Reservoir.sol | 15 +- contracts/l2/reservoir/L2ReservoirStorage.sol | 5 + contracts/reservoir/L1Reservoir.sol | 58 +++++- contracts/reservoir/L1ReservoirStorage.sol | 7 + test/l2/l2Reservoir.test.ts | 28 ++- test/reservoir/l1Reservoir.test.ts | 170 ++++++++++++++---- test/rewards/rewards.test.ts | 6 +- 8 files changed, 254 insertions(+), 46 deletions(-) diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol index 90b089ac9..5fd2fc861 100644 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -18,13 +18,22 @@ interface IL2Reservoir is IReservoir { * updates the issuanceBase and issuanceRate, * and snapshots the accumulated rewards. If issuanceRate changes, * it also triggers a snapshot of rewards per signal on the RewardsManager. + * Note that the transaction might revert if it's received out-of-order, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * A keeper reward will be sent to the keeper that dripped on L1, and part of it + * to whoever redeemed the current retryable ticket (tx.origin) * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _l1Keeper Address of the keeper that called drip in L1 */ function receiveDrip( uint256 _issuanceBase, uint256 _issuanceRate, - uint256 _nonce + uint256 _nonce, + uint256 _keeperReward, + address _l1Keeper ) external; } diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 1bc93c912..3b1dd131d 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -16,7 +16,7 @@ import "./L2ReservoirStorage.sol"; * It receives tokens for rewards from L1, and provides functions to compute accumulated and new * total rewards at a particular block number. */ -contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { +contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { using SafeMath for uint256; event DripReceived(uint256 _issuanceBase); @@ -88,14 +88,20 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * Note that the transaction might revert if it's received out-of-order, * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. + * A keeper reward will be sent to the keeper that dripped on L1, and part of it + * to whoever redeemed the current retryable ticket (tx.origin) * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _l1Keeper Address of the keeper that called drip in L1 */ function receiveDrip( uint256 _issuanceBase, uint256 _issuanceRate, - uint256 _nonce + uint256 _nonce, + uint256 _keeperReward, + address _l1Keeper ) external override onlyL2Gateway { require(_nonce == nextDripNonce, "INVALID_NONCE"); nextDripNonce = nextDripNonce.add(1); @@ -108,6 +114,11 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { snapshotAccumulatedRewards(); } issuanceBase = _issuanceBase; + uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS); + IGraphToken grt = graphToken(); + // solhint-disable-next-line avoid-tx-origin + grt.transfer(tx.origin, _l2KeeperReward); + grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); emit DripReceived(issuanceBase); } diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol index ee7880343..4e8c6825d 100644 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -9,3 +9,8 @@ contract L2ReservoirV1Storage { // Expected nonce value for the next drip hook uint256 public nextDripNonce; } + +contract L2ReservoirV2Storage is L2ReservoirV1Storage { + // Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18) + uint256 public l2KeeperRewardFraction; +} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 674e127ef..0e9059afa 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -17,7 +17,7 @@ import "./L1ReservoirStorage.sol"; * It provides a function to periodically drip rewards, and functions to compute accumulated and new * total rewards at a particular block number. */ -contract L1Reservoir is L1ReservoirV1Storage, Reservoir { +contract L1Reservoir is L1ReservoirV2Storage, Reservoir { using SafeMath for uint256; // Emitted when the initial supply snapshot is taken after contract deployment @@ -38,6 +38,10 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { event RewardsDripped(uint256 totalMinted, uint256 sentToL2, uint256 nextDeadline); // Emitted when the address for the L2Reservoir is updated event L2ReservoirAddressUpdated(address l2ReservoirAddress); + // Emitted when drip reward per block is updated + event DripRewardPerBlockUpdated(uint256 dripRewardPerBlock); + // Emitted when minDripInterval is updated + event MinDripIntervalUpdated(uint256 minDripInterval); /** * @dev Initialize this contract. @@ -105,6 +109,26 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { emit L2RewardsFractionStaged(_l2RewardsFraction); } + /** + * @dev Sets the drip reward per block + * This is the reward in GRT provided to the keeper that calls drip() + * @param _dripRewardPerBlock GRT accrued for each block after the threshold + */ + function setDripRewardPerBlock(uint256 _dripRewardPerBlock) external onlyGovernor { + dripRewardPerBlock = _dripRewardPerBlock; + emit DripRewardPerBlockUpdated(_dripRewardPerBlock); + } + + /** + * @dev Sets the minimum drip interval + * This is the minimum number of blocks between two successful drips + * @param _minDripInterval Minimum number of blocks since last drip for drip to be allowed + */ + function setMinDripInterval(uint256 _minDripInterval) external onlyGovernor { + minDripInterval = _minDripInterval; + emit MinDripIntervalUpdated(_minDripInterval); + } + /** * @dev Sets the L2 Reservoir address * This is the address on L2 to which we send tokens for rewards. @@ -153,16 +177,23 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) */ function drip( uint256 _l2MaxGas, uint256 _l2GasPriceBid, uint256 _l2MaxSubmissionCost + address _keeperRewardBeneficiary ) external payable notPaused { + require(block.number > lastRewardsUpdateBlock + minDripInterval, "WAIT_FOR_MIN_INTERVAL"); + uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); uint256 mintedRewardsActual = getNewGlobalRewards(block.number); // eps = (signed int) mintedRewardsTotal - mintedRewardsActual + uint256 keeperReward = dripRewardPerBlock.mul( + block.number.sub(lastRewardsUpdateBlock).sub(minDripInterval) + ); if (nextIssuanceRate != issuanceRate) { rewardsManager().updateAccRewardsPerSignal(); snapshotAccumulatedRewards(mintedRewardsActual); // This updates lastRewardsUpdateBlock @@ -178,7 +209,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // N = n - eps uint256 tokensToMint; { - uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual); + uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual).add(keeperReward); require( newRewardsPlusMintedActual >= mintedRewardsTotal, "Would mint negative tokens, wait before calling again" @@ -186,8 +217,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { tokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); } + IGraphToken grt = graphToken(); if (tokensToMint > 0) { - graphToken().mint(address(this), tokensToMint); + grt.mint(address(this), tokensToMint); } uint256 tokensToSendToL2 = 0; @@ -223,7 +255,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { tokensToSendToL2, _l2MaxGas, _l2GasPriceBid, - _l2MaxSubmissionCost + _l2MaxSubmissionCost, + keeperReward, + _keeperRewardBeneficiary ); } else if (l2RewardsFraction > 0) { tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(FIXED_POINT_SCALING_FACTOR); @@ -231,12 +265,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { tokensToSendToL2, _l2MaxGas, _l2GasPriceBid, - _l2MaxSubmissionCost + _l2MaxSubmissionCost, + keeperReward, + _keeperRewardBeneficiary ); } else { // Avoid locking funds in this contract if we don't need to // send a message to L2. require(msg.value == 0, "No eth value needed"); + // If we don't send rewards to L2, pay the keeper reward in L1 + grt.transfer(_keeperRewardBeneficiary, keeperReward); } emit RewardsDripped(tokensToMint, tokensToSendToL2, rewardsMintedUntilBlock); } @@ -317,12 +355,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _maxGas Max gas for the L2 retryable ticket execution * @param _gasPriceBid Gas price for the L2 retryable ticket execution * @param _maxSubmissionCost Max submission price for the L2 retryable ticket + * @param _keeperReward Tokens to assign as keeper reward for calling drip + * @param _keeper Address of the keeper that will be rewarded */ function _sendNewTokensAndStateToL2( uint256 _nTokens, uint256 _maxGas, uint256 _gasPriceBid, - uint256 _maxSubmissionCost + uint256 _maxSubmissionCost, + uint256 _keeperReward, + address _keeper ) internal { uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div( FIXED_POINT_SCALING_FACTOR @@ -331,7 +373,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { IL2Reservoir.receiveDrip.selector, l2IssuanceBase, issuanceRate, - nextDripNonce + nextDripNonce, + _keeperReward, + _keeper ); nextDripNonce = nextDripNonce.add(1); bytes memory data = abi.encode(_maxSubmissionCost, extraData); diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol index 90821c809..04bb2aa31 100644 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -21,3 +21,10 @@ contract L1ReservoirV1Storage { // Auto-incrementing nonce that will be used when sending rewards to L2, to ensure ordering uint256 public nextDripNonce; } + +contract L1ReservoirV2Storage is L1ReservoirV1Storage { + // Minimum number of blocks since last drip for a new drip to be allowed + uint256 public minDripInterval; + // Drip reward in GRT for each block since lastRewardsUpdateBlock + dripRewardThreshold + uint256 public dripRewardPerBlock; +} diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index ebfe3f52e..55c339c60 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -160,7 +160,13 @@ describe('L2Reservoir', () => { it('rejects the call when not called by the gateway', async function () { const tx = l2Reservoir .connect(governor.signer) - .receiveDrip(dripNormalizedSupply, dripIssuanceRate, toBN('0')) + .receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + toBN('0'), + testAccount1.address, + ) await expect(tx).revertedWith('ONLY_GATEWAY') }) it('rejects the call when received out of order', async function () { @@ -169,6 +175,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -181,6 +189,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply.add(1), dripIssuanceRate.add(1), toBN('2'), + toBN('0'), + testAccount1.address, ) const tx2 = gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -192,6 +202,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -205,6 +217,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -216,6 +230,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply.add(1), dripIssuanceRate.add(1), toBN('1'), + toBN('0'), + testAccount1.address, ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -230,6 +246,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -241,6 +259,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply.add(1), dripIssuanceRate, toBN('1'), + toBN('0'), + testAccount1.address, ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -255,6 +275,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -267,6 +289,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply.add(1), dripIssuanceRate, toBN('2'), + toBN('0'), + testAccount1.address, ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -285,6 +309,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, ISSUANCE_RATE_PER_BLOCK, toBN('0'), + toBN('0'), + testAccount1.address, ) await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index afc6233ca..86801149b 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -35,7 +35,7 @@ const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi const l2ReservoirIface = new Interface(l2ReservoirAbi) const { AddressZero } = constants -const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] +const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] const maxGas = toBN('1000000') const maxSubmissionCost = toBN('7') @@ -49,6 +49,7 @@ describe('L1Reservoir', () => { let mockL2GRT: Account let mockL2Gateway: Account let mockL2Reservoir: Account + let keeper: Account let fixture: NetworkFixture let grt: GraphToken @@ -120,7 +121,9 @@ describe('L1Reservoir', () => { expect(await tracker.accRewards(dripBlock)).to.eq(0) let expectedNextDeadline = dripBlock.add(dripInterval) let expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const tx1 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const tx1 = await l1Reservoir + .connect(keeper.signer) + .drip(toBN(0), toBN(0), toBN(0), keeper.address) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(await latestBlock()).eq(dripBlock) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) @@ -133,7 +136,9 @@ describe('L1Reservoir', () => { await advanceBlocks(blocksToAdvance) - const tx2 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const tx2 = await l1Reservoir + .connect(keeper.signer) + .drip(toBN(0), toBN(0), toBN(0), keeper.address) const newAmount = (await grt.balanceOf(l1Reservoir.address)).sub(actualAmount) expectedNextDeadline = (await latestBlock()).add(dripInterval) const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) @@ -147,7 +152,7 @@ describe('L1Reservoir', () => { } before(async function () { - ;[governor, testAccount1, mockRouter, mockL2GRT, mockL2Gateway, mockL2Reservoir] = + ;[governor, testAccount1, mockRouter, mockL2GRT, mockL2Gateway, mockL2Reservoir, keeper] = await getAccounts() fixture = new NetworkFixture() @@ -248,7 +253,7 @@ describe('L1Reservoir', () => { await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) expect(await l1Reservoir.issuanceRate()).eq(0) expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) - tx = l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + tx = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx).emit(l1Reservoir, 'IssuanceRateUpdated').withArgs(newIssuanceRate) expect(await l1Reservoir.issuanceRate()).eq(newIssuanceRate) }) @@ -313,12 +318,25 @@ describe('L1Reservoir', () => { await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) tx = l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) await expect(tx).emit(l1Reservoir, 'L2RewardsFractionUpdated').withArgs(newValue) expect(await l1Reservoir.l2RewardsFraction()).eq(newValue) }) }) + describe('minimum drip interval update', function () { + it('rejects setting minimum drip interval if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setMinDripInterval(toBN('200')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('sets the minimum drip interval', async function () { + const newValue = toBN('200') + const tx = l1Reservoir.connect(governor.signer).setMinDripInterval(newValue) + await expect(tx).emit(l1Reservoir, 'MinDripIntervalUpdated').withArgs(newValue) + expect(await l1Reservoir.minDripInterval()).eq(newValue) + }) + }) }) // TODO test that rewardsManager.updateAccRewardsPerSignal is called when @@ -337,7 +355,9 @@ describe('L1Reservoir', () => { expect(await tracker.accRewards(dripBlock)).to.eq(0) const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const tx = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const tx = await l1Reservoir + .connect(keeper.signer) + .drip(toBN(0), toBN(0), toBN(0), keeper.address) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) @@ -345,7 +365,7 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount, toBN(0), expectedNextDeadline) }) - it('has no effect if called a second time in the same block', async function () { + it('cannot be called more than once per minDripInterval', async function () { supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -358,14 +378,16 @@ describe('L1Reservoir', () => { expect(await tracker.accRewards(dripBlock)).to.eq(0) const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - await provider().send('evm_setAutomine', [false]) - const tx1 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) - const tx2 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) - await provider().send('evm_mine', []) - await provider().send('evm_setAutomine', [true]) + + const tx1 = await l1Reservoir + .connect(keeper.signer) + .drip(toBN(0), toBN(0), toBN(0), keeper.address) + + const minInterval = toBN('200') + await l1Reservoir.connect(governor.signer).setMinDripInterval(minInterval) const actualAmount = await grt.balanceOf(l1Reservoir.address) - expect(await latestBlock()).eq(dripBlock) // Just in case disabling automine stops working + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) await expect(tx1) .emit(l1Reservoir, 'RewardsDripped') @@ -373,15 +395,24 @@ describe('L1Reservoir', () => { await expect(tx1) .emit(grt, 'Transfer') .withArgs(AddressZero, l1Reservoir.address, actualAmount) - await expect(tx2) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(toBN(0), toBN(0), expectedNextDeadline) - await expect(tx2).not.emit(grt, 'Transfer') + + const tx2 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx2).revertedWith('WAIT_FOR_MIN_INTERVAL') + + // We've had 1 block since the last drip so far, so we jump to one block before the interval is done + await advanceBlocks(minInterval.sub(2)) + const tx3 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx3).revertedWith('WAIT_FOR_MIN_INTERVAL') + + await advanceBlocks(1) + // Now we're over the interval so we can drip again + const tx4 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx4).emit(l1Reservoir, 'RewardsDripped') }) it('prevents locking eth in the contract if l2RewardsFraction is 0', async function () { const tx = l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) await expect(tx).revertedWith('No eth value needed') }) it('mints only a few more tokens if called on the next block', async function () { @@ -413,8 +444,8 @@ describe('L1Reservoir', () => { const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -434,6 +465,69 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('0'), + toBN('0'), + keeper.address, + ]) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + }) + it('sends the specified fraction of the rewards with a keeper reward to L2', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).setDripRewardPerBlock(toGRT('3')) + await l1Reservoir.connect(governor.signer).setMinDripInterval(toBN('2')) + // lastRewardsUpdateBlock is set to block.number with initialSnapshot + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + await advanceBlocks(toBN('4')) + + // now we're at lastRewardsUpdateBlock + minDripInterval + 3, so keeper reward should be: + // dripRewardPerBlock * 3 + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedRewards = await tracker.accRewards(expectedNextDeadline) + const expectedMintedAmount = expectedMintedRewards.add(toGRT('9')) + const expectedSentToL2 = expectedMintedRewards.div(2) + const tx = await l1Reservoir + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + const normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('0'), + toGRT('9'), // keeper reward + keeper.address, ]) const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -462,8 +556,8 @@ describe('L1Reservoir', () => { const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -483,6 +577,8 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('0'), + toBN('0'), + keeper.address, ]) let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -511,8 +607,8 @@ describe('L1Reservoir', () => { .add(expectedTotalRewards.sub(rewardsUntilSecondDripBlock).mul(8).div(10)) const tx2 = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const newActualAmount = await grt.balanceOf(l1Reservoir.address) const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(newActualAmount)).to.eq( @@ -529,6 +625,8 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, ]) expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -564,8 +662,8 @@ describe('L1Reservoir', () => { const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -585,6 +683,8 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('0'), + toBN('0'), + keeper.address, ]) let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -610,8 +710,8 @@ describe('L1Reservoir', () => { const expectedNewTotalSentToL2 = expectedTotalRewards.div(2) const tx2 = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const newActualAmount = await grt.balanceOf(l1Reservoir.address) const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(newActualAmount)).to.eq( @@ -628,6 +728,8 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, ]) expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -654,7 +756,7 @@ describe('L1Reservoir', () => { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) supplyBeforeDrip = await grt.totalSupply() - await l1Reservoir.drip(toBN(0), toBN(0), toBN(0)) + await l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) dripBlock = await latestBlock() }) @@ -724,7 +826,9 @@ describe('L1Reservoir', () => { it('computes the rewards delta considering the L2 rewards fraction', async function () { const lambda = toGRT('0.32') await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) - await l1Reservoir.drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + await l1Reservoir + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) supplyBeforeDrip = await l1Reservoir.issuanceBase() // Has been updated accordingly dripBlock = await latestBlock() await advanceBlocks(20) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index b95b0464e..f5e18b2f9 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -42,6 +42,7 @@ describe('Rewards', () => { let indexer1: Account let indexer2: Account let oracle: Account + let keeper: Account let fixture: NetworkFixture @@ -107,7 +108,8 @@ describe('Rewards', () => { } before(async function () { - ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle] = await getAccounts() + ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle, keeper] = + await getAccounts() fixture = new NetworkFixture() ;({ grt, curation, epochManager, staking, rewardsManager, l1Reservoir } = await fixture.load( @@ -276,7 +278,7 @@ describe('Rewards', () => { beforeEach(async function () { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + await l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) dripBlock = await latestBlock() }) From 56de47a91784745d74e527e79791d9c86c861146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 7 Jun 2022 21:09:35 -0700 Subject: [PATCH 52/78] fix: don't use tx.origin as it will not work --- contracts/l2/reservoir/L2Reservoir.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 3b1dd131d..96a344f67 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -114,11 +114,15 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { snapshotAccumulatedRewards(); } issuanceBase = _issuanceBase; - uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS); IGraphToken grt = graphToken(); - // solhint-disable-next-line avoid-tx-origin - grt.transfer(tx.origin, _l2KeeperReward); - grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); + // We'd like to reward the keeper that redeemed the tx in L2 + // but this won't work right now as tx.origin will actually be the L1 sender. + // uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS); + // // solhint-disable-next-line avoid-tx-origin + // grt.transfer(tx.origin, _l2KeeperReward); + // grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); + // So for now we just send all the rewards to teh L1 keeper: + grt.transfer(_l1Keeper, _keeperReward); emit DripReceived(issuanceBase); } From a33bd303d7648a00f67045a01165c05e06f16b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 21 Jun 2022 15:37:33 +0300 Subject: [PATCH 53/78] fix: only allow indexers, their operators, or whitelisted addresses to call drip (needs tests) --- contracts/reservoir/L1Reservoir.sol | 122 ++++++++++++++++++++- contracts/reservoir/L1ReservoirStorage.sol | 2 + 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 0e9059afa..566181f43 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -42,6 +42,29 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { event DripRewardPerBlockUpdated(uint256 dripRewardPerBlock); // Emitted when minDripInterval is updated event MinDripIntervalUpdated(uint256 minDripInterval); + // Emitted when a new allowedDripper is added + event AllowedDripperAdded(address dripper); + // Emitted when an allowedDripper is revoked + event AllowedDripperRevoked(address dripper); + + /** + * @dev Checks that the sender is an indexer with stake on the Staking contract, + * or that the sender is an address whitelisted by governance to call. + */ + modifier onlyIndexerOrAllowedDripper() { + require(allowedDrippers[msg.sender] || _isIndexer(msg.sender), "UNAUTHORIZED"); + _; + } + + /** + * @dev Checks that the sender is an operator for the specified indexer + * (also checks that the specified indexer is, indeed, an indexer). + * @param _indexer Indexer for which the sender must be an operator + */ + modifier onlyIndexerOperator(address _indexer) { + require(_isIndexer(_indexer) && staking().isOperator(msg.sender, _indexer), "UNAUTHORIZED"); + _; + } /** * @dev Initialize this contract. @@ -140,6 +163,24 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { emit L2ReservoirAddressUpdated(_l2ReservoirAddress); } + /** + * @dev Grants an address permission to call drip() + * @param _dripper Address that will be an allowed dripper + */ + function grantDripPermission(address _dripper) external onlyGovernor { + allowedDrippers[_dripper] = true; + emit AllowedDripperAdded(_dripper); + } + + /** + * @dev Revokes an address' permission to call drip() + * @param _dripper Address that will not be an allowed dripper anymore + */ + function revokeDripPermission(address _dripper) external onlyGovernor { + allowedDrippers[_dripper] = false; + emit AllowedDripperRevoked(_dripper); + } + /** * @dev Computes the initial snapshot for token supply and mints any pending rewards * This will initialize the issuanceBase to the current GRT supply, after which @@ -164,7 +205,7 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. - * The call value must be equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must * only be nonzero if l2RewardsFraction is nonzero. * Calling this function can revert if the issuance rate has recently been reduced, and the existing * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait @@ -174,17 +215,83 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. + * This function with an additional parameter is only provided so that indexer operators can call it, + * specifying the indexer for which they are an operator. * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + * @param _indexer Indexer for whom the sender must be an authorized Operator */ function drip( uint256 _l2MaxGas, uint256 _l2GasPriceBid, - uint256 _l2MaxSubmissionCost + uint256 _l2MaxSubmissionCost, + address _keeperRewardBeneficiary, + address _indexer + ) external payable notPaused onlyIndexerOperator(_indexer) { + _drip(_l2MaxGas, _l2GasPriceBid, _l2MaxSubmissionCost, _keeperRewardBeneficiary); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2 + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + */ + function drip( + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost, address _keeperRewardBeneficiary - ) external payable notPaused { + ) external payable notPaused onlyIndexerOrAllowedDripper { + _drip(_l2MaxGas, _l2GasPriceBid, _l2MaxSubmissionCost, _keeperRewardBeneficiary); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2, private implementation. + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + */ + function _drip( + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost, + address _keeperRewardBeneficiary + ) private { require(block.number > lastRewardsUpdateBlock + minDripInterval, "WAIT_FOR_MIN_INTERVAL"); uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); @@ -391,4 +498,13 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { data ); } + + /** + * @dev Checks if an address is an indexer with stake in the Staking contract + * @param _indexer Address that will be checked + */ + function _isIndexer(address _indexer) internal view returns (bool) { + IStaking staking = staking(); + return staking.hasStake(_indexer); + } } diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol index 04bb2aa31..92b9d5107 100644 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -27,4 +27,6 @@ contract L1ReservoirV2Storage is L1ReservoirV1Storage { uint256 public minDripInterval; // Drip reward in GRT for each block since lastRewardsUpdateBlock + dripRewardThreshold uint256 public dripRewardPerBlock; + // True for addresses that are allowed to call drip() + mapping(address => bool) public allowedDrippers; } From 435abc2ae8027d10e9584bd5bafc27877a1dddcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 21 Jun 2022 17:21:47 +0300 Subject: [PATCH 54/78] test: fix rewards and reservoir tests after restricting drip callers --- contracts/reservoir/L1Reservoir.sol | 13 +- test/reservoir/l1Reservoir.test.ts | 208 +++++++++++++++++++++++----- test/rewards/rewards.test.ts | 20 ++- 3 files changed, 195 insertions(+), 46 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 566181f43..30fa4d82a 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -316,7 +316,9 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { // N = n - eps uint256 tokensToMint; { - uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual).add(keeperReward); + uint256 newRewardsPlusMintedActual = newRewardsToDistribute + .add(mintedRewardsActual) + .add(keeperReward); require( newRewardsPlusMintedActual >= mintedRewardsTotal, "Would mint negative tokens, wait before calling again" @@ -348,9 +350,9 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { tokensToSendToL2 > l2OffsetAmount, "Negative amount would be sent to L2, wait before calling again" ); - tokensToSendToL2 = tokensToSendToL2.sub(l2OffsetAmount); + tokensToSendToL2 = tokensToSendToL2.add(keeperReward).sub(l2OffsetAmount); } else { - tokensToSendToL2 = tokensToSendToL2.add( + tokensToSendToL2 = tokensToSendToL2.add(keeperReward).add( l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( FIXED_POINT_SCALING_FACTOR ) @@ -367,7 +369,10 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { _keeperRewardBeneficiary ); } else if (l2RewardsFraction > 0) { - tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(FIXED_POINT_SCALING_FACTOR); + tokensToSendToL2 = tokensToMint + .mul(l2RewardsFraction) + .div(FIXED_POINT_SCALING_FACTOR) + .add(keeperReward); _sendNewTokensAndStateToL2( tokensToSendToL2, _l2MaxGas, diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 86801149b..9c272c847 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai' -import { BigNumber, constants, utils } from 'ethers' +import { BigNumber, constants } from 'ethers' import { defaults, deployContract, deployL1Reservoir } from '../lib/deployment' import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' @@ -17,7 +17,6 @@ import { formatGRT, Account, RewardsTracker, - provider, } from '../lib/testHelpers' import { L1Reservoir } from '../../build/types/L1Reservoir' import { BridgeEscrow } from '../../build/types/BridgeEscrow' @@ -26,9 +25,9 @@ import path from 'path' import { Artifacts } from 'hardhat/internal/artifacts' import { Interface } from 'ethers/lib/utils' import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' -import { SubgraphDeploymentID } from '@graphprotocol/common-ts' import { Controller } from '../../build/types/Controller' import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' +import { Staking } from '../../build/types/Staking' const ARTIFACTS_PATH = path.resolve('build/contracts') const artifacts = new Artifacts(ARTIFACTS_PATH) const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi @@ -45,6 +44,7 @@ const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) describe('L1Reservoir', () => { let governor: Account let testAccount1: Account + let testAccount2: Account let mockRouter: Account let mockL2GRT: Account let mockL2Gateway: Account @@ -59,6 +59,7 @@ describe('L1Reservoir', () => { let l1GraphTokenGateway: L1GraphTokenGateway let controller: Controller let proxyAdmin: GraphProxyAdmin + let staking: Staking let supplyBeforeDrip: BigNumber let dripBlock: BigNumber @@ -123,7 +124,7 @@ describe('L1Reservoir', () => { let expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const tx1 = await l1Reservoir .connect(keeper.signer) - .drip(toBN(0), toBN(0), toBN(0), keeper.address) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(await latestBlock()).eq(dripBlock) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) @@ -138,7 +139,7 @@ describe('L1Reservoir', () => { const tx2 = await l1Reservoir .connect(keeper.signer) - .drip(toBN(0), toBN(0), toBN(0), keeper.address) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) const newAmount = (await grt.balanceOf(l1Reservoir.address)).sub(actualAmount) expectedNextDeadline = (await latestBlock()).add(dripInterval) const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) @@ -152,12 +153,20 @@ describe('L1Reservoir', () => { } before(async function () { - ;[governor, testAccount1, mockRouter, mockL2GRT, mockL2Gateway, mockL2Reservoir, keeper] = - await getAccounts() + ;[ + governor, + testAccount1, + mockRouter, + mockL2GRT, + mockL2Gateway, + mockL2Reservoir, + keeper, + testAccount2, + ] = await getAccounts() fixture = new NetworkFixture() fixtureContracts = await fixture.load(governor.signer) - ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway, controller, proxyAdmin } = + ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway, controller, proxyAdmin, staking } = fixtureContracts) await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) @@ -171,6 +180,7 @@ describe('L1Reservoir', () => { mockL2Gateway.address, mockL2Reservoir.address, ) + await l1Reservoir.connect(governor.signer).grantDripPermission(keeper.address) reservoirMock = (await deployContract( 'ReservoirMock', governor.signer, @@ -253,7 +263,9 @@ describe('L1Reservoir', () => { await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) expect(await l1Reservoir.issuanceRate()).eq(0) expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) - tx = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + tx = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx).emit(l1Reservoir, 'IssuanceRateUpdated').withArgs(newIssuanceRate) expect(await l1Reservoir.issuanceRate()).eq(newIssuanceRate) }) @@ -319,7 +331,13 @@ describe('L1Reservoir', () => { expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) tx = l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) await expect(tx).emit(l1Reservoir, 'L2RewardsFractionUpdated').withArgs(newValue) expect(await l1Reservoir.l2RewardsFraction()).eq(newValue) }) @@ -337,11 +355,74 @@ describe('L1Reservoir', () => { expect(await l1Reservoir.minDripInterval()).eq(newValue) }) }) + describe('allowed drippers whitelist', function () { + it('only allows the governor to add a dripper', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + .grantDripPermission(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('only allows the governor to revoke a dripper', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).revokeDripPermission(keeper.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('allows adding an address to the allowed drippers', async function () { + const tx = l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + await expect(tx).emit(l1Reservoir, 'AllowedDripperAdded').withArgs(testAccount1.address) + expect(await l1Reservoir.allowedDrippers(testAccount1.address)).eq(true) + }) + it('allows removing an address from the allowed drippers', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir.connect(governor.signer).revokeDripPermission(testAccount1.address) + await expect(tx).emit(l1Reservoir, 'AllowedDripperRevoked').withArgs(testAccount1.address) + expect(await l1Reservoir.allowedDrippers(testAccount1.address)).eq(false) + }) + }) }) // TODO test that rewardsManager.updateAccRewardsPerSignal is called when // issuanceRate or l2RewardsFraction is updated describe('drip', function () { + it('cannot be called by an unauthorized address', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).revertedWith('UNAUTHORIZED') + }) + it('can be called by an indexer', async function () { + const stakedAmount = toGRT('100000') + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) + it('can be called by a whitelisted address', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) + it('can be called by an indexer operator using an extra parameter', async function () { + const stakedAmount = toGRT('100000') + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + await staking.connect(testAccount1.signer).setOperator(testAccount2.address, true) + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount1.address, + ) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) it('mints rewards for the next week', async function () { supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) @@ -357,7 +438,7 @@ describe('L1Reservoir', () => { const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const tx = await l1Reservoir .connect(keeper.signer) - .drip(toBN(0), toBN(0), toBN(0), keeper.address) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) @@ -381,7 +462,7 @@ describe('L1Reservoir', () => { const tx1 = await l1Reservoir .connect(keeper.signer) - .drip(toBN(0), toBN(0), toBN(0), keeper.address) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) const minInterval = toBN('200') await l1Reservoir.connect(governor.signer).setMinDripInterval(minInterval) @@ -396,23 +477,35 @@ describe('L1Reservoir', () => { .emit(grt, 'Transfer') .withArgs(AddressZero, l1Reservoir.address, actualAmount) - const tx2 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + const tx2 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx2).revertedWith('WAIT_FOR_MIN_INTERVAL') // We've had 1 block since the last drip so far, so we jump to one block before the interval is done await advanceBlocks(minInterval.sub(2)) - const tx3 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + const tx3 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx3).revertedWith('WAIT_FOR_MIN_INTERVAL') await advanceBlocks(1) // Now we're over the interval so we can drip again - const tx4 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + const tx4 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx4).emit(l1Reservoir, 'RewardsDripped') }) it('prevents locking eth in the contract if l2RewardsFraction is 0', async function () { const tx = l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) await expect(tx).revertedWith('No eth value needed') }) it('mints only a few more tokens if called on the next block', async function () { @@ -445,7 +538,13 @@ describe('L1Reservoir', () => { const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -483,29 +582,37 @@ describe('L1Reservoir', () => { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) await l1Reservoir.connect(governor.signer).setDripRewardPerBlock(toGRT('3')) await l1Reservoir.connect(governor.signer).setMinDripInterval(toBN('2')) - // lastRewardsUpdateBlock is set to block.number with initialSnapshot - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + await advanceBlocks(toBN('4')) - // now we're at lastRewardsUpdateBlock + minDripInterval + 3, so keeper reward should be: - // dripRewardPerBlock * 3 supplyBeforeDrip = await grt.totalSupply() + const issuanceBase = await l1Reservoir.issuanceBase() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const expectedKeeperReward = dripBlock + .sub(await l1Reservoir.lastRewardsUpdateBlock()) + .sub(toBN('2')) + .mul(toGRT('3')) const tracker = await RewardsTracker.create( - supplyBeforeDrip, + issuanceBase, defaults.rewards.issuanceRate, dripBlock, ) expect(await tracker.accRewards(dripBlock)).to.eq(0) const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) const expectedMintedRewards = await tracker.accRewards(expectedNextDeadline) - const expectedMintedAmount = expectedMintedRewards.add(toGRT('9')) - const expectedSentToL2 = expectedMintedRewards.div(2) + const expectedMintedAmount = expectedMintedRewards.add(expectedKeeperReward) + const expectedSentToL2 = expectedMintedRewards.div(2).add(expectedKeeperReward) const tx = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) @@ -518,15 +625,15 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - const normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + const l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) const issuanceRate = await l1Reservoir.issuanceRate() const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('0'), - toGRT('9'), // keeper reward + expectedKeeperReward, keeper.address, ]) const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( @@ -557,7 +664,13 @@ describe('L1Reservoir', () => { const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -608,7 +721,13 @@ describe('L1Reservoir', () => { const tx2 = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const newActualAmount = await grt.balanceOf(l1Reservoir.address) const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(newActualAmount)).to.eq( @@ -663,7 +782,13 @@ describe('L1Reservoir', () => { const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -702,7 +827,6 @@ describe('L1Reservoir', () => { supplyBeforeDrip = await grt.totalSupply() const secondDripBlock = (await latestBlock()).add(1) const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) - const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) // The amount sent to L2 should cover up to the new drip block with the old fraction, @@ -711,7 +835,13 @@ describe('L1Reservoir', () => { const tx2 = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const newActualAmount = await grt.balanceOf(l1Reservoir.address) const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(newActualAmount)).to.eq( @@ -756,7 +886,9 @@ describe('L1Reservoir', () => { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) supplyBeforeDrip = await grt.totalSupply() - await l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) dripBlock = await latestBlock() }) @@ -828,7 +960,13 @@ describe('L1Reservoir', () => { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) supplyBeforeDrip = await l1Reservoir.issuanceBase() // Has been updated accordingly dripBlock = await latestBlock() await advanceBlocks(20) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index f5e18b2f9..2bcae309f 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -122,6 +122,7 @@ describe('Rewards', () => { await grt.connect(wallet.signer).approve(staking.address, toGRT('1000000')) await grt.connect(wallet.signer).approve(curation.address, toGRT('1000000')) } + await l1Reservoir.connect(governor.signer).grantDripPermission(keeper.address) await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() }) @@ -278,7 +279,9 @@ describe('Rewards', () => { beforeEach(async function () { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - await l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) dripBlock = await latestBlock() }) @@ -660,18 +663,15 @@ describe('Rewards', () => { describe('takeRewards', function () { it('should distribute rewards on closed allocation and stake', async function () { // Align with the epoch boundary - // dripBlock (81) await epochManager.setEpochLength(10) - // dripBlock + 1 await advanceToNextEpoch(epochManager) - // dripBlock + 4 + // Setup await setupIndexerAllocation() const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) - // dripBlock + 7 + // Jump await advanceToNextEpoch(epochManager) - // dripBlock + 14 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -797,7 +797,7 @@ describe('Rewards', () => { // Jump await advanceToNextEpoch(epochManager) - // dripBlock + 14 + // dripBlock + 13 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -839,17 +839,23 @@ describe('Rewards', () => { it('should deny and burn rewards if subgraph on denylist', async function () { // Setup + // dripBlock (82) await epochManager.setEpochLength(10) + // dripBlock + 1 await rewardsManager .connect(governor.signer) .setSubgraphAvailabilityOracle(governor.address) + // dripBlock + 2 await rewardsManager.connect(governor.signer).setDenied(subgraphDeploymentID1, true) + // dripBlock + 3 (epoch boundary!) await advanceToNextEpoch(epochManager) + // dripBlock + 13 await setupIndexerAllocation() const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) + // dripBlock + 23 const supplyBefore = await grt.totalSupply() // Close allocation. At this point rewards should be collected for that indexer From c4af2f615e32611a12dece9658f231162dbcf9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 23 Jun 2022 17:54:23 +0300 Subject: [PATCH 55/78] test: add a test for the keeper reward delivery in L2 --- test/l2/l2Reservoir.test.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index 55c339c60..bfa7026a8 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -106,6 +106,7 @@ describe('L2Reservoir', () => { const validGatewayFinalizeTransfer = async ( callhookData: string, + keeperReward = toGRT('0'), ): Promise => { const tx = await gatewayFinalizeTransfer(callhookData) await expect(tx) @@ -116,7 +117,7 @@ describe('L2Reservoir', () => { // newly minted GRT const receiverBalance = await grt.balanceOf(l2Reservoir.address) - await expect(receiverBalance).eq(dripAmount) + await expect(receiverBalance).eq(dripAmount.sub(keeperReward)) return tx } @@ -211,6 +212,26 @@ describe('L2Reservoir', () => { await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) }) + it('delivers the keeper reward to the beneficiary address', async function () { + normalizedSupply = dripNormalizedSupply + const reward = toBN('15') + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + reward, + testAccount1.address, + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount1.address, reward) + await expect(await grt.balanceOf(testAccount1.address)).to.eq(reward) + }) it('updates the normalized supply cache and issuance rate', async function () { normalizedSupply = dripNormalizedSupply let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( From 873b60051a29f0d61014adb3ca59723fabce7554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 27 Jun 2022 15:31:39 +0300 Subject: [PATCH 56/78] fix: provide part of the keeper reward to L2 redeemer --- contracts/l2/reservoir/L2Reservoir.sol | 58 +++++++++++++-- contracts/l2/reservoir/L2ReservoirStorage.sol | 2 + test/l2/l2Reservoir.test.ts | 73 ++++++++++++++++++- 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 96a344f67..8813ab73a 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -4,12 +4,22 @@ pragma solidity ^0.7.6; pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; +import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; import "../../reservoir/IReservoir.sol"; import "../../reservoir/Reservoir.sol"; import "./IL2Reservoir.sol"; import "./L2ReservoirStorage.sol"; +interface IArbTxWithRedeemer { + /** + * @notice Gets the redeemer of the current retryable redeem attempt. + * Returns the zero address if the current transaction is not a retryable redeem attempt. + * If this is an auto-redeem, returns the fee refund address of the retryable. + */ + function getCurrentRedeemer() external view returns (address); +} + /** * @title L2 Rewards Reservoir * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 2. @@ -19,8 +29,12 @@ import "./L2ReservoirStorage.sol"; contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { using SafeMath for uint256; + address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E; + event DripReceived(uint256 _issuanceBase); event NextDripNonceUpdated(uint256 _nonce); + event L1ReservoirAddressUpdated(address _l1ReservoirAddress); + event L2KeeperRewardFractionUpdated(uint256 _l2KeeperRewardFraction); /** * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. @@ -52,6 +66,29 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { emit NextDripNonceUpdated(_nonce); } + /** + * @dev Sets the L1 Reservoir address + * This is the address on L1 that will appear as redeemer when a ticket + * was auto-redeemed. + * @param _l1ReservoirAddress New address for the L1Reservoir on L1 + */ + function setL1ReservoirAddress(address _l1ReservoirAddress) external onlyGovernor { + l1ReservoirAddress = _l1ReservoirAddress; + emit L1ReservoirAddressUpdated(_l1ReservoirAddress); + } + + /** + * @dev Sets the L2 keeper reward fraction + * This is the fraction of the keeper reward that will be sent to the redeemer on L2 + * if the retryable ticket is not auto-redeemed + * @param _l2KeeperRewardFraction New value for the fraction, with fixed point at 1e18 + */ + function setL2KeeperRewardFraction(uint256 _l2KeeperRewardFraction) external onlyGovernor { + require(_l2KeeperRewardFraction <= FIXED_POINT_SCALING_FACTOR, "INVALID_VALUE"); + l2KeeperRewardFraction = _l2KeeperRewardFraction; + emit L2KeeperRewardFractionUpdated(_l2KeeperRewardFraction); + } + /** * @dev Get new total rewards accumulated since the last drip. * This is deltaR = p * r ^ (blocknum - t0) - p, where: @@ -115,14 +152,19 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { } issuanceBase = _issuanceBase; IGraphToken grt = graphToken(); - // We'd like to reward the keeper that redeemed the tx in L2 - // but this won't work right now as tx.origin will actually be the L1 sender. - // uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS); - // // solhint-disable-next-line avoid-tx-origin - // grt.transfer(tx.origin, _l2KeeperReward); - // grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); - // So for now we just send all the rewards to teh L1 keeper: - grt.transfer(_l1Keeper, _keeperReward); + + // Part of the reward always goes to whoever redeemed the ticket in L2, + // unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir + address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer(); + if (redeemer != l1ReservoirAddress) { + uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(FIXED_POINT_SCALING_FACTOR); + grt.transfer(redeemer, _l2KeeperReward); + grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); + } else { + // In an auto-redeem, we just send all the rewards to teh L1 keeper: + grt.transfer(_l1Keeper, _keeperReward); + } + emit DripReceived(issuanceBase); } diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol index 4e8c6825d..28c562901 100644 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -13,4 +13,6 @@ contract L2ReservoirV1Storage { contract L2ReservoirV2Storage is L2ReservoirV1Storage { // Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18) uint256 public l2KeeperRewardFraction; + // Address of the L1Reservoir on L1, used to check if a ticket was auto-redeemed + address public l1ReservoirAddress; } diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index bfa7026a8..0551c1e13 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -4,6 +4,7 @@ import { BigNumber, constants, ContractTransaction, utils } from 'ethers' import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' import { BigNumber as BN } from 'bignumber.js' +import { FakeContract, smock } from '@defi-wonderland/smock' import { advanceBlocks, @@ -30,11 +31,13 @@ const dripIssuanceRate = toBN('1000000023206889619') describe('L2Reservoir', () => { let governor: Account let testAccount1: Account + let testAccount2: Account let mockRouter: Account let mockL1GRT: Account let mockL1Gateway: Account let mockL1Reservoir: Account let fixture: NetworkFixture + let arbTxMock: FakeContract let grt: L2GraphToken let l2Reservoir: L2Reservoir @@ -122,7 +125,7 @@ describe('L2Reservoir', () => { } before(async function () { - ;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir] = + ;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir, testAccount2] = await getAccounts() fixture = new NetworkFixture() @@ -136,6 +139,11 @@ describe('L2Reservoir', () => { mockL1Gateway.address, mockL1Reservoir.address, ) + + arbTxMock = await smock.fake('IArbTxWithRedeemer', { + address: '0x000000000000000000000000000000000000006E', + }) + arbTxMock.getCurrentRedeemer.returns(mockL1Reservoir.address) }) beforeEach(async function () { @@ -157,7 +165,42 @@ describe('L2Reservoir', () => { await expect(await l2Reservoir.nextDripNonce()).to.eq(toBN('10')) }) }) + + describe('setL1ReservoirAddress', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir + .connect(testAccount1.signer) + .setL1ReservoirAddress(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets the L1Reservoir address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(testAccount1.address) + await expect(tx).emit(l2Reservoir, 'L1ReservoirAddressUpdated').withArgs(testAccount1.address) + await expect(await l2Reservoir.l1ReservoirAddress()).to.eq(testAccount1.address) + }) + }) + + describe('setL2KeeperRewardFraction', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir.connect(testAccount1.signer).setL2KeeperRewardFraction(toBN(1)) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('rejects invalid values (> 1)', async function () { + const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('1.000001')) + await expect(tx).revertedWith('INVALID_VALUE') + }) + it('sets the L1Reservoir address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.999')) + await expect(tx).emit(l2Reservoir, 'L2KeeperRewardFractionUpdated').withArgs(toGRT('0.999')) + await expect(await l2Reservoir.l2KeeperRewardFraction()).to.eq(toGRT('0.999')) + }) + }) + describe('receiveDrip', async function () { + beforeEach(async function () { + await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.2')) + await l2Reservoir.connect(governor.signer).setL1ReservoirAddress(mockL1Reservoir.address) + }) it('rejects the call when not called by the gateway', async function () { const tx = l2Reservoir .connect(governor.signer) @@ -224,7 +267,7 @@ describe('L2Reservoir', () => { ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) await expect(tx) @@ -232,6 +275,32 @@ describe('L2Reservoir', () => { .withArgs(l2Reservoir.address, testAccount1.address, reward) await expect(await grt.balanceOf(testAccount1.address)).to.eq(reward) }) + it('delivers part of the keeper reward to the L2 redeemer', async function () { + arbTxMock.getCurrentRedeemer.returns(testAccount2.address) + await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.25')) + normalizedSupply = dripNormalizedSupply + const reward = toGRT('16') + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + reward, + testAccount1.address, + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount1.address, toGRT('12')) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount2.address, toGRT('4')) + await expect(await grt.balanceOf(testAccount1.address)).to.eq(toGRT('12')) + await expect(await grt.balanceOf(testAccount2.address)).to.eq(toGRT('4')) + }) it('updates the normalized supply cache and issuance rate', async function () { normalizedSupply = dripNormalizedSupply let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( From 634477aa3f1d6d125676b90352043b0939d854af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 13:20:52 +0200 Subject: [PATCH 57/78] fix: clean up comments about redeemer --- contracts/l2/reservoir/IL2Reservoir.sol | 3 ++- contracts/l2/reservoir/L2Reservoir.sol | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol index 5fd2fc861..696e55797 100644 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -22,7 +22,8 @@ interface IL2Reservoir is IReservoir { * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. * A keeper reward will be sent to the keeper that dripped on L1, and part of it - * to whoever redeemed the current retryable ticket (tx.origin) + * to whoever redeemed the current retryable ticket (as reported by ArbRetryableTx.getCurrentRedeemer) if + * the ticket is not auto-redeemed. * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 8813ab73a..e52507f34 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -126,7 +126,8 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. * A keeper reward will be sent to the keeper that dripped on L1, and part of it - * to whoever redeemed the current retryable ticket (tx.origin) + * to whoever redeemed the current retryable ticket (as reported by ArbRetryableTx.getCurrentRedeemer) if + * the ticket is not auto-redeemed. * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order @@ -157,7 +158,9 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { // unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer(); if (redeemer != l1ReservoirAddress) { - uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(FIXED_POINT_SCALING_FACTOR); + uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div( + FIXED_POINT_SCALING_FACTOR + ); grt.transfer(redeemer, _l2KeeperReward); grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); } else { From b5429434cd390e61c9992606b60d95cf52c18664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 13:28:46 +0200 Subject: [PATCH 58/78] fix: more documentation details --- contracts/l2/reservoir/L2Reservoir.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index e52507f34..16f60e9e4 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -31,10 +31,10 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E; - event DripReceived(uint256 _issuanceBase); - event NextDripNonceUpdated(uint256 _nonce); - event L1ReservoirAddressUpdated(address _l1ReservoirAddress); - event L2KeeperRewardFractionUpdated(uint256 _l2KeeperRewardFraction); + event DripReceived(uint256 issuanceBase); + event NextDripNonceUpdated(uint256 nonce); + event L1ReservoirAddressUpdated(address l1ReservoirAddress); + event L2KeeperRewardFractionUpdated(uint256 l2KeeperRewardFraction); /** * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. @@ -50,6 +50,10 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { * are not set here because they are set from L1 through the drip function. * The RewardsManager's address might also not be available in the controller at initialization * time, so approveRewardsManager() must be called separately. + * The l1ReservoirAddress must also be set separately through setL1ReservoirAddress + * for the same reason. + * In the same vein, the l2KeeperRewardFraction is assumed to be zero at initialization, + * so it must be set through setL2KeeperRewardFraction. * @param _controller Address of the Controller that manages this contract */ function initialize(address _controller) external onlyImpl { From 6ef2faa68a84def6fdfa5b4e1b0ec52eee1a67a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 14:02:09 +0200 Subject: [PATCH 59/78] fix: use safe math for minDripInterval --- contracts/l2/reservoir/IL2Reservoir.sol | 2 +- contracts/l2/reservoir/L2Reservoir.sol | 4 ++-- contracts/reservoir/L1Reservoir.sol | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol index 696e55797..b8a2311c9 100644 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -27,7 +27,7 @@ interface IL2Reservoir is IReservoir { * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order - * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx * @param _l1Keeper Address of the keeper that called drip in L1 */ function receiveDrip( diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 16f60e9e4..8803fac68 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -135,7 +135,7 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order - * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx * @param _l1Keeper Address of the keeper that called drip in L1 */ function receiveDrip( @@ -168,7 +168,7 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { grt.transfer(redeemer, _l2KeeperReward); grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); } else { - // In an auto-redeem, we just send all the rewards to teh L1 keeper: + // In an auto-redeem, we just send all the rewards to the L1 keeper: grt.transfer(_l1Keeper, _keeperReward); } diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 30fa4d82a..807b56914 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -73,6 +73,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * to the drip function, that also requires the initial supply snapshot to be taken * using initialSnapshot. For this reason, issuanceRate and l2RewardsFraction * are not initialized here and instead need a call to setIssuanceRate and setL2RewardsFraction. + * The same applies to minDripInterval (set through setMinDripInterval) and dripRewardPerBlock + * (set through setDripRewardPerBlock). * On the other hand, the l2ReservoirAddress is not expected to be known at initialization * time and must therefore be set using setL2ReservoirAddress. * The RewardsManager's address might also not be available in the controller at initialization @@ -292,7 +294,10 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { uint256 _l2MaxSubmissionCost, address _keeperRewardBeneficiary ) private { - require(block.number > lastRewardsUpdateBlock + minDripInterval, "WAIT_FOR_MIN_INTERVAL"); + require( + block.number > lastRewardsUpdateBlock.add(minDripInterval), + "WAIT_FOR_MIN_INTERVAL" + ); uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); uint256 mintedRewardsActual = getNewGlobalRewards(block.number); From 1a6df5d6bc9b54ef0d0aab9dc9b07efae8919332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 14:46:21 +0200 Subject: [PATCH 60/78] fix: validate input when granting/revoking drip permission --- contracts/reservoir/L1Reservoir.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 807b56914..d9fb22f0e 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -170,6 +170,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * @param _dripper Address that will be an allowed dripper */ function grantDripPermission(address _dripper) external onlyGovernor { + require(_dripper != address(0), "INVALID_ADDRESS"); + require(!allowedDrippers[_dripper], "ALREADY_A_DRIPPER"); allowedDrippers[_dripper] = true; emit AllowedDripperAdded(_dripper); } @@ -179,6 +181,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * @param _dripper Address that will not be an allowed dripper anymore */ function revokeDripPermission(address _dripper) external onlyGovernor { + require(_dripper != address(0), "INVALID_ADDRESS"); + require(allowedDrippers[_dripper], "NOT_A_DRIPPER"); allowedDrippers[_dripper] = false; emit AllowedDripperRevoked(_dripper); } From 120753f2ed1413bf994f71e9de02a16ac75e3459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 28 Jul 2022 16:09:49 +0200 Subject: [PATCH 61/78] fix: docs and inheritance for IArbTxWithRedeemer --- contracts/l2/reservoir/L2Reservoir.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 8803fac68..839362ccd 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -11,7 +11,12 @@ import "../../reservoir/Reservoir.sol"; import "./IL2Reservoir.sol"; import "./L2ReservoirStorage.sol"; -interface IArbTxWithRedeemer { +/** + * @dev ArbRetryableTx with additional interface to query the current redeemer. + * This is being added by the Arbitrum team but hasn't made it into the arbos-precompiles + * package yet. + */ +interface IArbTxWithRedeemer is ArbRetryableTx { /** * @notice Gets the redeemer of the current retryable redeem attempt. * Returns the zero address if the current transaction is not a retryable redeem attempt. From 00d454704f58070457d7f0f7e71ecbea685f0931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 5 Aug 2022 14:17:51 +0200 Subject: [PATCH 62/78] fix: remove minDripInterval from the drip keeper reward calculation [L-01] --- contracts/reservoir/L1Reservoir.sol | 4 +--- test/reservoir/l1Reservoir.test.ts | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index d9fb22f0e..0e6fc5b47 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -307,9 +307,7 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { uint256 mintedRewardsActual = getNewGlobalRewards(block.number); // eps = (signed int) mintedRewardsTotal - mintedRewardsActual - uint256 keeperReward = dripRewardPerBlock.mul( - block.number.sub(lastRewardsUpdateBlock).sub(minDripInterval) - ); + uint256 keeperReward = dripRewardPerBlock.mul(block.number.sub(lastRewardsUpdateBlock)); if (nextIssuanceRate != issuanceRate) { rewardsManager().updateAccRewardsPerSignal(); snapshotAccumulatedRewards(mintedRewardsActual); // This updates lastRewardsUpdateBlock diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 9c272c847..0e5fdf415 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -592,7 +592,6 @@ describe('L1Reservoir', () => { const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction const expectedKeeperReward = dripBlock .sub(await l1Reservoir.lastRewardsUpdateBlock()) - .sub(toBN('2')) .mul(toGRT('3')) const tracker = await RewardsTracker.create( issuanceBase, From d36aab9647344dc357a6d9cd0e33fee04f3c832b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 8 Aug 2022 16:48:46 +0200 Subject: [PATCH 63/78] fix: use L2 alias of l1ReservoirAddress when comparing getCurrentRedeemer [H-01] --- contracts/l2/reservoir/L2Reservoir.sol | 3 ++- test/l2/l2Reservoir.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 839362ccd..2b8cd80fe 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -6,6 +6,7 @@ pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; +import "../../arbitrum/AddressAliasHelper.sol"; import "../../reservoir/IReservoir.sol"; import "../../reservoir/Reservoir.sol"; import "./IL2Reservoir.sol"; @@ -166,7 +167,7 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { // Part of the reward always goes to whoever redeemed the ticket in L2, // unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer(); - if (redeemer != l1ReservoirAddress) { + if (redeemer != AddressAliasHelper.applyL1ToL2Alias(l1ReservoirAddress)) { uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div( FIXED_POINT_SCALING_FACTOR ); diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index 0551c1e13..b7cfd2101 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -16,6 +16,7 @@ import { Account, RewardsTracker, getL2SignerFromL1, + applyL1ToL2Alias, } from '../lib/testHelpers' import { L2Reservoir } from '../../build/types/L2Reservoir' @@ -143,7 +144,7 @@ describe('L2Reservoir', () => { arbTxMock = await smock.fake('IArbTxWithRedeemer', { address: '0x000000000000000000000000000000000000006E', }) - arbTxMock.getCurrentRedeemer.returns(mockL1Reservoir.address) + arbTxMock.getCurrentRedeemer.returns(applyL1ToL2Alias(mockL1Reservoir.address)) }) beforeEach(async function () { From 2eea58c0edf1e8cbb506595f9ad64aaa45ca4ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 17 Aug 2022 13:30:58 +0200 Subject: [PATCH 64/78] fix: don't include keeper reward twice when computing what to send to L2 [H-03] [L-03] --- contracts/reservoir/L1Reservoir.sol | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 0e6fc5b47..8f62fb951 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -321,22 +321,18 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { // n = deltaR(t1, t0) uint256 newRewardsToDistribute = getNewGlobalRewards(rewardsMintedUntilBlock); // N = n - eps - uint256 tokensToMint; + uint256 rewardsTokensToMint; { - uint256 newRewardsPlusMintedActual = newRewardsToDistribute - .add(mintedRewardsActual) - .add(keeperReward); + uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual); require( - newRewardsPlusMintedActual >= mintedRewardsTotal, - "Would mint negative tokens, wait before calling again" + newRewardsPlusMintedActual > mintedRewardsTotal, + "Would mint negative or zero tokens, wait before calling again" ); - tokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); + rewardsTokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); } IGraphToken grt = graphToken(); - if (tokensToMint > 0) { - grt.mint(address(this), tokensToMint); - } + grt.mint(address(this), rewardsTokensToMint.add(keeperReward)); uint256 tokensToSendToL2 = 0; if (l2RewardsFraction != nextL2RewardsFraction) { @@ -376,7 +372,7 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { _keeperRewardBeneficiary ); } else if (l2RewardsFraction > 0) { - tokensToSendToL2 = tokensToMint + tokensToSendToL2 = rewardsTokensToMint .mul(l2RewardsFraction) .div(FIXED_POINT_SCALING_FACTOR) .add(keeperReward); @@ -395,7 +391,11 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { // If we don't send rewards to L2, pay the keeper reward in L1 grt.transfer(_keeperRewardBeneficiary, keeperReward); } - emit RewardsDripped(tokensToMint, tokensToSendToL2, rewardsMintedUntilBlock); + emit RewardsDripped( + rewardsTokensToMint.add(keeperReward), + tokensToSendToL2, + rewardsMintedUntilBlock + ); } /** From 448f506d09adc75740b88dcc8098a25c23b94c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 15:39:09 +0200 Subject: [PATCH 65/78] test: add test to ensure no DoS if l2RewardsFraction is zeroed [H-04] --- test/reservoir/l1Reservoir.test.ts | 139 +++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 0e5fdf415..64eedbf36 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -878,6 +878,145 @@ describe('L1Reservoir', () => { expectedNewNextDeadline, ) }) + + it('reverts for a while but can be called again later if L2 fraction goes to zero', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + + // First drip call, sending half the rewards to L2 + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('0'), + toBN('0'), + keeper.address, + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0')) + + // Second attempt to drip immediately afterwards will revert, because we + // would have to send negative tokens to L2 to compensate + const tx2 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + await expect(tx2).revertedWith( + 'Negative amount would be sent to L2, wait before calling again', + ) + + await advanceBlocks(await l1Reservoir.dripInterval()) + + // Now we should be able to drip again, and a small amount will be sent to L2 + // to cover the few blocks since the drip interval was over + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction, that is zero + const expectedNewTotalSentToL2 = rewardsUntilSecondDripBlock.div(2) + + const tx3 = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx3) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx3) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) }) context('calculating rewards', async function () { From cc51cb75c954e38fe821784da71eae49e7e3ca3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 15:40:10 +0200 Subject: [PATCH 66/78] test: optimize functions to advance blocks and fix some race conditions --- test/lib/testHelpers.ts | 23 ++++++++--------------- test/rewards/rewards.test.ts | 10 +++++----- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index 6673349e3..998d0d335 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -2,7 +2,7 @@ import hre from 'hardhat' import '@nomiclabs/hardhat-ethers' import '@nomiclabs/hardhat-waffle' import { providers, utils, BigNumber, Signer, Wallet } from 'ethers' -import { formatUnits, getAddress } from 'ethers/lib/utils' +import { formatUnits, getAddress, hexValue } from 'ethers/lib/utils' import { BigNumber as BN } from 'bignumber.js' import { EpochManager } from '../../build/types/EpochManager' @@ -66,24 +66,17 @@ export const advanceBlockTo = async (blockNumber: string | number | BigNumber): ? toBN(blockNumber) : blockNumber const currentBlock = await latestBlock() - const start = Date.now() - let notified - if (target.lt(currentBlock)) + if (target.lt(currentBlock)) { throw Error(`Target block #(${target}) is lower than current block #(${currentBlock})`) - while ((await latestBlock()).lt(target)) { - if (!notified && Date.now() - start >= 5000) { - notified = true - console.log(`advanceBlockTo: Advancing too ` + 'many blocks is causing this test to be slow.') - } - await advanceBlock() + } else if (target.eq(currentBlock)) { + return + } else { + await advanceBlocks(target.sub(currentBlock)) } } export const advanceBlocks = async (blocks: string | number | BigNumber): Promise => { - const steps = typeof blocks === 'number' || typeof blocks === 'string' ? toBN(blocks) : blocks - const currentBlock = await latestBlock() - const toBlock = currentBlock.add(steps) - return advanceBlockTo(toBlock) + await provider().send('hardhat_mine', [hexValue(BigNumber.from(blocks))]) } export const advanceToNextEpoch = async (epochManager: EpochManager): Promise => { @@ -185,7 +178,7 @@ export class RewardsTracker { async snapshotPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { this.accumulatedPerSignal = await this.accRewardsPerSignal(totalSignal, atBlock) this.accumulatedAtLastPerSignalUpdatedBlock = await this.accRewards(atBlock) - this.lastPerSignalUpdatedBlock = atBlock + this.lastPerSignalUpdatedBlock = atBlock || (await latestBlock()) return this.accumulatedPerSignal } diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 2bcae309f..38639acc4 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -79,7 +79,7 @@ describe('Rewards', () => { ) => { // -- t0 -- const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK, dripBlock) - tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) // Jump await advanceBlocks(nBlocks) @@ -330,11 +330,11 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, // so we snapshot using the previous value - tracker.snapshotPerSignal(prevSignal) + await tracker.snapshotPerSignal(prevSignal) // Update await rewardsManager.updateAccRewardsPerSignal() - tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) const contractAccrued = await rewardsManager.accRewardsPerSignal() @@ -359,14 +359,14 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, // so we snapshot using the previous value - tracker.snapshotPerSignal(prevSignal) + await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.updateAccRewardsPerSignal() - tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) const contractAccrued = await rewardsManager.accRewardsPerSignal() const blockNum = await latestBlock() From 0f8fb067dd92a291b49589cec9e8f85952e53b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 16:25:19 +0200 Subject: [PATCH 67/78] fix: add some missing validation on reservoirs [M-01] --- contracts/l2/reservoir/L2Reservoir.sol | 1 + contracts/reservoir/L1Reservoir.sol | 4 ++++ test/l2/l2Reservoir.test.ts | 4 ++++ test/reservoir/l1Reservoir.test.ts | 20 +++++++++++++++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 2b8cd80fe..1d6dddf2e 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -83,6 +83,7 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { * @param _l1ReservoirAddress New address for the L1Reservoir on L1 */ function setL1ReservoirAddress(address _l1ReservoirAddress) external onlyGovernor { + require(_l1ReservoirAddress != address(0), "INVALID_L1_RESERVOIR"); l1ReservoirAddress = _l1ReservoirAddress; emit L1ReservoirAddressUpdated(_l1ReservoirAddress); } diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 8f62fb951..aa25f308d 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -150,6 +150,7 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * @param _minDripInterval Minimum number of blocks since last drip for drip to be allowed */ function setMinDripInterval(uint256 _minDripInterval) external onlyGovernor { + require(_minDripInterval < dripInterval, "MUST_BE_LT_DRIP_INTERVAL"); minDripInterval = _minDripInterval; emit MinDripIntervalUpdated(_minDripInterval); } @@ -302,6 +303,9 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { block.number > lastRewardsUpdateBlock.add(minDripInterval), "WAIT_FOR_MIN_INTERVAL" ); + // Note we only validate that the beneficiary is nonzero, as the caller might + // want to send the reward to an address that is different to the indexer/dripper's address. + require(_keeperRewardBeneficiary != address(0), "INVALID_BENEFICIARY"); uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); uint256 mintedRewardsActual = getNewGlobalRewards(block.number); diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index b7cfd2101..26a9ccc75 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -174,6 +174,10 @@ describe('L2Reservoir', () => { .setL1ReservoirAddress(testAccount1.address) await expect(tx).revertedWith('Caller must be Controller governor') }) + it('rejects setting a zero address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(constants.AddressZero) + await expect(tx).revertedWith('INVALID_L1_RESERVOIR') + }) it('sets the L1Reservoir address', async function () { const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(testAccount1.address) await expect(tx).emit(l2Reservoir, 'L1ReservoirAddressUpdated').withArgs(testAccount1.address) diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 64eedbf36..2ea01ac6d 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -347,7 +347,18 @@ describe('L1Reservoir', () => { const tx = l1Reservoir.connect(testAccount1.signer).setMinDripInterval(toBN('200')) await expect(tx).revertedWith('Caller must be Controller governor') }) - + it('rejects setting minimum drip interval if equal to dripInterval', async function () { + const tx = l1Reservoir + .connect(governor.signer) + .setMinDripInterval(await l1Reservoir.dripInterval()) + await expect(tx).revertedWith('MUST_BE_LT_DRIP_INTERVAL') + }) + it('rejects setting minimum drip interval if larger than dripInterval', async function () { + const tx = l1Reservoir + .connect(governor.signer) + .setMinDripInterval((await l1Reservoir.dripInterval()).add(1)) + await expect(tx).revertedWith('MUST_BE_LT_DRIP_INTERVAL') + }) it('sets the minimum drip interval', async function () { const newValue = toBN('200') const tx = l1Reservoir.connect(governor.signer).setMinDripInterval(newValue) @@ -406,6 +417,13 @@ describe('L1Reservoir', () => { ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) await expect(tx).emit(l1Reservoir, 'RewardsDripped') }) + it('cannot be called with a zero address for the keeper reward beneficiary', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), constants.AddressZero) + await expect(tx).revertedWith('INVALID_BENEFICIARY') + }) it('can be called by an indexer operator using an extra parameter', async function () { const stakedAmount = toGRT('100000') await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) From 365e307d021ee84cc8ceb69f51a1961ba4a98c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 17:59:10 +0200 Subject: [PATCH 68/78] fix: add some missing docstrings [L-04] --- contracts/l2/reservoir/L2Reservoir.sol | 5 +++++ contracts/l2/reservoir/L2ReservoirStorage.sol | 6 +++++- contracts/reservoir/L1ReservoirStorage.sol | 6 +++++- contracts/reservoir/Reservoir.sol | 2 ++ contracts/reservoir/ReservoirStorage.sol | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 1d6dddf2e..9a341b6b5 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -35,11 +35,16 @@ interface IArbTxWithRedeemer is ArbRetryableTx { contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { using SafeMath for uint256; + // Address for the ArbRetryableTx interface provided by Arbitrum address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E; + // Emitted when a rewards drip is received from L1 event DripReceived(uint256 issuanceBase); + // Emitted when the next drip nonce is manually updated by governance event NextDripNonceUpdated(uint256 nonce); + // Emitted when the L1Reservoir's address is updated event L1ReservoirAddressUpdated(address l1ReservoirAddress); + // Emitted when the L2 keeper reward fraction is updated event L2KeeperRewardFractionUpdated(uint256 l2KeeperRewardFraction); /** diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol index 28c562901..4d469f889 100644 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -3,13 +3,17 @@ pragma solidity ^0.7.6; /** - * @dev Storage variables for the L2Reservoir + * @dev Storage variables for the L2Reservoir, version 1 */ contract L2ReservoirV1Storage { // Expected nonce value for the next drip hook uint256 public nextDripNonce; } +/** + * @dev Storage variables for the L2Reservoir, version 2 + * This version adds some variables needed when introducing the keeper reward. + */ contract L2ReservoirV2Storage is L2ReservoirV1Storage { // Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18) uint256 public l2KeeperRewardFraction; diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol index 92b9d5107..9f60249bd 100644 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -3,7 +3,7 @@ pragma solidity ^0.7.6; /** - * @dev Storage variables for the L1Reservoir + * @dev Storage variables for the L1Reservoir, version 1 */ contract L1ReservoirV1Storage { // Fraction of total rewards to be sent by L2, expressed in fixed point at 1e18 @@ -22,6 +22,10 @@ contract L1ReservoirV1Storage { uint256 public nextDripNonce; } +/** + * @dev Storage variables for the L1Reservoir, version 2 + * This version adds some variables that are needed when introducing keeper rewards. + */ contract L1ReservoirV2Storage is L1ReservoirV1Storage { // Minimum number of blocks since last drip for a new drip to be allowed uint256 public minDripInterval; diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index d2a0bf6cb..fff7f108a 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -19,7 +19,9 @@ import "./IReservoir.sol"; abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir { using SafeMath for uint256; + // Scaling factor for all fixed point arithmetics uint256 internal constant FIXED_POINT_SCALING_FACTOR = 1e18; + // Minimum issuance rate (expressed in fixed point at 1e18) uint256 internal constant MIN_ISSUANCE_RATE = 1e18; /** diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol index b46e44d35..b964d87b0 100644 --- a/contracts/reservoir/ReservoirStorage.sol +++ b/contracts/reservoir/ReservoirStorage.sol @@ -5,7 +5,7 @@ pragma solidity ^0.7.6; import "../governance/Managed.sol"; /** - * @dev Base storage variables for the Reservoir on both layers + * @dev Base storage variables for the Reservoir on both layers, version 1 */ contract ReservoirV1Storage is Managed { // Relative increase of the total supply per block, plus 1, expressed in fixed point at 1e18. From 91a0219df44926553ec1bfd8ac70cc8add31624d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 18:14:06 +0200 Subject: [PATCH 69/78] fix: use a single-condition requires for the drip auth check [L-05] --- contracts/reservoir/L1Reservoir.sol | 3 ++- test/reservoir/l1Reservoir.test.ts | 40 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index aa25f308d..9c7613700 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -62,7 +62,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * @param _indexer Indexer for which the sender must be an operator */ modifier onlyIndexerOperator(address _indexer) { - require(_isIndexer(_indexer) && staking().isOperator(msg.sender, _indexer), "UNAUTHORIZED"); + require(_isIndexer(_indexer), "UNAUTHORIZED_INVALID_INDEXER"); + require(staking().isOperator(msg.sender, _indexer), "UNAUTHORIZED_INVALID_OPERATOR"); _; } diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 2ea01ac6d..261a36c39 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -45,6 +45,7 @@ describe('L1Reservoir', () => { let governor: Account let testAccount1: Account let testAccount2: Account + let testAccount3: Account let mockRouter: Account let mockL2GRT: Account let mockL2Gateway: Account @@ -162,6 +163,7 @@ describe('L1Reservoir', () => { mockL2Reservoir, keeper, testAccount2, + testAccount3, ] = await getAccounts() fixture = new NetworkFixture() @@ -424,7 +426,43 @@ describe('L1Reservoir', () => { ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), constants.AddressZero) await expect(tx).revertedWith('INVALID_BENEFICIARY') }) - it('can be called by an indexer operator using an extra parameter', async function () { + it('(operator variant) cannot be called with an invalid indexer', async function () { + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount1.address, + ) + await expect(tx).revertedWith('UNAUTHORIZED_INVALID_INDEXER') + }) + it('(operator variant) cannot be called by someone who is not an operator for the right indexer', async function () { + const stakedAmount = toGRT('100000') + // testAccount1 is a valid indexer + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + // testAccount2 is an operator for testAccount1's indexer + await staking.connect(testAccount1.signer).setOperator(testAccount2.address, true) + // testAccount3 is another valid indexer + await grt.connect(governor.signer).mint(testAccount3.address, stakedAmount) + await grt.connect(testAccount3.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount3.signer).stake(stakedAmount) + // But testAccount2 is not an operator for testAccount3's indexer + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount3.address, + ) + await expect(tx).revertedWith('UNAUTHORIZED_INVALID_OPERATOR') + }) + it('(operator variant) can be called by an indexer operator using an extra parameter', async function () { const stakedAmount = toGRT('100000') await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) From 80cc2f2b4a8c1aecaabf15f9fa412141eb0b281c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 18:48:34 +0200 Subject: [PATCH 70/78] fix: add indexed params to dripper change events [N-01] --- contracts/reservoir/L1Reservoir.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 9c7613700..b57d38fa1 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -43,9 +43,9 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { // Emitted when minDripInterval is updated event MinDripIntervalUpdated(uint256 minDripInterval); // Emitted when a new allowedDripper is added - event AllowedDripperAdded(address dripper); + event AllowedDripperAdded(address indexed dripper); // Emitted when an allowedDripper is revoked - event AllowedDripperRevoked(address dripper); + event AllowedDripperRevoked(address indexed dripper); /** * @dev Checks that the sender is an indexer with stake on the Staking contract, From b99d31f4713156bf71f74dde9f7a8a9c54c99653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 18:45:43 +0200 Subject: [PATCH 71/78] fix: use explicit imports in relevant reservoir contracts [N-02] --- contracts/l2/reservoir/IL2Reservoir.sol | 2 +- contracts/l2/reservoir/L2Reservoir.sol | 18 ++++++++++-------- contracts/reservoir/L1Reservoir.sol | 13 ++++++++----- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol index b8a2311c9..8144efae2 100644 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -2,7 +2,7 @@ pragma solidity ^0.7.6; -import "../../reservoir/IReservoir.sol"; +import { IReservoir } from "../../reservoir/IReservoir.sol"; /** * @title Interface for the L2 Rewards Reservoir diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 9a341b6b5..e5cce6fee 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -3,14 +3,16 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; - -import "../../arbitrum/AddressAliasHelper.sol"; -import "../../reservoir/IReservoir.sol"; -import "../../reservoir/Reservoir.sol"; -import "./IL2Reservoir.sol"; -import "./L2ReservoirStorage.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { ArbRetryableTx } from "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; + +import { Managed } from "../../governance/Managed.sol"; +import { IGraphToken } from "../../token/IGraphToken.sol"; +import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; +import { IReservoir } from "../../reservoir/IReservoir.sol"; +import { Reservoir } from "../../reservoir/Reservoir.sol"; +import { IL2Reservoir } from "./IL2Reservoir.sol"; +import { L2ReservoirV2Storage } from "./L2ReservoirStorage.sol"; /** * @dev ArbRetryableTx with additional interface to query the current redeemer. diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index b57d38fa1..1fc1c30ce 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -3,13 +3,16 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/math/SafeMath.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import "../arbitrum/ITokenGateway.sol"; +import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; -import "../l2/reservoir/IL2Reservoir.sol"; -import "./Reservoir.sol"; -import "./L1ReservoirStorage.sol"; +import { Managed } from "../governance/Managed.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { IStaking } from "../staking/IStaking.sol"; +import { IL2Reservoir } from "../l2/reservoir/IL2Reservoir.sol"; +import { Reservoir } from "./Reservoir.sol"; +import { L1ReservoirV2Storage } from "./L1ReservoirStorage.sol"; /** * @title L1 Rewards Reservoir From 03988fe661464f2d52862c43c03d44c7a23507e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 30 Aug 2022 17:56:47 -0300 Subject: [PATCH 72/78] feat: start implementing an alternative rewards distribution minting in L2 and without reservoirs --- cli/commands/migrate.ts | 2 - contracts/governance/Managed.sol | 10 - contracts/l2/reservoir/IL2Reservoir.sol | 40 - contracts/l2/reservoir/L2Reservoir.sol | 200 --- contracts/l2/reservoir/L2ReservoirStorage.sol | 22 - contracts/reservoir/IReservoir.sol | 33 - contracts/reservoir/L1Reservoir.sol | 530 -------- contracts/reservoir/L1ReservoirStorage.sol | 36 - contracts/reservoir/Reservoir.sol | 117 -- contracts/reservoir/ReservoirStorage.sol | 19 - contracts/rewards/IRewardsManager.sol | 4 +- contracts/rewards/RewardsManager.sol | 221 +-- contracts/rewards/RewardsManagerStorage.sol | 9 +- contracts/staking/Staking.sol | 25 +- contracts/tests/ReservoirMock.sol | 27 - contracts/tests/RewardsManagerMock.sol | 68 + test/gateway/l1GraphTokenGateway.test.ts | 14 +- test/l2/l2GraphTokenGateway.test.ts | 17 +- test/l2/l2Reservoir.test.ts | 479 ------- test/lib/deployment.ts | 28 - test/lib/fixtures.ts | 31 +- test/lib/testHelpers.ts | 2 +- test/reservoir/l1Reservoir.test.ts | 1197 ----------------- test/rewards/rewards.test.ts | 892 ++++++------ 24 files changed, 663 insertions(+), 3360 deletions(-) delete mode 100644 contracts/l2/reservoir/IL2Reservoir.sol delete mode 100644 contracts/l2/reservoir/L2Reservoir.sol delete mode 100644 contracts/l2/reservoir/L2ReservoirStorage.sol delete mode 100644 contracts/reservoir/IReservoir.sol delete mode 100644 contracts/reservoir/L1Reservoir.sol delete mode 100644 contracts/reservoir/L1ReservoirStorage.sol delete mode 100644 contracts/reservoir/Reservoir.sol delete mode 100644 contracts/reservoir/ReservoirStorage.sol delete mode 100644 contracts/tests/ReservoirMock.sol create mode 100644 contracts/tests/RewardsManagerMock.sol delete mode 100644 test/l2/l2Reservoir.test.ts delete mode 100644 test/reservoir/l1Reservoir.test.ts diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index fb817cd67..7af52bcd5 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -36,7 +36,6 @@ let allContracts = [ 'AllocationExchange', 'L1GraphTokenGateway', 'BridgeEscrow', - 'L1Reservoir', ] const l2Contracts = [ @@ -56,7 +55,6 @@ const l2Contracts = [ 'DisputeManager', 'AllocationExchange', 'L2GraphTokenGateway', - 'L2Reservoir', ] export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index 20a9191e5..ce18fb009 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -10,7 +10,6 @@ import "../rewards/IRewardsManager.sol"; import "../staking/IStaking.sol"; import "../token/IGraphToken.sol"; import "../arbitrum/ITokenGateway.sol"; -import "../reservoir/IReservoir.sol"; /** * @title Graph Managed contract @@ -155,14 +154,6 @@ contract Managed { return ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); } - /** - * @dev Return Reservoir (L1 or L2) interface. - * @return Reservoir contract registered with Controller - */ - function reservoir() internal view returns (IReservoir) { - return IReservoir(_resolveContract(keccak256("Reservoir"))); - } - /** * @dev Resolve a contract address from the cache or the Controller if not found. * @return Address of the contract @@ -201,6 +192,5 @@ contract Managed { _syncContract("Staking"); _syncContract("GraphToken"); _syncContract("GraphTokenGateway"); - _syncContract("Reservoir"); } } diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol deleted file mode 100644 index 8144efae2..000000000 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -import { IReservoir } from "../../reservoir/IReservoir.sol"; - -/** - * @title Interface for the L2 Rewards Reservoir - * @dev This exposes a specific function for the L2Reservoir that is called - * as a callhook from L1 to L2, so that state can be updated when dripped rewards - * are bridged between layers. - */ -interface IL2Reservoir is IReservoir { - /** - * @dev Receive dripped tokens from L1. - * This function can only be called by the gateway, as it is - * meant to be a callhook when receiving tokens from L1. It - * updates the issuanceBase and issuanceRate, - * and snapshots the accumulated rewards. If issuanceRate changes, - * it also triggers a snapshot of rewards per signal on the RewardsManager. - * Note that the transaction might revert if it's received out-of-order, - * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed - * again once the ticket for previous drip has been redeemed. - * A keeper reward will be sent to the keeper that dripped on L1, and part of it - * to whoever redeemed the current retryable ticket (as reported by ArbRetryableTx.getCurrentRedeemer) if - * the ticket is not auto-redeemed. - * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) - * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 - * @param _nonce Incrementing nonce to ensure messages are received in order - * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx - * @param _l1Keeper Address of the keeper that called drip in L1 - */ - function receiveDrip( - uint256 _issuanceBase, - uint256 _issuanceRate, - uint256 _nonce, - uint256 _keeperReward, - address _l1Keeper - ) external; -} diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol deleted file mode 100644 index e5cce6fee..000000000 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; -pragma abicoder v2; - -import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import { ArbRetryableTx } from "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; - -import { Managed } from "../../governance/Managed.sol"; -import { IGraphToken } from "../../token/IGraphToken.sol"; -import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; -import { IReservoir } from "../../reservoir/IReservoir.sol"; -import { Reservoir } from "../../reservoir/Reservoir.sol"; -import { IL2Reservoir } from "./IL2Reservoir.sol"; -import { L2ReservoirV2Storage } from "./L2ReservoirStorage.sol"; - -/** - * @dev ArbRetryableTx with additional interface to query the current redeemer. - * This is being added by the Arbitrum team but hasn't made it into the arbos-precompiles - * package yet. - */ -interface IArbTxWithRedeemer is ArbRetryableTx { - /** - * @notice Gets the redeemer of the current retryable redeem attempt. - * Returns the zero address if the current transaction is not a retryable redeem attempt. - * If this is an auto-redeem, returns the fee refund address of the retryable. - */ - function getCurrentRedeemer() external view returns (address); -} - -/** - * @title L2 Rewards Reservoir - * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 2. - * It receives tokens for rewards from L1, and provides functions to compute accumulated and new - * total rewards at a particular block number. - */ -contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { - using SafeMath for uint256; - - // Address for the ArbRetryableTx interface provided by Arbitrum - address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E; - - // Emitted when a rewards drip is received from L1 - event DripReceived(uint256 issuanceBase); - // Emitted when the next drip nonce is manually updated by governance - event NextDripNonceUpdated(uint256 nonce); - // Emitted when the L1Reservoir's address is updated - event L1ReservoirAddressUpdated(address l1ReservoirAddress); - // Emitted when the L2 keeper reward fraction is updated - event L2KeeperRewardFractionUpdated(uint256 l2KeeperRewardFraction); - - /** - * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. - */ - modifier onlyL2Gateway() { - require(msg.sender == address(graphTokenGateway()), "ONLY_GATEWAY"); - _; - } - - /** - * @dev Initialize this contract. - * The contract will be paused. Note that issuance parameters - * are not set here because they are set from L1 through the drip function. - * The RewardsManager's address might also not be available in the controller at initialization - * time, so approveRewardsManager() must be called separately. - * The l1ReservoirAddress must also be set separately through setL1ReservoirAddress - * for the same reason. - * In the same vein, the l2KeeperRewardFraction is assumed to be zero at initialization, - * so it must be set through setL2KeeperRewardFraction. - * @param _controller Address of the Controller that manages this contract - */ - function initialize(address _controller) external onlyImpl { - Managed._initialize(_controller); - } - - /** - * @dev Update the next drip nonce - * To be used only as a backup option if the two layers get out of sync. - * @param _nonce Expected value for the nonce of the next drip message - */ - function setNextDripNonce(uint256 _nonce) external onlyGovernor { - nextDripNonce = _nonce; - emit NextDripNonceUpdated(_nonce); - } - - /** - * @dev Sets the L1 Reservoir address - * This is the address on L1 that will appear as redeemer when a ticket - * was auto-redeemed. - * @param _l1ReservoirAddress New address for the L1Reservoir on L1 - */ - function setL1ReservoirAddress(address _l1ReservoirAddress) external onlyGovernor { - require(_l1ReservoirAddress != address(0), "INVALID_L1_RESERVOIR"); - l1ReservoirAddress = _l1ReservoirAddress; - emit L1ReservoirAddressUpdated(_l1ReservoirAddress); - } - - /** - * @dev Sets the L2 keeper reward fraction - * This is the fraction of the keeper reward that will be sent to the redeemer on L2 - * if the retryable ticket is not auto-redeemed - * @param _l2KeeperRewardFraction New value for the fraction, with fixed point at 1e18 - */ - function setL2KeeperRewardFraction(uint256 _l2KeeperRewardFraction) external onlyGovernor { - require(_l2KeeperRewardFraction <= FIXED_POINT_SCALING_FACTOR, "INVALID_VALUE"); - l2KeeperRewardFraction = _l2KeeperRewardFraction; - emit L2KeeperRewardFractionUpdated(_l2KeeperRewardFraction); - } - - /** - * @dev Get new total rewards accumulated since the last drip. - * This is deltaR = p * r ^ (blocknum - t0) - p, where: - * - p is the issuance base at t0 (normalized by the L2 rewards fraction) - * - t0 is the last drip block, i.e. lastRewardsUpdateBlock - * - r is the issuanceRate - * @param _blocknum Block number at which to calculate rewards - * @return New total rewards on L2 since the last drip - */ - function getNewRewards(uint256 _blocknum) - public - view - override(Reservoir, IReservoir) - returns (uint256) - { - uint256 t0 = lastRewardsUpdateBlock; - if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { - return 0; - } - return - issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), FIXED_POINT_SCALING_FACTOR)) - .div(FIXED_POINT_SCALING_FACTOR) - .sub(issuanceBase); - } - - /** - * @dev Receive dripped tokens from L1. - * This function can only be called by the gateway, as it is - * meant to be a callhook when receiving tokens from L1. It - * updates the issuanceBase and issuanceRate, - * and snapshots the accumulated rewards. If issuanceRate changes, - * it also triggers a snapshot of rewards per signal on the RewardsManager. - * Note that the transaction might revert if it's received out-of-order, - * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed - * again once the ticket for previous drip has been redeemed. - * A keeper reward will be sent to the keeper that dripped on L1, and part of it - * to whoever redeemed the current retryable ticket (as reported by ArbRetryableTx.getCurrentRedeemer) if - * the ticket is not auto-redeemed. - * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) - * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 - * @param _nonce Incrementing nonce to ensure messages are received in order - * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx - * @param _l1Keeper Address of the keeper that called drip in L1 - */ - function receiveDrip( - uint256 _issuanceBase, - uint256 _issuanceRate, - uint256 _nonce, - uint256 _keeperReward, - address _l1Keeper - ) external override onlyL2Gateway { - require(_nonce == nextDripNonce, "INVALID_NONCE"); - nextDripNonce = nextDripNonce.add(1); - if (_issuanceRate != issuanceRate) { - rewardsManager().updateAccRewardsPerSignal(); - snapshotAccumulatedRewards(); - issuanceRate = _issuanceRate; - emit IssuanceRateUpdated(_issuanceRate); - } else { - snapshotAccumulatedRewards(); - } - issuanceBase = _issuanceBase; - IGraphToken grt = graphToken(); - - // Part of the reward always goes to whoever redeemed the ticket in L2, - // unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir - address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer(); - if (redeemer != AddressAliasHelper.applyL1ToL2Alias(l1ReservoirAddress)) { - uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div( - FIXED_POINT_SCALING_FACTOR - ); - grt.transfer(redeemer, _l2KeeperReward); - grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); - } else { - // In an auto-redeem, we just send all the rewards to the L1 keeper: - grt.transfer(_l1Keeper, _keeperReward); - } - - emit DripReceived(issuanceBase); - } - - /** - * @dev Snapshot accumulated rewards on this layer - * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. - */ - function snapshotAccumulatedRewards() internal { - accumulatedLayerRewards = getAccumulatedRewards(block.number); - lastRewardsUpdateBlock = block.number; - } -} diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol deleted file mode 100644 index 4d469f889..000000000 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -/** - * @dev Storage variables for the L2Reservoir, version 1 - */ -contract L2ReservoirV1Storage { - // Expected nonce value for the next drip hook - uint256 public nextDripNonce; -} - -/** - * @dev Storage variables for the L2Reservoir, version 2 - * This version adds some variables needed when introducing the keeper reward. - */ -contract L2ReservoirV2Storage is L2ReservoirV1Storage { - // Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18) - uint256 public l2KeeperRewardFraction; - // Address of the L1Reservoir on L1, used to check if a ticket was auto-redeemed - address public l1ReservoirAddress; -} diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol deleted file mode 100644 index a101952e0..000000000 --- a/contracts/reservoir/IReservoir.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -/** - * @title Interface for the Rewards Reservoir - * @dev This is the shared interface between L1 and L2, for the contracts - * that hold rewards on each layer and provide functions to compute - * accumulated and new total rewards. - */ -interface IReservoir { - // Emitted when the issuance rate is updated - event IssuanceRateUpdated(uint256 newValue); - - /** - * @dev Approve the RewardsManager to manage the reservoir's token funds - */ - function approveRewardsManager() external; - - /** - * @dev Get accumulated total rewards on this layer at a particular block - * @param _blocknum Block number at which to calculate rewards - * @return Accumulated total rewards on this layer - */ - function getAccumulatedRewards(uint256 _blocknum) external view returns (uint256); - - /** - * @dev Get new total rewards on this layer at a particular block, since the last drip event - * @param _blocknum Block number at which to calculate rewards - * @return New total rewards on this layer since the last drip - */ - function getNewRewards(uint256 _blocknum) external view returns (uint256); -} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol deleted file mode 100644 index 1fc1c30ce..000000000 --- a/contracts/reservoir/L1Reservoir.sol +++ /dev/null @@ -1,530 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; -pragma abicoder v2; - -import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; - -import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; - -import { Managed } from "../governance/Managed.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; -import { IStaking } from "../staking/IStaking.sol"; -import { IL2Reservoir } from "../l2/reservoir/IL2Reservoir.sol"; -import { Reservoir } from "./Reservoir.sol"; -import { L1ReservoirV2Storage } from "./L1ReservoirStorage.sol"; - -/** - * @title L1 Rewards Reservoir - * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 1. - * It provides a function to periodically drip rewards, and functions to compute accumulated and new - * total rewards at a particular block number. - */ -contract L1Reservoir is L1ReservoirV2Storage, Reservoir { - using SafeMath for uint256; - - // Emitted when the initial supply snapshot is taken after contract deployment - event InitialSnapshotTaken( - uint256 blockNumber, - uint256 issuanceBase, - uint256 mintedPendingRewards - ); - // Emitted when an issuance rate update is staged, to be applied on the next drip - event IssuanceRateStaged(uint256 newValue); - // Emitted when an L2 rewards fraction update is staged, to be applied on the next drip - event L2RewardsFractionStaged(uint256 newValue); - // Emitted when the L2 rewards fraction is updated (during a drip) - event L2RewardsFractionUpdated(uint256 newValue); - // Emitted when the drip interval is updated - event DripIntervalUpdated(uint256 newValue); - // Emitted when new rewards are dripped and potentially sent to L2 - event RewardsDripped(uint256 totalMinted, uint256 sentToL2, uint256 nextDeadline); - // Emitted when the address for the L2Reservoir is updated - event L2ReservoirAddressUpdated(address l2ReservoirAddress); - // Emitted when drip reward per block is updated - event DripRewardPerBlockUpdated(uint256 dripRewardPerBlock); - // Emitted when minDripInterval is updated - event MinDripIntervalUpdated(uint256 minDripInterval); - // Emitted when a new allowedDripper is added - event AllowedDripperAdded(address indexed dripper); - // Emitted when an allowedDripper is revoked - event AllowedDripperRevoked(address indexed dripper); - - /** - * @dev Checks that the sender is an indexer with stake on the Staking contract, - * or that the sender is an address whitelisted by governance to call. - */ - modifier onlyIndexerOrAllowedDripper() { - require(allowedDrippers[msg.sender] || _isIndexer(msg.sender), "UNAUTHORIZED"); - _; - } - - /** - * @dev Checks that the sender is an operator for the specified indexer - * (also checks that the specified indexer is, indeed, an indexer). - * @param _indexer Indexer for which the sender must be an operator - */ - modifier onlyIndexerOperator(address _indexer) { - require(_isIndexer(_indexer), "UNAUTHORIZED_INVALID_INDEXER"); - require(staking().isOperator(msg.sender, _indexer), "UNAUTHORIZED_INVALID_OPERATOR"); - _; - } - - /** - * @dev Initialize this contract. - * The contract will be paused. - * Note that the contract is designed to not accrue rewards until the first call - * to the drip function, that also requires the initial supply snapshot to be taken - * using initialSnapshot. For this reason, issuanceRate and l2RewardsFraction - * are not initialized here and instead need a call to setIssuanceRate and setL2RewardsFraction. - * The same applies to minDripInterval (set through setMinDripInterval) and dripRewardPerBlock - * (set through setDripRewardPerBlock). - * On the other hand, the l2ReservoirAddress is not expected to be known at initialization - * time and must therefore be set using setL2ReservoirAddress. - * The RewardsManager's address might also not be available in the controller at initialization - * time, so approveRewardsManager() must be called separately as well. - * @param _controller Address of the Controller that manages this contract - * @param _dripInterval Drip interval, i.e. time period for which rewards are minted each time we drip - */ - function initialize(address _controller, uint256 _dripInterval) external onlyImpl { - Managed._initialize(_controller); - _setDripInterval(_dripInterval); - } - - /** - * @dev Sets the drip interval. - * This is the time in the future (in blocks) for which drip() will mint rewards. - * Keep in mind that changing this value will require manually re-adjusting - * the reservoir's token balance, because the first call to drip might produce - * more or less tokens than needed. - * @param _dripInterval The new interval in blocks for which drip() will mint rewards - */ - function setDripInterval(uint256 _dripInterval) external onlyGovernor { - _setDripInterval(_dripInterval); - } - - /** - * @dev Sets the issuance rate. - * The issuance rate is defined as a relative increase of the total supply per block, plus 1. - * This means that it needs to be greater than 1.0, any number under 1.0 is not - * allowed and an issuance rate of 1.0 means no issuance. - * To accommodate a high precision the issuance rate is expressed in wei, i.e. fixed point at 1e18. - * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, - * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure - * good synchronization between L1 and L2. - * @param _issuanceRate Issuance rate expressed in wei / fixed point at 1e18 - */ - function setIssuanceRate(uint256 _issuanceRate) external onlyGovernor { - require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); - nextIssuanceRate = _issuanceRate; - emit IssuanceRateStaged(_issuanceRate); - } - - /** - * @dev Sets the L2 rewards fraction. - * This is the portion of the indexer rewards that are sent to L2. - * The value is in fixed point at 1e18 and must be less than or equal to 1. - * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, - * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure - * good synchronization between L1 and L2. - * @param _l2RewardsFraction Fraction of rewards to send to L2, in wei / fixed point at 1e18 - */ - function setL2RewardsFraction(uint256 _l2RewardsFraction) external onlyGovernor { - require( - _l2RewardsFraction <= FIXED_POINT_SCALING_FACTOR, - "L2 Rewards fraction must be <= 1" - ); - nextL2RewardsFraction = _l2RewardsFraction; - emit L2RewardsFractionStaged(_l2RewardsFraction); - } - - /** - * @dev Sets the drip reward per block - * This is the reward in GRT provided to the keeper that calls drip() - * @param _dripRewardPerBlock GRT accrued for each block after the threshold - */ - function setDripRewardPerBlock(uint256 _dripRewardPerBlock) external onlyGovernor { - dripRewardPerBlock = _dripRewardPerBlock; - emit DripRewardPerBlockUpdated(_dripRewardPerBlock); - } - - /** - * @dev Sets the minimum drip interval - * This is the minimum number of blocks between two successful drips - * @param _minDripInterval Minimum number of blocks since last drip for drip to be allowed - */ - function setMinDripInterval(uint256 _minDripInterval) external onlyGovernor { - require(_minDripInterval < dripInterval, "MUST_BE_LT_DRIP_INTERVAL"); - minDripInterval = _minDripInterval; - emit MinDripIntervalUpdated(_minDripInterval); - } - - /** - * @dev Sets the L2 Reservoir address - * This is the address on L2 to which we send tokens for rewards. - * @param _l2ReservoirAddress New address for the L2Reservoir on L2 - */ - function setL2ReservoirAddress(address _l2ReservoirAddress) external onlyGovernor { - require(_l2ReservoirAddress != address(0), "INVALID_L2_RESERVOIR"); - l2ReservoirAddress = _l2ReservoirAddress; - emit L2ReservoirAddressUpdated(_l2ReservoirAddress); - } - - /** - * @dev Grants an address permission to call drip() - * @param _dripper Address that will be an allowed dripper - */ - function grantDripPermission(address _dripper) external onlyGovernor { - require(_dripper != address(0), "INVALID_ADDRESS"); - require(!allowedDrippers[_dripper], "ALREADY_A_DRIPPER"); - allowedDrippers[_dripper] = true; - emit AllowedDripperAdded(_dripper); - } - - /** - * @dev Revokes an address' permission to call drip() - * @param _dripper Address that will not be an allowed dripper anymore - */ - function revokeDripPermission(address _dripper) external onlyGovernor { - require(_dripper != address(0), "INVALID_ADDRESS"); - require(allowedDrippers[_dripper], "NOT_A_DRIPPER"); - allowedDrippers[_dripper] = false; - emit AllowedDripperRevoked(_dripper); - } - - /** - * @dev Computes the initial snapshot for token supply and mints any pending rewards - * This will initialize the issuanceBase to the current GRT supply, after which - * we will keep an internal accounting only using newly minted rewards. This function - * will also mint any pending rewards to cover up to the current block for open allocations, - * to be computed off-chain. Can only be called once as it checks that the issuanceBase is zero. - * @param _pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function - */ - function initialSnapshot(uint256 _pendingRewards) external onlyGovernor { - require(issuanceBase == 0, "Cannot call this function more than once"); - lastRewardsUpdateBlock = block.number; - IGraphToken grt = graphToken(); - grt.mint(address(this), _pendingRewards); - issuanceBase = grt.totalSupply(); - emit InitialSnapshotTaken(block.number, issuanceBase, _pendingRewards); - } - - /** - * @dev Drip indexer rewards for layers 1 and 2 - * This function will mint enough tokens to cover all indexer rewards for the next - * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send - * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. - * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function - * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. - * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must - * only be nonzero if l2RewardsFraction is nonzero. - * Calling this function can revert if the issuance rate has recently been reduced, and the existing - * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait - * until the drip amount becomes positive before calling the function again. It can also revert - * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we - * should send now. - * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, - * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed - * again once the ticket for previous drip has been redeemed. - * This function with an additional parameter is only provided so that indexer operators can call it, - * specifying the indexer for which they are an operator. - * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) - * @param _indexer Indexer for whom the sender must be an authorized Operator - */ - function drip( - uint256 _l2MaxGas, - uint256 _l2GasPriceBid, - uint256 _l2MaxSubmissionCost, - address _keeperRewardBeneficiary, - address _indexer - ) external payable notPaused onlyIndexerOperator(_indexer) { - _drip(_l2MaxGas, _l2GasPriceBid, _l2MaxSubmissionCost, _keeperRewardBeneficiary); - } - - /** - * @dev Drip indexer rewards for layers 1 and 2 - * This function will mint enough tokens to cover all indexer rewards for the next - * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send - * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. - * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function - * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. - * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must - * only be nonzero if l2RewardsFraction is nonzero. - * Calling this function can revert if the issuance rate has recently been reduced, and the existing - * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait - * until the drip amount becomes positive before calling the function again. It can also revert - * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we - * should send now. - * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, - * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed - * again once the ticket for previous drip has been redeemed. - * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) - */ - function drip( - uint256 _l2MaxGas, - uint256 _l2GasPriceBid, - uint256 _l2MaxSubmissionCost, - address _keeperRewardBeneficiary - ) external payable notPaused onlyIndexerOrAllowedDripper { - _drip(_l2MaxGas, _l2GasPriceBid, _l2MaxSubmissionCost, _keeperRewardBeneficiary); - } - - /** - * @dev Drip indexer rewards for layers 1 and 2, private implementation. - * This function will mint enough tokens to cover all indexer rewards for the next - * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send - * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. - * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function - * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. - * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must - * only be nonzero if l2RewardsFraction is nonzero. - * Calling this function can revert if the issuance rate has recently been reduced, and the existing - * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait - * until the drip amount becomes positive before calling the function again. It can also revert - * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we - * should send now. - * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, - * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed - * again once the ticket for previous drip has been redeemed. - * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 - * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) - */ - function _drip( - uint256 _l2MaxGas, - uint256 _l2GasPriceBid, - uint256 _l2MaxSubmissionCost, - address _keeperRewardBeneficiary - ) private { - require( - block.number > lastRewardsUpdateBlock.add(minDripInterval), - "WAIT_FOR_MIN_INTERVAL" - ); - // Note we only validate that the beneficiary is nonzero, as the caller might - // want to send the reward to an address that is different to the indexer/dripper's address. - require(_keeperRewardBeneficiary != address(0), "INVALID_BENEFICIARY"); - - uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); - uint256 mintedRewardsActual = getNewGlobalRewards(block.number); - // eps = (signed int) mintedRewardsTotal - mintedRewardsActual - - uint256 keeperReward = dripRewardPerBlock.mul(block.number.sub(lastRewardsUpdateBlock)); - if (nextIssuanceRate != issuanceRate) { - rewardsManager().updateAccRewardsPerSignal(); - snapshotAccumulatedRewards(mintedRewardsActual); // This updates lastRewardsUpdateBlock - issuanceRate = nextIssuanceRate; - emit IssuanceRateUpdated(issuanceRate); - } else { - snapshotAccumulatedRewards(mintedRewardsActual); - } - - rewardsMintedUntilBlock = block.number.add(dripInterval); - // n = deltaR(t1, t0) - uint256 newRewardsToDistribute = getNewGlobalRewards(rewardsMintedUntilBlock); - // N = n - eps - uint256 rewardsTokensToMint; - { - uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual); - require( - newRewardsPlusMintedActual > mintedRewardsTotal, - "Would mint negative or zero tokens, wait before calling again" - ); - rewardsTokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); - } - - IGraphToken grt = graphToken(); - grt.mint(address(this), rewardsTokensToMint.add(keeperReward)); - - uint256 tokensToSendToL2 = 0; - if (l2RewardsFraction != nextL2RewardsFraction) { - tokensToSendToL2 = nextL2RewardsFraction.mul(newRewardsToDistribute).div( - FIXED_POINT_SCALING_FACTOR - ); - if (mintedRewardsTotal > mintedRewardsActual) { - // eps > 0, i.e. t < t1_old - // Note this can fail if the old l2RewardsFraction is larger - // than the new, in which case we just have to wait until enough time has passed - // so that eps is small enough. This also applies to the case where the new - // l2RewardsFraction is zero, since we still need to send one last message - // with the new values to the L2Reservoir. - uint256 l2OffsetAmount = l2RewardsFraction - .mul(mintedRewardsTotal.sub(mintedRewardsActual)) - .div(FIXED_POINT_SCALING_FACTOR); - require( - tokensToSendToL2 > l2OffsetAmount, - "Negative amount would be sent to L2, wait before calling again" - ); - tokensToSendToL2 = tokensToSendToL2.add(keeperReward).sub(l2OffsetAmount); - } else { - tokensToSendToL2 = tokensToSendToL2.add(keeperReward).add( - l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( - FIXED_POINT_SCALING_FACTOR - ) - ); - } - l2RewardsFraction = nextL2RewardsFraction; - emit L2RewardsFractionUpdated(l2RewardsFraction); - _sendNewTokensAndStateToL2( - tokensToSendToL2, - _l2MaxGas, - _l2GasPriceBid, - _l2MaxSubmissionCost, - keeperReward, - _keeperRewardBeneficiary - ); - } else if (l2RewardsFraction > 0) { - tokensToSendToL2 = rewardsTokensToMint - .mul(l2RewardsFraction) - .div(FIXED_POINT_SCALING_FACTOR) - .add(keeperReward); - _sendNewTokensAndStateToL2( - tokensToSendToL2, - _l2MaxGas, - _l2GasPriceBid, - _l2MaxSubmissionCost, - keeperReward, - _keeperRewardBeneficiary - ); - } else { - // Avoid locking funds in this contract if we don't need to - // send a message to L2. - require(msg.value == 0, "No eth value needed"); - // If we don't send rewards to L2, pay the keeper reward in L1 - grt.transfer(_keeperRewardBeneficiary, keeperReward); - } - emit RewardsDripped( - rewardsTokensToMint.add(keeperReward), - tokensToSendToL2, - rewardsMintedUntilBlock - ); - } - - /** - * @dev Get new total rewards on both layers at a particular block, since the last drip event - * This is deltaR = p * r ^ (blocknum - t0) - p, where: - * - p is the total token supply snapshot at t0 - * - t0 is the last drip block, i.e. lastRewardsUpdateBlock - * - r is the issuanceRate - * @param _blocknum Block number at which to calculate rewards - * @return New total rewards on both layers since the last drip - */ - function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256) { - uint256 t0 = lastRewardsUpdateBlock; - if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { - return 0; - } - return - issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), FIXED_POINT_SCALING_FACTOR)) - .div(FIXED_POINT_SCALING_FACTOR) - .sub(issuanceBase); - } - - /** - * @dev Get new total rewards on this layer at a particular block, since the last drip event - * This is deltaR_L1 = (1-lambda) * deltaR, where: - * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) - * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction - * @param _blocknum Block number at which to calculate rewards - * @return New total rewards on Layer 1 since the last drip - */ - function getNewRewards(uint256 _blocknum) public view override returns (uint256) { - return - getNewGlobalRewards(_blocknum) - .mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)) - .div(FIXED_POINT_SCALING_FACTOR); - } - - /** - * @dev Sets the drip interval (internal). - * This is the time in the future (in blocks) for which drip() will mint rewards. - * Keep in mind that changing this value will require manually re-adjusting - * the reservoir's token balance, because the first call to drip might produce - * more or less tokens than needed. - * @param _dripInterval The new interval in blocks for which drip() will mint rewards - */ - function _setDripInterval(uint256 _dripInterval) internal { - require(_dripInterval > 0, "Drip interval must be > 0"); - dripInterval = _dripInterval; - emit DripIntervalUpdated(_dripInterval); - } - - /** - * @dev Snapshot accumulated rewards on this layer - * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. - * We also update the issuanceBase by adding the new total rewards on both layers. - * @param _globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block - */ - function snapshotAccumulatedRewards(uint256 _globalDelta) internal { - issuanceBase = issuanceBase.add(_globalDelta); - // Reimplementation of getAccumulatedRewards but reusing the _globalDelta calculated above, - // to save gas - accumulatedLayerRewards = accumulatedLayerRewards.add( - _globalDelta.mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)).div( - FIXED_POINT_SCALING_FACTOR - ) - ); - lastRewardsUpdateBlock = block.number; - } - - /** - * @dev Send new tokens and a message with state to L2 - * This function will use the L1GraphTokenGateway to send tokens - * to L2, and will also encode a callhook to update state on the L2Reservoir. - * @param _nTokens Number of tokens to send to L2 - * @param _maxGas Max gas for the L2 retryable ticket execution - * @param _gasPriceBid Gas price for the L2 retryable ticket execution - * @param _maxSubmissionCost Max submission price for the L2 retryable ticket - * @param _keeperReward Tokens to assign as keeper reward for calling drip - * @param _keeper Address of the keeper that will be rewarded - */ - function _sendNewTokensAndStateToL2( - uint256 _nTokens, - uint256 _maxGas, - uint256 _gasPriceBid, - uint256 _maxSubmissionCost, - uint256 _keeperReward, - address _keeper - ) internal { - uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div( - FIXED_POINT_SCALING_FACTOR - ); - bytes memory extraData = abi.encodeWithSelector( - IL2Reservoir.receiveDrip.selector, - l2IssuanceBase, - issuanceRate, - nextDripNonce, - _keeperReward, - _keeper - ); - nextDripNonce = nextDripNonce.add(1); - bytes memory data = abi.encode(_maxSubmissionCost, extraData); - IGraphToken grt = graphToken(); - ITokenGateway gateway = graphTokenGateway(); - grt.approve(address(gateway), _nTokens); - gateway.outboundTransfer{ value: msg.value }( - address(grt), - l2ReservoirAddress, - _nTokens, - _maxGas, - _gasPriceBid, - data - ); - } - - /** - * @dev Checks if an address is an indexer with stake in the Staking contract - * @param _indexer Address that will be checked - */ - function _isIndexer(address _indexer) internal view returns (bool) { - IStaking staking = staking(); - return staking.hasStake(_indexer); - } -} diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol deleted file mode 100644 index 9f60249bd..000000000 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -/** - * @dev Storage variables for the L1Reservoir, version 1 - */ -contract L1ReservoirV1Storage { - // Fraction of total rewards to be sent by L2, expressed in fixed point at 1e18 - uint256 public l2RewardsFraction; - // New fraction of total rewards to be sent by L2, to be applied on the next drip - uint256 public nextL2RewardsFraction; - // Address for the L2Reservoir to which we send rewards - address public l2ReservoirAddress; - // Block until the minted supplies should last before another drip is needed - uint256 public rewardsMintedUntilBlock; - // New issuance rate to be applied on the next drip - uint256 public nextIssuanceRate; - // Interval for rewards drip, in blocks - uint256 public dripInterval; - // Auto-incrementing nonce that will be used when sending rewards to L2, to ensure ordering - uint256 public nextDripNonce; -} - -/** - * @dev Storage variables for the L1Reservoir, version 2 - * This version adds some variables that are needed when introducing keeper rewards. - */ -contract L1ReservoirV2Storage is L1ReservoirV1Storage { - // Minimum number of blocks since last drip for a new drip to be allowed - uint256 public minDripInterval; - // Drip reward in GRT for each block since lastRewardsUpdateBlock + dripRewardThreshold - uint256 public dripRewardPerBlock; - // True for addresses that are allowed to call drip() - mapping(address => bool) public allowedDrippers; -} diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol deleted file mode 100644 index fff7f108a..000000000 --- a/contracts/reservoir/Reservoir.sol +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; - -import "../upgrades/GraphUpgradeable.sol"; - -import "./ReservoirStorage.sol"; -import "./IReservoir.sol"; - -/** - * @title Rewards Reservoir base contract - * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 1 or Layer 2. - * It provides functions to compute accumulated and new total rewards at a particular block number. - * This base contract provides functionality that is common to L1 and L2, to be extended on each layer. - */ -abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir { - using SafeMath for uint256; - - // Scaling factor for all fixed point arithmetics - uint256 internal constant FIXED_POINT_SCALING_FACTOR = 1e18; - // Minimum issuance rate (expressed in fixed point at 1e18) - uint256 internal constant MIN_ISSUANCE_RATE = 1e18; - - /** - * @dev Approve the RewardsManager to manage the reservoir's token funds - */ - function approveRewardsManager() external override onlyGovernor { - graphToken().approve(address(rewardsManager()), type(uint256).max); - } - - /** - * @dev Get accumulated total rewards on this layer at a particular block - * @param _blocknum Block number at which to calculate rewards - * @return Accumulated total rewards on this layer - */ - function getAccumulatedRewards(uint256 _blocknum) public view override returns (uint256) { - // R(t) = R(t0) + (DeltaR(t, t0)) - return accumulatedLayerRewards.add(getNewRewards(_blocknum)); - } - - /** - * @dev Get new total rewards on this layer at a particular block, since the last drip event. - * Must be implemented by the reservoir on each layer. - * @param _blocknum Block number at which to calculate rewards - * @return New total rewards on this layer since the last drip - */ - function getNewRewards(uint256 _blocknum) public view virtual override returns (uint256); - - /** - * @dev Raises _x to the power of _n with scaling factor of _base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param _x Base of the exponentiation - * @param _n Exponent - * @param _base Scaling factor - * @return Exponential of _n with base _x - */ - function _pow( - uint256 _x, - uint256 _n, - uint256 _base - ) internal pure returns (uint256) { - uint256 z; - // solhint-disable-next-line no-inline-assembly - assembly { - switch _x - case 0 { - switch _n - case 0 { - z := _base - } - default { - z := 0 - } - } - default { - switch mod(_n, 2) - case 0 { - z := _base - } - default { - z := _x - } - let half := div(_base, 2) // for rounding. - for { - _n := div(_n, 2) - } _n { - _n := div(_n, 2) - } { - let xx := mul(_x, _x) - if iszero(eq(div(xx, _x), _x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - _x := div(xxRound, _base) - if mod(_n, 2) { - let zx := mul(z, _x) - if and(iszero(iszero(_x)), iszero(eq(div(zx, _x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, _base) - } - } - } - } - return z; - } -} diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol deleted file mode 100644 index b964d87b0..000000000 --- a/contracts/reservoir/ReservoirStorage.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -import "../governance/Managed.sol"; - -/** - * @dev Base storage variables for the Reservoir on both layers, version 1 - */ -contract ReservoirV1Storage is Managed { - // Relative increase of the total supply per block, plus 1, expressed in fixed point at 1e18. - uint256 public issuanceRate; - // Accumulated total rewards on the corresponding layer (L1 or L2) - uint256 public accumulatedLayerRewards; - // Last block at which rewards when updated, i.e. block at which the last drip happened or was received - uint256 public lastRewardsUpdateBlock; - // Base value for token issuance, set initially to GRT supply and afterwards using accumulated rewards to update - uint256 public issuanceBase; -} diff --git a/contracts/rewards/IRewardsManager.sol b/contracts/rewards/IRewardsManager.sol index f92ca422a..dc17c8ba8 100644 --- a/contracts/rewards/IRewardsManager.sol +++ b/contracts/rewards/IRewardsManager.sol @@ -15,6 +15,8 @@ interface IRewardsManager { // -- Config -- + function setIssuanceRate(uint256 _issuanceRate) external; + function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; // -- Denylist -- @@ -52,8 +54,6 @@ interface IRewardsManager { function takeRewards(address _allocationID) external returns (uint256); - function takeAndBurnRewards(address _allocationID) external; - // -- Hooks -- function onSubgraphSignalUpdate(bytes32 _subgraphDeploymentID) external returns (uint256); diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index c9dcf103d..0c68adf49 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -10,8 +10,6 @@ import "../upgrades/GraphUpgradeable.sol"; import "./RewardsManagerStorage.sol"; import "./IRewardsManager.sol"; -import "../reservoir/IReservoir.sol"; - /** * @title Rewards Manager Contract * @dev Tracks how inflationary GRT rewards should be handed out. Relies on the Curation contract @@ -29,10 +27,10 @@ import "../reservoir/IReservoir.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; - uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; + uint256 private constant TOKEN_DECIMALS = 1e18; uint256 private constant MIN_ISSUANCE_RATE = 1e18; // -- Events -- @@ -48,24 +46,9 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa ); /** - * @dev Emitted when rewards are denied to an indexer (and therefore burned). + * @dev Emitted when rewards are denied to an indexer. */ - event RewardsDenied( - address indexed indexer, - address indexed allocationID, - uint256 epoch, - uint256 amount - ); - - /** - * @dev Emitted when rewards for an indexer are burned . - */ - event RewardsBurned( - address indexed indexer, - address indexed allocationID, - uint256 epoch, - uint256 amount - ); + event RewardsDenied(address indexed indexer, address indexed allocationID, uint256 epoch); /** * @dev Emitted when a subgraph is denied for claiming rewards. @@ -91,6 +74,32 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa // -- Config -- + /** + * @dev Sets the issuance rate. + * The issuance rate is defined as a percentage increase of the total supply per block. + * This means that it needs to be greater than 1.0, any number under 1.0 is not + * allowed and an issuance rate of 1.0 means no issuance. + * To accommodate a high precision the issuance rate is expressed in wei. + * @param _issuanceRate Issuance rate expressed in wei + */ + function setIssuanceRate(uint256 _issuanceRate) external override onlyGovernor { + _setIssuanceRate(_issuanceRate); + } + + /** + * @dev Sets the issuance rate. + * @param _issuanceRate Issuance rate + */ + function _setIssuanceRate(uint256 _issuanceRate) private { + require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); + + // Called since `issuance rate` will change + updateAccRewardsPerSignal(); + + issuanceRate = _issuanceRate; + emit ParameterUpdated("issuanceRate"); + } + /** * @dev Sets the subgraph oracle allowed to denegate distribution of rewards to subgraphs. * @param _subgraphAvailabilityOracle Address of the subgraph availability oracle @@ -178,13 +187,32 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa /** * @dev Gets the issuance of rewards per signal since last updated. * - * The compound interest formula is applied in the Reservoir contract. - * This function will compare accumulated rewards at the current block - * with the value that was cached at accRewardsPerSignalLastBlockUpdated. + * Compound interest formula: `a = p(1 + r/n)^nt` + * The formula is simplified with `n = 1` as we apply the interest once every time step. + * The `r` is passed with +1 included. So for 10% instead of 0.1 it is 1.1 + * The simplified formula is `a = p * r^t` + * + * Notation: + * t: time steps are in blocks since last updated + * p: total supply of GRT tokens + * a: inflated amount of total supply for the period `t` when interest `r` is applied + * x: newly accrued rewards token for the period `t` * * @return newly accrued rewards per signal since last update */ function getNewRewardsPerSignal() public view override returns (uint256) { + // Calculate time steps + uint256 t = block.number.sub(accRewardsPerSignalLastBlockUpdated); + // Optimization to skip calculations if zero time steps elapsed + if (t == 0) { + return 0; + } + + // Zero issuance under a rate of 1.0 + if (issuanceRate <= MIN_ISSUANCE_RATE) { + return 0; + } + // Zero issuance if no signalled tokens IGraphToken graphToken = graphToken(); uint256 signalledTokens = graphToken.balanceOf(address(curation())); @@ -192,14 +220,16 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa return 0; } - uint256 accRewardsNow = reservoir().getAccumulatedRewards(block.number); + uint256 r = issuanceRate; + uint256 p = tokenSupplySnapshot; + uint256 a = p.mul(_pow(r, t, TOKEN_DECIMALS)).div(TOKEN_DECIMALS); + + // New issuance of tokens during time steps + uint256 x = a.sub(p); // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number - return - (accRewardsNow.sub(accRewardsOnLastSignalUpdate)).mul(FIXED_POINT_SCALING_FACTOR).div( - signalledTokens - ); + return x.mul(TOKEN_DECIMALS).div(signalledTokens); } /** @@ -231,7 +261,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa ? getAccRewardsPerSignal() .sub(subgraph.accRewardsPerSignalSnapshot) .mul(subgraphSignalledTokens) - .div(FIXED_POINT_SCALING_FACTOR) + .div(TOKEN_DECIMALS) : 0; return subgraph.accRewardsForSubgraph.add(newRewards); } @@ -251,9 +281,12 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; uint256 accRewardsForSubgraph = getAccRewardsForSubgraph(_subgraphDeploymentID); - uint256 newRewardsForSubgraph = accRewardsForSubgraph.sub( - subgraph.accRewardsForSubgraphSnapshot - ); + uint256 newRewardsForSubgraph; + if (accRewardsForSubgraph > subgraph.accRewardsForSubgraphSnapshot) { + newRewardsForSubgraph = accRewardsForSubgraph.sub( + subgraph.accRewardsForSubgraphSnapshot + ); + } uint256 subgraphAllocatedTokens = staking().getSubgraphAllocatedTokens( _subgraphDeploymentID @@ -262,9 +295,9 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa return (0, accRewardsForSubgraph); } - uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph - .mul(FIXED_POINT_SCALING_FACTOR) - .div(subgraphAllocatedTokens); + uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph.mul(TOKEN_DECIMALS).div( + subgraphAllocatedTokens + ); return ( subgraph.accRewardsPerAllocatedToken.add(newRewardsPerAllocatedToken), accRewardsForSubgraph @@ -274,8 +307,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa // -- Updates -- /** - * @dev Updates the accumulated rewards per signal and saves the checkpoint block number. - * Also snapshots total accumulated rewards (`accRewardsOnLastSignalUpdate`). + * @dev Updates the accumulated rewards per signal and save checkpoint block number. * Must be called before `issuanceRate` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() * @return Accumulated rewards per signal @@ -283,7 +315,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; - accRewardsOnLastSignalUpdate = reservoir().getAccumulatedRewards(block.number); + tokenSupplySnapshot = graphToken().totalSupply(); return accRewardsPerSignal; } @@ -364,13 +396,13 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa uint256 _endAccRewardsPerAllocatedToken ) private pure returns (uint256) { uint256 newAccrued = _endAccRewardsPerAllocatedToken.sub(_startAccRewardsPerAllocatedToken); - return newAccrued.mul(_tokens).div(FIXED_POINT_SCALING_FACTOR); + return newAccrued.mul(_tokens).div(TOKEN_DECIMALS); } /** * @dev Pull rewards from the contract for a particular allocation. * This function can only be called by the Staking contract. - * This function will transfer the necessary tokens to reward based on the inflation calculation. + * This function will mint the necessary tokens to reward based on the inflation calculation. * @param _allocationID Allocation * @return Assigned rewards amount */ @@ -384,55 +416,90 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa alloc.subgraphDeploymentID ); + // Do not do rewards on denied subgraph deployments ID + if (isDenied(alloc.subgraphDeploymentID)) { + emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch); + return 0; + } + // Calculate rewards accrued by this allocation uint256 rewards = _calcRewards( alloc.tokens, alloc.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken ); - if (!isDenied(alloc.subgraphDeploymentID)) { - // Transfer to staking contract for the reward amount + if (rewards > 0) { + // Mint directly to staking contract for the reward amount // The staking contract will do bookkeeping of the reward and // assign in proportion to each stakeholder incentive - if (rewards > 0) { - graphToken().transferFrom(address(reservoir()), address(staking), rewards); - } - emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); - return rewards; - } else { - if (rewards > 0) { - graphToken().burnFrom(address(reservoir()), rewards); - } - emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); - return 0; + graphToken().mint(address(staking), rewards); } + + emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + + return rewards; } /** - * @dev Burn rewards for a particular allocation. - * This function can only be called by the Staking contract. - * This function will burn the necessary tokens to reward based on the inflation calculation. - * @param _allocationID Allocation + * @dev Raises x to the power of n with scaling factor of base. + * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 + * @param x Base of the exponentiation + * @param n Exponent + * @param base Scaling factor + * @return z Exponential of n with base x */ - function takeAndBurnRewards(address _allocationID) external override { - // Only Staking contract is authorized as caller - IStaking staking = staking(); - require(msg.sender == address(staking), "Caller must be the staking contract"); - - IStaking.Allocation memory alloc = staking.getAllocation(_allocationID); - uint256 accRewardsPerAllocatedToken = onSubgraphAllocationUpdate( - alloc.subgraphDeploymentID - ); - - // Calculate rewards accrued by this allocation - uint256 rewards = _calcRewards( - alloc.tokens, - alloc.accRewardsPerAllocatedToken, - accRewardsPerAllocatedToken - ); - if (rewards > 0) { - graphToken().burnFrom(address(reservoir()), rewards); - emit RewardsBurned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + function _pow( + uint256 x, + uint256 n, + uint256 base + ) private pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { + z := base + } + default { + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + z := base + } + default { + z := x + } + let half := div(base, 2) // for rounding. + for { + n := div(n, 2) + } n { + n := div(n, 2) + } { + let xx := mul(x, x) + if iszero(eq(div(xx, x), x)) { + revert(0, 0) + } + let xxRound := add(xx, half) + if lt(xxRound, xx) { + revert(0, 0) + } + x := div(xxRound, base) + if mod(n, 2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { + revert(0, 0) + } + let zxRound := add(zx, half) + if lt(zxRound, zx) { + revert(0, 0) + } + z := div(zxRound, base) + } + } + } } } } diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index d8a6284e5..7626992da 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -8,7 +8,7 @@ import "../governance/Managed.sol"; contract RewardsManagerV1Storage is Managed { // -- State -- - uint256 public issuanceRateDeprecated; + uint256 public issuanceRate; uint256 public accRewardsPerSignal; uint256 public accRewardsPerSignalLastBlockUpdated; @@ -29,10 +29,5 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage { contract RewardsManagerV3Storage is RewardsManagerV2Storage { // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated - uint256 public tokenSupplySnapshotDeprecated; -} - -contract RewardsManagerV4Storage is RewardsManagerV3Storage { - // Accumulated rewards at accRewardsPerSignalLastBlockUpdated - uint256 public accRewardsOnLastSignalUpdate; + uint256 public tokenSupplySnapshot; } diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index 37c972a53..2bcc8d74d 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -1218,12 +1218,11 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { // Process non-zero-allocation rewards tracking if (alloc.tokens > 0) { - // Distribute rewards if proof of indexing was presented by the indexer or operator, - // otherwise the rewards will be burned from the reservoir. + // Distribute rewards if proof of indexing was presented by the indexer or operator if (isIndexer && _poi != 0) { _distributeRewards(_allocationID, alloc.indexer); } else { - _takeAndBurnRewards(_allocationID); + _updateRewards(alloc.subgraphDeploymentID); } // Free allocated tokens from use @@ -1585,6 +1584,9 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _updateRewards(bytes32 _subgraphDeploymentID) private returns (uint256) { IRewardsManager rewardsManager = rewardsManager(); + if (address(rewardsManager) == address(0)) { + return 0; + } return rewardsManager.onSubgraphAllocationUpdate(_subgraphDeploymentID); } @@ -1594,9 +1596,12 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _distributeRewards(address _allocationID, address _indexer) private { IRewardsManager rewardsManager = rewardsManager(); + if (address(rewardsManager) == address(0)) { + return; + } // Automatically triggers update of rewards snapshot as allocation will change - // after this call. Take rewards transfers tokens for the Staking contract to distribute + // after this call. Take rewards mint tokens for the Staking contract to distribute // between indexer and delegators uint256 totalRewards = rewardsManager.takeRewards(_allocationID); if (totalRewards == 0) { @@ -1616,18 +1621,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { ); } - /** - * @dev Burn rewards for the closed allocation and update the allocation state. - * @param _allocationID Allocation - */ - function _takeAndBurnRewards(address _allocationID) private { - IRewardsManager rewardsManager = rewardsManager(); - - // Automatically triggers update of rewards snapshot as allocation will change - // after this call. - rewardsManager.takeAndBurnRewards(_allocationID); - } - /** * @dev Send rewards to the appropiate destination. * @param _graphToken Graph token diff --git a/contracts/tests/ReservoirMock.sol b/contracts/tests/ReservoirMock.sol deleted file mode 100644 index 2ce40b3f2..000000000 --- a/contracts/tests/ReservoirMock.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "../reservoir/Reservoir.sol"; - -// Mock contract used for testing rewards -contract ReservoirMock is Reservoir { - function getNewRewards(uint256) public view override returns (uint256 r) {} - - /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x - */ - function pow( - uint256 x, - uint256 n, - uint256 base - ) public pure returns (uint256 z) { - z = _pow(x, n, base); - } -} diff --git a/contracts/tests/RewardsManagerMock.sol b/contracts/tests/RewardsManagerMock.sol new file mode 100644 index 000000000..cbd57b2d3 --- /dev/null +++ b/contracts/tests/RewardsManagerMock.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.7.6; +pragma abicoder v2; + +// Mock contract used for testing rewards +contract RewardsManagerMock { + /** + * @dev Raises x to the power of n with scaling factor of base. + * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 + * @param x Base of the exponentiation + * @param n Exponent + * @param base Scaling factor + * @return z Exponential of n with base x + */ + function pow( + uint256 x, + uint256 n, + uint256 base + ) public pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { + z := base + } + default { + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + z := base + } + default { + z := x + } + let half := div(base, 2) // for rounding. + for { + n := div(n, 2) + } n { + n := div(n, 2) + } { + let xx := mul(x, x) + if iszero(eq(div(xx, x), x)) { + revert(0, 0) + } + let xxRound := add(xx, half) + if lt(xxRound, xx) { + revert(0, 0) + } + x := div(xxRound, base) + if mod(n, 2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { + revert(0, 0) + } + let zxRound := add(zx, half) + if lt(zxRound, zx) { + revert(0, 0) + } + z := div(zxRound, base) + } + } + } + } + } +} diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index f14204816..a5a473d86 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -29,7 +29,6 @@ describe('L1GraphTokenGateway', () => { let mockL2GRT: Account let mockL2Gateway: Account let pauseGuardian: Account - let mockL2Reservoir: Account let fixture: NetworkFixture let grt: GraphToken @@ -59,16 +58,8 @@ describe('L1GraphTokenGateway', () => { ) before(async function () { - ;[ - governor, - tokenSender, - l2Receiver, - mockRouter, - mockL2GRT, - mockL2Gateway, - pauseGuardian, - mockL2Reservoir, - ] = await getAccounts() + ;[governor, tokenSender, l2Receiver, mockRouter, mockL2GRT, mockL2Gateway, pauseGuardian] = + await getAccounts() fixture = new NetworkFixture() fixtureContracts = await fixture.load(governor.signer) @@ -380,7 +371,6 @@ describe('L1GraphTokenGateway', () => { mockRouter.address, mockL2GRT.address, mockL2Gateway.address, - mockL2Reservoir.address, ) }) diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index d95c92871..f4d57dbab 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -12,7 +12,7 @@ import path from 'path' import { Artifacts } from 'hardhat/internal/artifacts' const ARTIFACTS_PATH = path.resolve('build/contracts') const artifacts = new Artifacts(ARTIFACTS_PATH) -const reservoirMockAbi = artifacts.readArtifactSync('ReservoirMock').abi +const rewardsManagerMockAbi = artifacts.readArtifactSync('RewardsManagerMock').abi use(smock.matchers) @@ -31,7 +31,6 @@ describe('L2GraphTokenGateway', () => { let mockL1GRT: Account let mockL1Gateway: Account let pauseGuardian: Account - let mockL1Reservoir: Account let fixture: NetworkFixture let arbSysMock: FakeContract @@ -41,7 +40,7 @@ describe('L2GraphTokenGateway', () => { const senderTokens = toGRT('1000') const defaultData = '0x' - const mockIface = new Interface(reservoirMockAbi) + const mockIface = new Interface(rewardsManagerMockAbi) const notEmptyCallHookData = mockIface.encodeFunctionData('pow', [toBN(1), toBN(2), toBN(3)]) const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( ['bytes', 'bytes'], @@ -59,7 +58,6 @@ describe('L2GraphTokenGateway', () => { mockL1Gateway, l2Receiver, pauseGuardian, - mockL1Reservoir, ] = await getAccounts() fixture = new NetworkFixture() @@ -254,7 +252,6 @@ describe('L2GraphTokenGateway', () => { mockRouter.address, mockL1GRT.address, mockL1Gateway.address, - mockL1Reservoir.address, ) }) @@ -379,18 +376,18 @@ describe('L2GraphTokenGateway', () => { await testValidFinalizeTransfer(defaultData) }) it('calls a callhook if the sender is whitelisted', async function () { - const reservoirMock = await smock.fake('ReservoirMock', { + const rewardsManagerMock = await smock.fake('RewardsManagerMock', { address: l2Receiver.address, }) - reservoirMock.pow.returns(1) + rewardsManagerMock.pow.returns(1) await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData) - expect(reservoirMock.pow).to.have.been.calledWith(toBN(1), toBN(2), toBN(3)) + expect(rewardsManagerMock.pow).to.have.been.calledWith(toBN(1), toBN(2), toBN(3)) }) it('reverts if a callhook reverts', async function () { - const reservoirMock = await smock.fake('ReservoirMock', { + const rewardsManagerMock = await smock.fake('RewardsManagerMock', { address: l2Receiver.address, }) - reservoirMock.pow.reverts() + rewardsManagerMock.pow.reverts() const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) await me.signer.sendTransaction({ to: await mockL1GatewayL2Alias.getAddress(), diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts deleted file mode 100644 index 26a9ccc75..000000000 --- a/test/l2/l2Reservoir.test.ts +++ /dev/null @@ -1,479 +0,0 @@ -import { expect } from 'chai' -import { BigNumber, constants, ContractTransaction, utils } from 'ethers' - -import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' - -import { BigNumber as BN } from 'bignumber.js' -import { FakeContract, smock } from '@defi-wonderland/smock' - -import { - advanceBlocks, - getAccounts, - latestBlock, - toBN, - toGRT, - formatGRT, - Account, - RewardsTracker, - getL2SignerFromL1, - applyL1ToL2Alias, -} from '../lib/testHelpers' -import { L2Reservoir } from '../../build/types/L2Reservoir' - -import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' -import { L2GraphToken } from '../../build/types/L2GraphToken' - -const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] - -const dripAmount = toBN('5851557519569225000000000') -const dripNormalizedSupply = toGRT('10004000000') -const dripIssuanceRate = toBN('1000000023206889619') - -describe('L2Reservoir', () => { - let governor: Account - let testAccount1: Account - let testAccount2: Account - let mockRouter: Account - let mockL1GRT: Account - let mockL1Gateway: Account - let mockL1Reservoir: Account - let fixture: NetworkFixture - let arbTxMock: FakeContract - - let grt: L2GraphToken - let l2Reservoir: L2Reservoir - let l2GraphTokenGateway: L2GraphTokenGateway - - let fixtureContracts: L2FixtureContracts - - let normalizedSupply: BigNumber - let dripBlock: BigNumber - - const ISSUANCE_RATE_PERIODS = toBN(4) // blocks required to issue 0.05% rewards - const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block - - // Test accumulated rewards after nBlocksToAdvance, - // asking for the value at blockToQuery - const shouldGetNewRewards = async ( - initialSupply: BigNumber, - nBlocksToAdvance: BigNumber = ISSUANCE_RATE_PERIODS, - blockToQuery?: BigNumber, - expectedValue?: BigNumber, - round = true, - ) => { - // -- t0 -- - const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK) - const startAccrued = await l2Reservoir.getAccumulatedRewards(await latestBlock()) - // Jump - await advanceBlocks(nBlocksToAdvance) - - // -- t1 -- - - // Contract calculation - if (!blockToQuery) { - blockToQuery = await latestBlock() - } - const contractAccrued = await l2Reservoir.getAccumulatedRewards(blockToQuery) - // Local calculation - if (expectedValue == null) { - expectedValue = await tracker.newRewards(blockToQuery) - } - - // Check - if (round) { - expect(toRound(contractAccrued.sub(startAccrued))).eq(toRound(expectedValue)) - } else { - expect(contractAccrued.sub(startAccrued)).eq(expectedValue) - } - - return expectedValue - } - - const gatewayFinalizeTransfer = async (callhookData: string): Promise => { - const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) - await testAccount1.signer.sendTransaction({ - to: await mockL1GatewayL2Alias.getAddress(), - value: utils.parseUnits('1', 'ether'), - }) - const data = utils.defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', callhookData]) - const tx = l2GraphTokenGateway - .connect(mockL1GatewayL2Alias) - .finalizeInboundTransfer( - mockL1GRT.address, - mockL1Reservoir.address, - l2Reservoir.address, - dripAmount, - data, - ) - return tx - } - - const validGatewayFinalizeTransfer = async ( - callhookData: string, - keeperReward = toGRT('0'), - ): Promise => { - const tx = await gatewayFinalizeTransfer(callhookData) - await expect(tx) - .emit(l2GraphTokenGateway, 'DepositFinalized') - .withArgs(mockL1GRT.address, mockL1Reservoir.address, l2Reservoir.address, dripAmount) - - await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Reservoir.address, dripAmount) - - // newly minted GRT - const receiverBalance = await grt.balanceOf(l2Reservoir.address) - await expect(receiverBalance).eq(dripAmount.sub(keeperReward)) - return tx - } - - before(async function () { - ;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir, testAccount2] = - await getAccounts() - - fixture = new NetworkFixture() - fixtureContracts = await fixture.loadL2(governor.signer) - ;({ grt, l2Reservoir, l2GraphTokenGateway } = fixtureContracts) - await fixture.configureL2Bridge( - governor.signer, - fixtureContracts, - mockRouter.address, - mockL1GRT.address, - mockL1Gateway.address, - mockL1Reservoir.address, - ) - - arbTxMock = await smock.fake('IArbTxWithRedeemer', { - address: '0x000000000000000000000000000000000000006E', - }) - arbTxMock.getCurrentRedeemer.returns(applyL1ToL2Alias(mockL1Reservoir.address)) - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('setNextDripNonce', async function () { - it('rejects unauthorized calls', async function () { - const tx = l2Reservoir.connect(testAccount1.signer).setNextDripNonce(toBN('10')) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - it('sets the next expected drip nonce', async function () { - const tx = l2Reservoir.connect(governor.signer).setNextDripNonce(toBN('10')) - await expect(tx).emit(l2Reservoir, 'NextDripNonceUpdated').withArgs(toBN('10')) - await expect(await l2Reservoir.nextDripNonce()).to.eq(toBN('10')) - }) - }) - - describe('setL1ReservoirAddress', async function () { - it('rejects unauthorized calls', async function () { - const tx = l2Reservoir - .connect(testAccount1.signer) - .setL1ReservoirAddress(testAccount1.address) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - it('rejects setting a zero address', async function () { - const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(constants.AddressZero) - await expect(tx).revertedWith('INVALID_L1_RESERVOIR') - }) - it('sets the L1Reservoir address', async function () { - const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(testAccount1.address) - await expect(tx).emit(l2Reservoir, 'L1ReservoirAddressUpdated').withArgs(testAccount1.address) - await expect(await l2Reservoir.l1ReservoirAddress()).to.eq(testAccount1.address) - }) - }) - - describe('setL2KeeperRewardFraction', async function () { - it('rejects unauthorized calls', async function () { - const tx = l2Reservoir.connect(testAccount1.signer).setL2KeeperRewardFraction(toBN(1)) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - it('rejects invalid values (> 1)', async function () { - const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('1.000001')) - await expect(tx).revertedWith('INVALID_VALUE') - }) - it('sets the L1Reservoir address', async function () { - const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.999')) - await expect(tx).emit(l2Reservoir, 'L2KeeperRewardFractionUpdated').withArgs(toGRT('0.999')) - await expect(await l2Reservoir.l2KeeperRewardFraction()).to.eq(toGRT('0.999')) - }) - }) - - describe('receiveDrip', async function () { - beforeEach(async function () { - await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.2')) - await l2Reservoir.connect(governor.signer).setL1ReservoirAddress(mockL1Reservoir.address) - }) - it('rejects the call when not called by the gateway', async function () { - const tx = l2Reservoir - .connect(governor.signer) - .receiveDrip( - dripNormalizedSupply, - dripIssuanceRate, - toBN('0'), - toBN('0'), - testAccount1.address, - ) - await expect(tx).revertedWith('ONLY_GATEWAY') - }) - it('rejects the call when received out of order', async function () { - normalizedSupply = dripNormalizedSupply - let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply, - dripIssuanceRate, - toBN('0'), - toBN('0'), - testAccount1.address, - ) - const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) - - // Incorrect nonce - receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply.add(1), - dripIssuanceRate.add(1), - toBN('2'), - toBN('0'), - testAccount1.address, - ) - const tx2 = gatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(tx2).revertedWith('CALLHOOK_FAILED') // Gateway overrides revert message - }) - it('updates the normalized supply cache', async function () { - normalizedSupply = dripNormalizedSupply - const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply, - dripIssuanceRate, - toBN('0'), - toBN('0'), - testAccount1.address, - ) - const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) - }) - it('delivers the keeper reward to the beneficiary address', async function () { - normalizedSupply = dripNormalizedSupply - const reward = toBN('15') - const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply, - dripIssuanceRate, - toBN('0'), - reward, - testAccount1.address, - ) - const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) - await expect(tx) - .emit(grt, 'Transfer') - .withArgs(l2Reservoir.address, testAccount1.address, reward) - await expect(await grt.balanceOf(testAccount1.address)).to.eq(reward) - }) - it('delivers part of the keeper reward to the L2 redeemer', async function () { - arbTxMock.getCurrentRedeemer.returns(testAccount2.address) - await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.25')) - normalizedSupply = dripNormalizedSupply - const reward = toGRT('16') - const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply, - dripIssuanceRate, - toBN('0'), - reward, - testAccount1.address, - ) - const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) - await expect(tx) - .emit(grt, 'Transfer') - .withArgs(l2Reservoir.address, testAccount1.address, toGRT('12')) - await expect(tx) - .emit(grt, 'Transfer') - .withArgs(l2Reservoir.address, testAccount2.address, toGRT('4')) - await expect(await grt.balanceOf(testAccount1.address)).to.eq(toGRT('12')) - await expect(await grt.balanceOf(testAccount2.address)).to.eq(toGRT('4')) - }) - it('updates the normalized supply cache and issuance rate', async function () { - normalizedSupply = dripNormalizedSupply - let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply, - dripIssuanceRate, - toBN('0'), - toBN('0'), - testAccount1.address, - ) - let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) - - receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply.add(1), - dripIssuanceRate.add(1), - toBN('1'), - toBN('0'), - testAccount1.address, - ) - tx = await gatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate.add(1)) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) - await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) - }) - it('accepts subsequent calls without changing issuance rate', async function () { - normalizedSupply = dripNormalizedSupply - let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply, - dripIssuanceRate, - toBN('0'), - toBN('0'), - testAccount1.address, - ) - let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) - - receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply.add(1), - dripIssuanceRate, - toBN('1'), - toBN('0'), - testAccount1.address, - ) - tx = await gatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) - await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) - }) - it('accepts a different nonce set through setNextDripNonce', async function () { - normalizedSupply = dripNormalizedSupply - let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply, - dripIssuanceRate, - toBN('0'), - toBN('0'), - testAccount1.address, - ) - let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) - - await l2Reservoir.connect(governor.signer).setNextDripNonce(toBN('2')) - receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply.add(1), - dripIssuanceRate, - toBN('2'), - toBN('0'), - testAccount1.address, - ) - tx = await gatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) - await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) - await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) - await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) - }) - }) - - context('calculating rewards', async function () { - beforeEach(async function () { - // 5% minute rate (4 blocks) - normalizedSupply = dripNormalizedSupply - const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( - dripNormalizedSupply, - ISSUANCE_RATE_PER_BLOCK, - toBN('0'), - toBN('0'), - testAccount1.address, - ) - await validGatewayFinalizeTransfer(receiveDripTx.data) - dripBlock = await latestBlock() - }) - - describe('getAccumulatedRewards', function () { - it('returns rewards accrued after some blocks', async function () { - await shouldGetNewRewards(normalizedSupply) - }) - it('returns zero if evaluated at the block where reservoir had the first drip', async function () { - await shouldGetNewRewards( - normalizedSupply, - ISSUANCE_RATE_PERIODS, - dripBlock, - toBN(0), - false, - ) - }) - it('returns the supply times issuance rate one block after the first drip', async function () { - const expectedVal = normalizedSupply - .mul(ISSUANCE_RATE_PER_BLOCK.sub(toGRT(1))) - .div(toGRT(1)) - await shouldGetNewRewards( - normalizedSupply, - ISSUANCE_RATE_PERIODS, - dripBlock.add(1), - expectedVal, - false, - ) - }) - it('returns the rewards for a block some time in the future', async function () { - await shouldGetNewRewards(normalizedSupply, toBN(1), dripBlock.add(10000)) - }) - }) - describe('getNewRewards', function () { - const computeDelta = function (t1: BigNumber, t0: BigNumber, lambda = toBN(0)): BigNumber { - const deltaT = new BN(t1.toString()).minus(new BN(t0.toString())) - const rate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) - const supply = new BN(normalizedSupply.toString()) - return toBN(supply.times(rate.pow(deltaT)).minus(supply).precision(18).toString(10)) - .mul(toGRT('1').sub(lambda)) - .div(toGRT('1')) - } - it('computes the rewards delta between the last drip block and the current block', async function () { - const t0 = dripBlock - const t1 = t0.add(200) - const expectedVal = computeDelta(t1, t0) - expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) - }) - it('returns zero rewards if the time delta is zero', async function () { - const t0 = dripBlock - const expectedVal = toBN('0') - expect(await l2Reservoir.getNewRewards(t0)).to.eq(expectedVal) - }) - it('computes the rewards delta between a past drip block and a future block', async function () { - await advanceBlocks(20) - const t0 = dripBlock - const t1 = t0.add(100) - const expectedVal = computeDelta(t1, t0) - expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) - }) - it('computes the rewards delta between a past drip block and the current block', async function () { - await advanceBlocks(20) - const t0 = dripBlock - const t1 = await latestBlock() - const expectedVal = computeDelta(t1, t0) - expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) - }) - }) - }) -}) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index 4bdcbe629..a6afe9dbb 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -22,8 +22,6 @@ import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' import { L2GraphToken } from '../../build/types/L2GraphToken' import { BridgeEscrow } from '../../build/types/BridgeEscrow' -import { L1Reservoir } from '../../build/types/L1Reservoir' -import { L2Reservoir } from '../../build/types/L2Reservoir' // Disable logging for tests logger.pause() @@ -301,29 +299,3 @@ export async function deployL2GRT( deployer, ) as unknown as L2GraphToken } - -export async function deployL1Reservoir( - deployer: Signer, - controller: string, - proxyAdmin: GraphProxyAdmin, -): Promise { - return network.deployContractWithProxy( - proxyAdmin, - 'L1Reservoir', - [controller, defaults.rewards.dripInterval], - deployer, - ) as unknown as L1Reservoir -} - -export async function deployL2Reservoir( - deployer: Signer, - controller: string, - proxyAdmin: GraphProxyAdmin, -): Promise { - return network.deployContractWithProxy( - proxyAdmin, - 'L2Reservoir', - [controller], - deployer, - ) as unknown as L2Reservoir -} diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 5a4f1e53a..ae6832938 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -19,10 +19,8 @@ import { ServiceRegistry } from '../../build/types/ServiceRegistry' import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' import { BridgeEscrow } from '../../build/types/BridgeEscrow' -import { L1Reservoir } from '../../build/types/L1Reservoir' import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' import { L2GraphToken } from '../../build/types/L2GraphToken' -import { L2Reservoir } from '../../build/types/L2Reservoir' export interface L1FixtureContracts { controller: Controller @@ -37,7 +35,6 @@ export interface L1FixtureContracts { proxyAdmin: GraphProxyAdmin l1GraphTokenGateway: L1GraphTokenGateway bridgeEscrow: BridgeEscrow - l1Reservoir: L1Reservoir } export interface L2FixtureContracts { @@ -52,7 +49,6 @@ export interface L2FixtureContracts { serviceRegistry: ServiceRegistry proxyAdmin: GraphProxyAdmin l2GraphTokenGateway: L2GraphTokenGateway - l2Reservoir: L2Reservoir } export interface ArbitrumL1Mocks { @@ -118,15 +114,12 @@ export class NetworkFixture { let l1GraphTokenGateway: L1GraphTokenGateway let l2GraphTokenGateway: L2GraphTokenGateway let bridgeEscrow: BridgeEscrow - let l1Reservoir: L1Reservoir - let l2Reservoir: L2Reservoir if (isL2) { l2GraphTokenGateway = await deployment.deployL2GraphTokenGateway( deployer, controller.address, proxyAdmin, ) - l2Reservoir = await deployment.deployL2Reservoir(deployer, controller.address, proxyAdmin) } else { l1GraphTokenGateway = await deployment.deployL1GraphTokenGateway( deployer, @@ -134,7 +127,6 @@ export class NetworkFixture { proxyAdmin, ) bridgeEscrow = await deployment.deployBridgeEscrow(deployer, controller.address, proxyAdmin) - l1Reservoir = await deployment.deployL1Reservoir(deployer, controller.address, proxyAdmin) } // Setup controller @@ -147,10 +139,8 @@ export class NetworkFixture { await controller.setContractProxy(utils.id('ServiceRegistry'), serviceRegistry.address) if (isL2) { await controller.setContractProxy(utils.id('GraphTokenGateway'), l2GraphTokenGateway.address) - await controller.setContractProxy(utils.id('Reservoir'), l2Reservoir.address) } else { await controller.setContractProxy(utils.id('GraphTokenGateway'), l1GraphTokenGateway.address) - await controller.setContractProxy(utils.id('Reservoir'), l1Reservoir.address) } // Setup contracts @@ -162,23 +152,14 @@ export class NetworkFixture { await staking.connect(deployer).syncAllContracts() if (isL2) { await l2GraphTokenGateway.connect(deployer).syncAllContracts() - await l2Reservoir.connect(deployer).syncAllContracts() } else { await l1GraphTokenGateway.connect(deployer).syncAllContracts() await bridgeEscrow.connect(deployer).syncAllContracts() - await l1Reservoir.connect(deployer).syncAllContracts() } await staking.connect(deployer).setSlasher(slasherAddress, true) await gns.connect(deployer).approveAll() - if (isL2) { - await grt.connect(deployer).addMinter(l2GraphTokenGateway.address) - await l2Reservoir.connect(deployer).approveRewardsManager() - } else { - await grt.connect(deployer).addMinter(l1Reservoir.address) - await l1Reservoir.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) - await l1Reservoir.connect(deployer).approveRewardsManager() - } + await grt.connect(deployer).addMinter(rewardsManager.address) // Unpause the protocol await controller.connect(deployer).setPaused(false) @@ -196,7 +177,6 @@ export class NetworkFixture { serviceRegistry, proxyAdmin, l2GraphTokenGateway, - l2Reservoir, } as L2FixtureContracts } else { return { @@ -212,7 +192,6 @@ export class NetworkFixture { proxyAdmin, l1GraphTokenGateway, bridgeEscrow, - l1Reservoir, } as L1FixtureContracts } } @@ -251,7 +230,6 @@ export class NetworkFixture { mockRouterAddress: string, mockL2GRTAddress: string, mockL2GatewayAddress: string, - mockL2ReservoirAddress: string, ): Promise { // First configure the Arbitrum bridge mocks await arbitrumMocks.bridgeMock.connect(deployer).setInbox(arbitrumMocks.inboxMock.address, true) @@ -274,15 +252,9 @@ export class NetworkFixture { await l1FixtureContracts.l1GraphTokenGateway .connect(deployer) .setEscrowAddress(l1FixtureContracts.bridgeEscrow.address) - await l1FixtureContracts.l1GraphTokenGateway - .connect(deployer) - .addToCallhookWhitelist(l1FixtureContracts.l1Reservoir.address) await l1FixtureContracts.bridgeEscrow .connect(deployer) .approveAll(l1FixtureContracts.l1GraphTokenGateway.address) - await l1FixtureContracts.l1Reservoir - .connect(deployer) - .setL2ReservoirAddress(mockL2ReservoirAddress) await l1FixtureContracts.l1GraphTokenGateway.connect(deployer).setPaused(false) } @@ -292,7 +264,6 @@ export class NetworkFixture { mockRouterAddress: string, mockL1GRTAddress: string, mockL1GatewayAddress: string, - mockL1ReservoirAddress: string, ): Promise { // Configure the L2 GRT // Configure the gateway diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index 998d0d335..ea4f20264 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -140,7 +140,7 @@ const getRewards = (p: BN, r: BN, t: BN): string => { } // Tracks the accumulated rewards as supply changes across snapshots -// both at a global level (like the Reservoir) and per signal (like RewardsManager) +// both at a global level and per signal export class RewardsTracker { totalSupply = BigNumber.from(0) lastUpdatedBlock = BigNumber.from(0) diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts deleted file mode 100644 index 261a36c39..000000000 --- a/test/reservoir/l1Reservoir.test.ts +++ /dev/null @@ -1,1197 +0,0 @@ -import { expect } from 'chai' -import { BigNumber, constants } from 'ethers' - -import { defaults, deployContract, deployL1Reservoir } from '../lib/deployment' -import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' - -import { GraphToken } from '../../build/types/GraphToken' -import { ReservoirMock } from '../../build/types/ReservoirMock' -import { BigNumber as BN } from 'bignumber.js' - -import { - advanceBlocks, - getAccounts, - latestBlock, - toBN, - toGRT, - formatGRT, - Account, - RewardsTracker, -} from '../lib/testHelpers' -import { L1Reservoir } from '../../build/types/L1Reservoir' -import { BridgeEscrow } from '../../build/types/BridgeEscrow' - -import path from 'path' -import { Artifacts } from 'hardhat/internal/artifacts' -import { Interface } from 'ethers/lib/utils' -import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' -import { Controller } from '../../build/types/Controller' -import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' -import { Staking } from '../../build/types/Staking' -const ARTIFACTS_PATH = path.resolve('build/contracts') -const artifacts = new Artifacts(ARTIFACTS_PATH) -const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi -const l2ReservoirIface = new Interface(l2ReservoirAbi) - -const { AddressZero } = constants -const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] - -const maxGas = toBN('1000000') -const maxSubmissionCost = toBN('7') -const gasPriceBid = toBN('2') -const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) - -describe('L1Reservoir', () => { - let governor: Account - let testAccount1: Account - let testAccount2: Account - let testAccount3: Account - let mockRouter: Account - let mockL2GRT: Account - let mockL2Gateway: Account - let mockL2Reservoir: Account - let keeper: Account - let fixture: NetworkFixture - - let grt: GraphToken - let reservoirMock: ReservoirMock - let l1Reservoir: L1Reservoir - let bridgeEscrow: BridgeEscrow - let l1GraphTokenGateway: L1GraphTokenGateway - let controller: Controller - let proxyAdmin: GraphProxyAdmin - let staking: Staking - - let supplyBeforeDrip: BigNumber - let dripBlock: BigNumber - let fixtureContracts: L1FixtureContracts - let arbitrumMocks: ArbitrumL1Mocks - - const ISSUANCE_RATE_PERIODS = toBN(4) // blocks required to issue 0.05% rewards - const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block - - // Test accumulated rewards after nBlocksToAdvance, - // asking for the value at blockToQuery - const shouldGetNewRewards = async ( - initialSupply: BigNumber, - nBlocksToAdvance: BigNumber = ISSUANCE_RATE_PERIODS, - blockToQuery?: BigNumber, - expectedValue?: BigNumber, - round = true, - ) => { - // -- t0 -- - const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK) - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - // Jump - await advanceBlocks(nBlocksToAdvance) - - // -- t1 -- - - // Contract calculation - if (!blockToQuery) { - blockToQuery = await latestBlock() - } - const contractAccrued = await l1Reservoir.getAccumulatedRewards(blockToQuery) - // Local calculation - if (expectedValue == null) { - expectedValue = await tracker.newRewards(blockToQuery) - } - - // Check - if (round) { - expect(toRound(contractAccrued.sub(startAccrued))).eq(toRound(expectedValue)) - } else { - expect(contractAccrued.sub(startAccrued)).eq(expectedValue) - } - - return expectedValue - } - - const sequentialDoubleDrip = async ( - blocksToAdvance: BigNumber, - dripInterval = defaults.rewards.dripInterval, - ) => { - const supplyBeforeDrip = await grt.totalSupply() - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - expect(startAccrued).to.eq(0) - const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - defaults.rewards.issuanceRate, - dripBlock, - ) - expect(await tracker.accRewards(dripBlock)).to.eq(0) - let expectedNextDeadline = dripBlock.add(dripInterval) - let expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const tx1 = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - const actualAmount = await grt.balanceOf(l1Reservoir.address) - expect(await latestBlock()).eq(dripBlock) - expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) - expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) - await expect(tx1) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(actualAmount, toBN(0), expectedNextDeadline) - await expect(tx1).emit(grt, 'Transfer').withArgs(AddressZero, l1Reservoir.address, actualAmount) - await tracker.snapshotRewards() - - await advanceBlocks(blocksToAdvance) - - const tx2 = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - const newAmount = (await grt.balanceOf(l1Reservoir.address)).sub(actualAmount) - expectedNextDeadline = (await latestBlock()).add(dripInterval) - const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) - expectedMintedAmount = (await tracker.accRewards(expectedNextDeadline)).sub(actualAmount) - expect(toRound(newAmount)).to.eq(toRound(expectedMintedAmount)) - expect(toRound(await l1Reservoir.issuanceBase())).to.eq(toRound(expectedSnapshottedSupply)) - await expect(tx2) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(newAmount, toBN(0), expectedNextDeadline) - await expect(tx2).emit(grt, 'Transfer').withArgs(AddressZero, l1Reservoir.address, newAmount) - } - - before(async function () { - ;[ - governor, - testAccount1, - mockRouter, - mockL2GRT, - mockL2Gateway, - mockL2Reservoir, - keeper, - testAccount2, - testAccount3, - ] = await getAccounts() - - fixture = new NetworkFixture() - fixtureContracts = await fixture.load(governor.signer) - ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway, controller, proxyAdmin, staking } = - fixtureContracts) - - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) - arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) - await fixture.configureL1Bridge( - governor.signer, - arbitrumMocks, - fixtureContracts, - mockRouter.address, - mockL2GRT.address, - mockL2Gateway.address, - mockL2Reservoir.address, - ) - await l1Reservoir.connect(governor.signer).grantDripPermission(keeper.address) - reservoirMock = (await deployContract( - 'ReservoirMock', - governor.signer, - )) as unknown as ReservoirMock - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('configuration', function () { - describe('initial snapshot', function () { - let reservoir: L1Reservoir - beforeEach(async function () { - // Deploy a new reservoir to avoid issues with initialSnapshot being called twice - reservoir = await deployL1Reservoir(governor.signer, controller.address, proxyAdmin) - await grt.connect(governor.signer).addMinter(reservoir.address) - }) - - it('rejects call if unauthorized', async function () { - const tx = reservoir.connect(testAccount1.signer).initialSnapshot(toGRT('1.025')) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - - it('snapshots the total GRT supply', async function () { - const tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) - const supply = await grt.totalSupply() - await expect(tx) - .emit(reservoir, 'InitialSnapshotTaken') - .withArgs(await latestBlock(), supply, toGRT('0')) - expect(await grt.balanceOf(reservoir.address)).to.eq(toGRT('0')) - expect(await reservoir.issuanceBase()).to.eq(supply) - expect(await reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) - }) - it('mints pending rewards and includes them in the snapshot', async function () { - const pending = toGRT('10000000') - const tx = reservoir.connect(governor.signer).initialSnapshot(pending) - const supply = await grt.totalSupply() - const expectedSupply = supply.add(pending) - await expect(tx) - .emit(reservoir, 'InitialSnapshotTaken') - .withArgs(await latestBlock(), expectedSupply, pending) - expect(await grt.balanceOf(reservoir.address)).to.eq(pending) - expect(await reservoir.issuanceBase()).to.eq(expectedSupply) - expect(await reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) - }) - it('cannot be called more than once', async function () { - let tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) - await expect(tx).emit(reservoir, 'InitialSnapshotTaken') - tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) - await expect(tx).revertedWith('Cannot call this function more than once') - }) - }) - describe('issuance rate update', function () { - it('rejects setting issuance rate if unauthorized', async function () { - const tx = l1Reservoir.connect(testAccount1.signer).setIssuanceRate(toGRT('1.025')) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - - it('rejects setting issuance rate to less than minimum allowed', async function () { - const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 - const tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) - await expect(tx).revertedWith('Issuance rate under minimum allowed') - }) - - it('should set issuance rate to minimum allowed', async function () { - const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 - const tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) - await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) - expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) - }) - - it('should set issuance rate to apply on next drip', async function () { - const newIssuanceRate = toGRT('1.00025') - let tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) - await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) - expect(await l1Reservoir.issuanceRate()).eq(0) - expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) - tx = l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - await expect(tx).emit(l1Reservoir, 'IssuanceRateUpdated').withArgs(newIssuanceRate) - expect(await l1Reservoir.issuanceRate()).eq(newIssuanceRate) - }) - }) - describe('drip interval update', function () { - it('rejects setting drip interval if unauthorized', async function () { - const tx = l1Reservoir.connect(testAccount1.signer).setDripInterval(toBN(40800)) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - - it('rejects setting drip interval to zero', async function () { - const tx = l1Reservoir.connect(governor.signer).setDripInterval(toBN(0)) - await expect(tx).revertedWith('Drip interval must be > 0') - }) - - it('updates the drip interval', async function () { - const newInterval = toBN(40800) - const tx = l1Reservoir.connect(governor.signer).setDripInterval(newInterval) - await expect(tx).emit(l1Reservoir, 'DripIntervalUpdated').withArgs(newInterval) - expect(await l1Reservoir.dripInterval()).eq(newInterval) - }) - }) - describe('L2 reservoir address update', function () { - it('rejects setting L2 reservoir address if unauthorized', async function () { - const tx = l1Reservoir - .connect(testAccount1.signer) - .setL2ReservoirAddress(testAccount1.address) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - - it('updates the L2 reservoir address', async function () { - const tx = l1Reservoir.connect(governor.signer).setL2ReservoirAddress(testAccount1.address) - await expect(tx) - .emit(l1Reservoir, 'L2ReservoirAddressUpdated') - .withArgs(testAccount1.address) - expect(await l1Reservoir.l2ReservoirAddress()).eq(testAccount1.address) - }) - }) - describe('L2 rewards fraction update', function () { - it('rejects setting L2 rewards fraction if unauthorized', async function () { - const tx = l1Reservoir.connect(testAccount1.signer).setL2RewardsFraction(toGRT('1.025')) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - - it('rejects setting L2 rewards fraction to more than 1', async function () { - const newValue = toGRT('1').add(1) - const tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) - await expect(tx).revertedWith('L2 Rewards fraction must be <= 1') - }) - - it('should set L2 rewards fraction to maximum allowed', async function () { - const newValue = toGRT('1') // this gets a bignumber with 1e18 - const tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) - await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) - expect(await l1Reservoir.l2RewardsFraction()).eq(0) - expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) - }) - - it('should set L2 rewards fraction to apply on next drip', async function () { - const newValue = toGRT('0.25') - let tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) - await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) - expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) - tx = l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - await expect(tx).emit(l1Reservoir, 'L2RewardsFractionUpdated').withArgs(newValue) - expect(await l1Reservoir.l2RewardsFraction()).eq(newValue) - }) - }) - describe('minimum drip interval update', function () { - it('rejects setting minimum drip interval if unauthorized', async function () { - const tx = l1Reservoir.connect(testAccount1.signer).setMinDripInterval(toBN('200')) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - it('rejects setting minimum drip interval if equal to dripInterval', async function () { - const tx = l1Reservoir - .connect(governor.signer) - .setMinDripInterval(await l1Reservoir.dripInterval()) - await expect(tx).revertedWith('MUST_BE_LT_DRIP_INTERVAL') - }) - it('rejects setting minimum drip interval if larger than dripInterval', async function () { - const tx = l1Reservoir - .connect(governor.signer) - .setMinDripInterval((await l1Reservoir.dripInterval()).add(1)) - await expect(tx).revertedWith('MUST_BE_LT_DRIP_INTERVAL') - }) - it('sets the minimum drip interval', async function () { - const newValue = toBN('200') - const tx = l1Reservoir.connect(governor.signer).setMinDripInterval(newValue) - await expect(tx).emit(l1Reservoir, 'MinDripIntervalUpdated').withArgs(newValue) - expect(await l1Reservoir.minDripInterval()).eq(newValue) - }) - }) - describe('allowed drippers whitelist', function () { - it('only allows the governor to add a dripper', async function () { - const tx = l1Reservoir - .connect(testAccount1.signer) - .grantDripPermission(testAccount1.address) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - it('only allows the governor to revoke a dripper', async function () { - const tx = l1Reservoir.connect(testAccount1.signer).revokeDripPermission(keeper.address) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - it('allows adding an address to the allowed drippers', async function () { - const tx = l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) - await expect(tx).emit(l1Reservoir, 'AllowedDripperAdded').withArgs(testAccount1.address) - expect(await l1Reservoir.allowedDrippers(testAccount1.address)).eq(true) - }) - it('allows removing an address from the allowed drippers', async function () { - await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) - const tx = l1Reservoir.connect(governor.signer).revokeDripPermission(testAccount1.address) - await expect(tx).emit(l1Reservoir, 'AllowedDripperRevoked').withArgs(testAccount1.address) - expect(await l1Reservoir.allowedDrippers(testAccount1.address)).eq(false) - }) - }) - }) - - // TODO test that rewardsManager.updateAccRewardsPerSignal is called when - // issuanceRate or l2RewardsFraction is updated - describe('drip', function () { - it('cannot be called by an unauthorized address', async function () { - const tx = l1Reservoir - .connect(testAccount1.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) - await expect(tx).revertedWith('UNAUTHORIZED') - }) - it('can be called by an indexer', async function () { - const stakedAmount = toGRT('100000') - await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) - await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) - await staking.connect(testAccount1.signer).stake(stakedAmount) - const tx = l1Reservoir - .connect(testAccount1.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) - await expect(tx).emit(l1Reservoir, 'RewardsDripped') - }) - it('can be called by a whitelisted address', async function () { - await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) - const tx = l1Reservoir - .connect(testAccount1.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) - await expect(tx).emit(l1Reservoir, 'RewardsDripped') - }) - it('cannot be called with a zero address for the keeper reward beneficiary', async function () { - await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) - const tx = l1Reservoir - .connect(testAccount1.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), constants.AddressZero) - await expect(tx).revertedWith('INVALID_BENEFICIARY') - }) - it('(operator variant) cannot be called with an invalid indexer', async function () { - const tx = l1Reservoir - .connect(testAccount2.signer) - ['drip(uint256,uint256,uint256,address,address)']( - toBN(0), - toBN(0), - toBN(0), - testAccount1.address, - testAccount1.address, - ) - await expect(tx).revertedWith('UNAUTHORIZED_INVALID_INDEXER') - }) - it('(operator variant) cannot be called by someone who is not an operator for the right indexer', async function () { - const stakedAmount = toGRT('100000') - // testAccount1 is a valid indexer - await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) - await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) - await staking.connect(testAccount1.signer).stake(stakedAmount) - // testAccount2 is an operator for testAccount1's indexer - await staking.connect(testAccount1.signer).setOperator(testAccount2.address, true) - // testAccount3 is another valid indexer - await grt.connect(governor.signer).mint(testAccount3.address, stakedAmount) - await grt.connect(testAccount3.signer).approve(staking.address, stakedAmount) - await staking.connect(testAccount3.signer).stake(stakedAmount) - // But testAccount2 is not an operator for testAccount3's indexer - const tx = l1Reservoir - .connect(testAccount2.signer) - ['drip(uint256,uint256,uint256,address,address)']( - toBN(0), - toBN(0), - toBN(0), - testAccount1.address, - testAccount3.address, - ) - await expect(tx).revertedWith('UNAUTHORIZED_INVALID_OPERATOR') - }) - it('(operator variant) can be called by an indexer operator using an extra parameter', async function () { - const stakedAmount = toGRT('100000') - await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) - await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) - await staking.connect(testAccount1.signer).stake(stakedAmount) - await staking.connect(testAccount1.signer).setOperator(testAccount2.address, true) - const tx = l1Reservoir - .connect(testAccount2.signer) - ['drip(uint256,uint256,uint256,address,address)']( - toBN(0), - toBN(0), - toBN(0), - testAccount1.address, - testAccount1.address, - ) - await expect(tx).emit(l1Reservoir, 'RewardsDripped') - }) - it('mints rewards for the next week', async function () { - supplyBeforeDrip = await grt.totalSupply() - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - expect(startAccrued).to.eq(0) - const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - defaults.rewards.issuanceRate, - dripBlock, - ) - expect(await tracker.accRewards(dripBlock)).to.eq(0) - const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) - const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const tx = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - const actualAmount = await grt.balanceOf(l1Reservoir.address) - expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) - expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) - await expect(tx) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(actualAmount, toBN(0), expectedNextDeadline) - }) - it('cannot be called more than once per minDripInterval', async function () { - supplyBeforeDrip = await grt.totalSupply() - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - expect(startAccrued).to.eq(0) - const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - defaults.rewards.issuanceRate, - dripBlock, - ) - expect(await tracker.accRewards(dripBlock)).to.eq(0) - const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) - const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - - const tx1 = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - - const minInterval = toBN('200') - await l1Reservoir.connect(governor.signer).setMinDripInterval(minInterval) - - const actualAmount = await grt.balanceOf(l1Reservoir.address) - - expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) - await expect(tx1) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(actualAmount, toBN(0), expectedNextDeadline) - await expect(tx1) - .emit(grt, 'Transfer') - .withArgs(AddressZero, l1Reservoir.address, actualAmount) - - const tx2 = l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - await expect(tx2).revertedWith('WAIT_FOR_MIN_INTERVAL') - - // We've had 1 block since the last drip so far, so we jump to one block before the interval is done - await advanceBlocks(minInterval.sub(2)) - const tx3 = l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - await expect(tx3).revertedWith('WAIT_FOR_MIN_INTERVAL') - - await advanceBlocks(1) - // Now we're over the interval so we can drip again - const tx4 = l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - await expect(tx4).emit(l1Reservoir, 'RewardsDripped') - }) - it('prevents locking eth in the contract if l2RewardsFraction is 0', async function () { - const tx = l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - await expect(tx).revertedWith('No eth value needed') - }) - it('mints only a few more tokens if called on the next block', async function () { - await sequentialDoubleDrip(toBN(0)) - }) - it('mints the right amount of tokens if called before the drip period is over', async function () { - const dripInterval = toBN('100') - await l1Reservoir.connect(governor.signer).setDripInterval(dripInterval) - await sequentialDoubleDrip(toBN('50'), dripInterval) - }) - it('mints the right amount of tokens filling the gap if called after the drip period is over', async function () { - const dripInterval = toBN('100') - await l1Reservoir.connect(governor.signer).setDripInterval(dripInterval) - await sequentialDoubleDrip(toBN('150'), dripInterval) - }) - it('sends the specified fraction of the rewards with a callhook to L2', async function () { - await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - supplyBeforeDrip = await grt.totalSupply() - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - expect(startAccrued).to.eq(0) - const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - defaults.rewards.issuanceRate, - dripBlock, - ) - expect(await tracker.accRewards(dripBlock)).to.eq(0) - const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) - const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const expectedSentToL2 = expectedMintedAmount.div(2) - const tx = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - const actualAmount = await grt.balanceOf(l1Reservoir.address) - const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) - expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) - expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( - toRound(expectedMintedAmount), - ) - expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) - await expect(tx) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - - const l2IssuanceBase = (await l1Reservoir.issuanceBase()) - .mul(await l1Reservoir.l2RewardsFraction()) - .div(toGRT('1')) - const issuanceRate = await l1Reservoir.issuanceRate() - const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - l2IssuanceBase, - issuanceRate, - toBN('0'), - toBN('0'), - keeper.address, - ]) - const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - l1Reservoir.address, - mockL2Reservoir.address, - escrowedAmount, - expectedCallhookData, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) - }) - it('sends the specified fraction of the rewards with a keeper reward to L2', async function () { - await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - await l1Reservoir.connect(governor.signer).setDripRewardPerBlock(toGRT('3')) - await l1Reservoir.connect(governor.signer).setMinDripInterval(toBN('2')) - - await advanceBlocks(toBN('4')) - - supplyBeforeDrip = await grt.totalSupply() - const issuanceBase = await l1Reservoir.issuanceBase() - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - expect(startAccrued).to.eq(0) - const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction - const expectedKeeperReward = dripBlock - .sub(await l1Reservoir.lastRewardsUpdateBlock()) - .mul(toGRT('3')) - const tracker = await RewardsTracker.create( - issuanceBase, - defaults.rewards.issuanceRate, - dripBlock, - ) - expect(await tracker.accRewards(dripBlock)).to.eq(0) - const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) - const expectedMintedRewards = await tracker.accRewards(expectedNextDeadline) - const expectedMintedAmount = expectedMintedRewards.add(expectedKeeperReward) - const expectedSentToL2 = expectedMintedRewards.div(2).add(expectedKeeperReward) - const tx = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - const actualAmount = await grt.balanceOf(l1Reservoir.address) - const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) - - expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) - expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( - toRound(expectedMintedAmount), - ) - expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) - await expect(tx) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - - const l2IssuanceBase = (await l1Reservoir.issuanceBase()) - .mul(await l1Reservoir.l2RewardsFraction()) - .div(toGRT('1')) - const issuanceRate = await l1Reservoir.issuanceRate() - const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - l2IssuanceBase, - issuanceRate, - toBN('0'), - expectedKeeperReward, - keeper.address, - ]) - const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - l1Reservoir.address, - mockL2Reservoir.address, - escrowedAmount, - expectedCallhookData, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) - }) - it('sends the outstanding amount if the L2 rewards fraction changes', async function () { - await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - supplyBeforeDrip = await grt.totalSupply() - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - expect(startAccrued).to.eq(0) - const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - defaults.rewards.issuanceRate, - dripBlock, - ) - expect(await tracker.accRewards(dripBlock)).to.eq(0) - const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) - const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const expectedSentToL2 = expectedMintedAmount.div(2) - const tx = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - const actualAmount = await grt.balanceOf(l1Reservoir.address) - const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) - expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) - expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( - toRound(expectedMintedAmount), - ) - expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) - await expect(tx) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - - let l2IssuanceBase = (await l1Reservoir.issuanceBase()) - .mul(await l1Reservoir.l2RewardsFraction()) - .div(toGRT('1')) - const issuanceRate = await l1Reservoir.issuanceRate() - let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - l2IssuanceBase, - issuanceRate, - toBN('0'), - toBN('0'), - keeper.address, - ]) - let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - l1Reservoir.address, - mockL2Reservoir.address, - escrowedAmount, - expectedCallhookData, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) - - await tracker.snapshotRewards() - - await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.8')) - supplyBeforeDrip = await grt.totalSupply() - const secondDripBlock = (await latestBlock()).add(1) - const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) - const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) - const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) - const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) - // The amount sent to L2 should cover up to the new drip block with the old fraction, - // and from then onwards with the new fraction - const expectedNewTotalSentToL2 = rewardsUntilSecondDripBlock - .div(2) - .add(expectedTotalRewards.sub(rewardsUntilSecondDripBlock).mul(8).div(10)) - - const tx2 = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - const newActualAmount = await grt.balanceOf(l1Reservoir.address) - const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) - expect(toRound(newActualAmount)).to.eq( - toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), - ) - expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( - toRound(expectedNewMintedAmount), - ) - expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) - l2IssuanceBase = (await l1Reservoir.issuanceBase()) - .mul(await l1Reservoir.l2RewardsFraction()) - .div(toGRT('1')) - expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - l2IssuanceBase, - issuanceRate, - toBN('1'), // Incremented nonce - toBN('0'), - keeper.address, - ]) - expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - l1Reservoir.address, - mockL2Reservoir.address, - newEscrowedAmount.sub(escrowedAmount), - expectedCallhookData, - ) - await expect(tx2) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) - await expect(tx2) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs( - newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), - newEscrowedAmount.sub(escrowedAmount), - expectedNewNextDeadline, - ) - }) - it('sends the outstanding amount if the L2 rewards fraction stays constant', async function () { - await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - supplyBeforeDrip = await grt.totalSupply() - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - expect(startAccrued).to.eq(0) - const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - defaults.rewards.issuanceRate, - dripBlock, - ) - expect(await tracker.accRewards(dripBlock)).to.eq(0) - const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) - const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const expectedSentToL2 = expectedMintedAmount.div(2) - const tx = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - const actualAmount = await grt.balanceOf(l1Reservoir.address) - const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) - expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) - expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( - toRound(expectedMintedAmount), - ) - expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) - await expect(tx) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - - let l2IssuanceBase = (await l1Reservoir.issuanceBase()) - .mul(await l1Reservoir.l2RewardsFraction()) - .div(toGRT('1')) - const issuanceRate = await l1Reservoir.issuanceRate() - let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - l2IssuanceBase, - issuanceRate, - toBN('0'), - toBN('0'), - keeper.address, - ]) - let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - l1Reservoir.address, - mockL2Reservoir.address, - escrowedAmount, - expectedCallhookData, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) - - await tracker.snapshotRewards() - - supplyBeforeDrip = await grt.totalSupply() - const secondDripBlock = (await latestBlock()).add(1) - const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) - const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) - const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) - // The amount sent to L2 should cover up to the new drip block with the old fraction, - // and from then onwards with the new fraction - const expectedNewTotalSentToL2 = expectedTotalRewards.div(2) - - const tx2 = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - const newActualAmount = await grt.balanceOf(l1Reservoir.address) - const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) - expect(toRound(newActualAmount)).to.eq( - toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), - ) - expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( - toRound(expectedNewMintedAmount), - ) - expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) - l2IssuanceBase = (await l1Reservoir.issuanceBase()) - .mul(await l1Reservoir.l2RewardsFraction()) - .div(toGRT('1')) - expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - l2IssuanceBase, - issuanceRate, - toBN('1'), // Incremented nonce - toBN('0'), - keeper.address, - ]) - expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - l1Reservoir.address, - mockL2Reservoir.address, - newEscrowedAmount.sub(escrowedAmount), - expectedCallhookData, - ) - await expect(tx2) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) - await expect(tx2) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs( - newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), - newEscrowedAmount.sub(escrowedAmount), - expectedNewNextDeadline, - ) - }) - - it('reverts for a while but can be called again later if L2 fraction goes to zero', async function () { - await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - - // First drip call, sending half the rewards to L2 - supplyBeforeDrip = await grt.totalSupply() - const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) - expect(startAccrued).to.eq(0) - const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - defaults.rewards.issuanceRate, - dripBlock, - ) - expect(await tracker.accRewards(dripBlock)).to.eq(0) - const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) - const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const expectedSentToL2 = expectedMintedAmount.div(2) - const tx = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - const actualAmount = await grt.balanceOf(l1Reservoir.address) - const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) - expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) - expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( - toRound(expectedMintedAmount), - ) - expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) - await expect(tx) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - - let l2IssuanceBase = (await l1Reservoir.issuanceBase()) - .mul(await l1Reservoir.l2RewardsFraction()) - .div(toGRT('1')) - const issuanceRate = await l1Reservoir.issuanceRate() - let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - l2IssuanceBase, - issuanceRate, - toBN('0'), - toBN('0'), - keeper.address, - ]) - let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - l1Reservoir.address, - mockL2Reservoir.address, - escrowedAmount, - expectedCallhookData, - ) - await expect(tx) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) - - await tracker.snapshotRewards() - - await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0')) - - // Second attempt to drip immediately afterwards will revert, because we - // would have to send negative tokens to L2 to compensate - const tx2 = l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - await expect(tx2).revertedWith( - 'Negative amount would be sent to L2, wait before calling again', - ) - - await advanceBlocks(await l1Reservoir.dripInterval()) - - // Now we should be able to drip again, and a small amount will be sent to L2 - // to cover the few blocks since the drip interval was over - supplyBeforeDrip = await grt.totalSupply() - const secondDripBlock = (await latestBlock()).add(1) - const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) - const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) - const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) - const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) - // The amount sent to L2 should cover up to the new drip block with the old fraction, - // and from then onwards with the new fraction, that is zero - const expectedNewTotalSentToL2 = rewardsUntilSecondDripBlock.div(2) - - const tx3 = await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - const newActualAmount = await grt.balanceOf(l1Reservoir.address) - const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) - expect(toRound(newActualAmount)).to.eq( - toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), - ) - expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( - toRound(expectedNewMintedAmount), - ) - expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) - l2IssuanceBase = (await l1Reservoir.issuanceBase()) - .mul(await l1Reservoir.l2RewardsFraction()) - .div(toGRT('1')) - expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - l2IssuanceBase, - issuanceRate, - toBN('1'), // Incremented nonce - toBN('0'), - keeper.address, - ]) - expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( - grt.address, - l1Reservoir.address, - mockL2Reservoir.address, - newEscrowedAmount.sub(escrowedAmount), - expectedCallhookData, - ) - await expect(tx3) - .emit(l1GraphTokenGateway, 'TxToL2') - .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) - await expect(tx3) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs( - newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), - newEscrowedAmount.sub(escrowedAmount), - expectedNewNextDeadline, - ) - }) - }) - - context('calculating rewards', async function () { - beforeEach(async function () { - // 5% minute rate (4 blocks) - await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - supplyBeforeDrip = await grt.totalSupply() - await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - dripBlock = await latestBlock() - }) - - describe('getAccumulatedRewards', function () { - it('returns rewards accrued after some blocks', async function () { - await shouldGetNewRewards(supplyBeforeDrip) - }) - it('returns zero if evaluated at the block where reservoir had the first drip', async function () { - await shouldGetNewRewards( - supplyBeforeDrip, - ISSUANCE_RATE_PERIODS, - dripBlock, - toBN(0), - false, - ) - }) - it('returns the supply times issuance rate one block after the first drip', async function () { - const expectedVal = supplyBeforeDrip - .mul(ISSUANCE_RATE_PER_BLOCK.sub(toGRT(1))) - .div(toGRT(1)) - await shouldGetNewRewards( - supplyBeforeDrip, - ISSUANCE_RATE_PERIODS, - dripBlock.add(1), - expectedVal, - false, - ) - }) - it('returns the rewards for a block some time in the future', async function () { - await shouldGetNewRewards(supplyBeforeDrip, toBN(1), dripBlock.add(10000)) - }) - }) - describe('getNewRewards', function () { - const computeDelta = function (t1: BigNumber, t0: BigNumber, lambda = toBN(0)): BigNumber { - const deltaT = new BN(t1.toString()).minus(new BN(t0.toString())) - const rate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) - const supply = new BN(supplyBeforeDrip.toString()) - return toBN(supply.times(rate.pow(deltaT)).minus(supply).precision(18).toString(10)) - .mul(toGRT('1').sub(lambda)) - .div(toGRT('1')) - } - it('computes the rewards delta between the last drip block and the current block', async function () { - const t0 = dripBlock - const t1 = t0.add(200) - const expectedVal = computeDelta(t1, t0) - expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) - }) - it('returns zero rewards if the time delta is zero', async function () { - const t0 = dripBlock - const expectedVal = toBN('0') - expect(await l1Reservoir.getNewRewards(t0)).to.eq(expectedVal) - }) - it('computes the rewards delta between a past drip block and a future block', async function () { - await advanceBlocks(20) - const t0 = dripBlock - const t1 = t0.add(100) - const expectedVal = computeDelta(t1, t0) - expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) - }) - it('computes the rewards delta between a past drip block and the current block', async function () { - await advanceBlocks(20) - const t0 = dripBlock - const t1 = await latestBlock() - const expectedVal = computeDelta(t1, t0) - expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) - }) - it('computes the rewards delta considering the L2 rewards fraction', async function () { - const lambda = toGRT('0.32') - await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) - await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)']( - maxGas, - gasPriceBid, - maxSubmissionCost, - keeper.address, - { value: defaultEthValue }, - ) - supplyBeforeDrip = await l1Reservoir.issuanceBase() // Has been updated accordingly - dripBlock = await latestBlock() - await advanceBlocks(20) - const t0 = dripBlock - const t1 = await latestBlock() - - const expectedVal = computeDelta(t1, t0, lambda) - expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) - }) - }) - }) - - describe('pow', function () { - it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { - const baseRatio = toGRT('0.000000004641377923') // 1% annual rate - const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks - const powPrecision = 14 // Compare up to this amount of significant digits - BN.config({ POW_PRECISION: 100 }) - for (let i = 0; i < 50; i = i + 4) { - const r = baseRatio.mul(i * 4).add(toGRT('1')) - const h = await reservoirMock.pow(r, timePeriods, toGRT('1')) - console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) - expect(new BN(h.toString()).precision(powPrecision).toString(10)).to.eq( - new BN(r.toString()) - .div(1e18) - .pow(timePeriods) - .times(1e18) - .precision(powPrecision) - .toString(10), - ) - } - }) - }) -}) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 38639acc4..23aee0dfa 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -1,16 +1,17 @@ import { expect } from 'chai' -import { constants, BigNumber, ContractReceipt } from 'ethers' +import { constants, BigNumber } from 'ethers' +import { BigNumber as BN } from 'bignumber.js' +import { deployContract } from '../lib/deployment' import { NetworkFixture } from '../lib/fixtures' import { Curation } from '../../build/types/Curation' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' import { RewardsManager } from '../../build/types/RewardsManager' +import { RewardsManagerMock } from '../../build/types/RewardsManagerMock' import { Staking } from '../../build/types/Staking' -import { BigNumber as BN } from 'bignumber.js' - import { advanceBlocks, deriveChannelKey, @@ -23,10 +24,7 @@ import { Account, advanceToNextEpoch, provider, - RewardsTracker, } from '../lib/testHelpers' -import { L1Reservoir } from '../../build/types/L1Reservoir' -import { LogDescription } from 'ethers/lib/utils' const MAX_PPM = 1000000 @@ -42,7 +40,6 @@ describe('Rewards', () => { let indexer1: Account let indexer2: Account let oracle: Account - let keeper: Account let fixture: NetworkFixture @@ -51,10 +48,7 @@ describe('Rewards', () => { let epochManager: EpochManager let staking: Staking let rewardsManager: RewardsManager - let l1Reservoir: L1Reservoir - - let supplyBeforeDrip: BigNumber - let dripBlock: BigNumber + let rewardsManagerMock: RewardsManagerMock // Derive some channel keys for each indexer used to sign attestations const channelKey1 = deriveChannelKey() @@ -68,18 +62,64 @@ describe('Rewards', () => { const metadata = HashZero - const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 0.05% rewards - const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 5% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1012272234429039270') // % increase every block + + // Core formula that gets accumulated rewards per signal for a period of time + const getRewardsPerSignal = (p: BN, r: BN, t: BN, s: BN): string => { + if (s.eq(0)) { + return '0' + } + return p.times(r.pow(t)).minus(p).div(s).toPrecision(18).toString() + } + + // Tracks the accumulated rewards as totalSignalled or supply changes across snapshots + class RewardsTracker { + totalSupply = BigNumber.from(0) + totalSignalled = BigNumber.from(0) + lastUpdatedBlock = BigNumber.from(0) + accumulated = BigNumber.from(0) + + static async create() { + const tracker = new RewardsTracker() + await tracker.snapshot() + return tracker + } + + async snapshot() { + this.accumulated = this.accumulated.add(await this.accrued()) + this.totalSupply = await grt.totalSupply() + this.totalSignalled = await grt.balanceOf(curation.address) + this.lastUpdatedBlock = await latestBlock() + return this + } + + async elapsedBlocks() { + const currentBlock = await latestBlock() + return currentBlock.sub(this.lastUpdatedBlock) + } + + async accrued() { + const nBlocks = await this.elapsedBlocks() + return this.accruedByElapsed(nBlocks) + } + + async accruedByElapsed(nBlocks: BigNumber | number) { + const n = getRewardsPerSignal( + new BN(this.totalSupply.toString()), + new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18), + new BN(nBlocks.toString()), + new BN(this.totalSignalled.toString()), + ) + return toGRT(n) + } + } // Test accumulated rewards per signal - const shouldGetNewRewardsPerSignal = async ( - initialSupply: BigNumber, - nBlocks = ISSUANCE_RATE_PERIODS, - dripBlock?: BigNumber, - ) => { + const shouldGetNewRewardsPerSignal = async (nBlocks = ISSUANCE_RATE_PERIODS) => { // -- t0 -- - const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK, dripBlock) - await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + const tracker = await RewardsTracker.create() + // Jump await advanceBlocks(nBlocks) @@ -88,43 +128,35 @@ describe('Rewards', () => { // Contract calculation const contractAccrued = await rewardsManager.getNewRewardsPerSignal() // Local calculation - const expectedAccrued = await tracker.newRewardsPerSignal(await grt.balanceOf(curation.address)) + const expectedAccrued = await tracker.accrued() // Check - expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) + expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) return expectedAccrued } - const findRewardsManagerEvents = (receipt: ContractReceipt): Array => { - return receipt.logs - .map((l) => { - try { - return rewardsManager.interface.parseLog(l) - } catch { - return null - } - }) - .filter((l) => !!l) - } - before(async function () { - ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle, keeper] = - await getAccounts() + ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle] = await getAccounts() fixture = new NetworkFixture() - ;({ grt, curation, epochManager, staking, rewardsManager, l1Reservoir } = await fixture.load( + ;({ grt, curation, epochManager, staking, rewardsManager } = await fixture.load( governor.signer, )) + rewardsManagerMock = (await deployContract( + 'RewardsManagerMock', + governor.signer, + )) as unknown as RewardsManagerMock + + // 5% minute rate (4 blocks) + await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { await grt.connect(governor.signer).mint(wallet.address, toGRT('1000000')) await grt.connect(wallet.signer).approve(staking.address, toGRT('1000000')) await grt.connect(wallet.signer).approve(curation.address, toGRT('1000000')) } - await l1Reservoir.connect(governor.signer).grantDripPermission(keeper.address) - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) - supplyBeforeDrip = await grt.totalSupply() }) beforeEach(async function () { @@ -136,6 +168,32 @@ describe('Rewards', () => { }) describe('configuration', function () { + describe('issuance rate update', function () { + it('reject set issuance rate if unauthorized', async function () { + const tx = rewardsManager.connect(indexer1.signer).setIssuanceRate(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('reject set issuance rate to less than minimum allowed', async function () { + const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 + const tx = rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).revertedWith('Issuance rate under minimum allowed') + }) + + it('should set issuance rate to minimum allowed', async function () { + const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 + await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) + expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) + }) + + it('should set issuance rate', async function () { + const newIssuanceRate = toGRT('1.025') + await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) + expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) + expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await latestBlock()) + }) + }) + describe('subgraph availability service', function () { it('reject set subgraph oracle if unauthorized', async function () { const tx = rewardsManager @@ -185,104 +243,9 @@ describe('Rewards', () => { }) context('issuing rewards', async function () { - interface DelegationParameters { - indexingRewardCut: BigNumber - queryFeeCut: BigNumber - cooldownBlocks: number - } - - async function setupIndexerAllocation() { - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - } - - async function setupIndexerAllocationWithDelegation( - tokensToDelegate: BigNumber, - delegationParams: DelegationParameters, - ) { - const tokensToAllocate = toGRT('12500') - - // Transfer some funds from the curator, I don't want to mint new tokens - await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate) - await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate) - - // Stake and set delegation parameters - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .setDelegationParameters( - delegationParams.indexingRewardCut, - delegationParams.queryFeeCut, - delegationParams.cooldownBlocks, - ) - - // Delegate - await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate) - - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - - // Allocate - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - } - - function calculatedExpectedRewards( - firstSnapshotBlocks: BN, - lastSnapshotBlocks: BN, - allocatedTokens: BN, - ): BigNumber { - const issuanceBase = new BN(10004000000) - const issuanceRate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (issuanceBase * issuanceRate^nBlocks - issuanceBase) / allocatedTokens - // The first snapshot is after allocating, that is lastSnapshotBlocks blocks after dripBlock: - const startRewardsPerToken = issuanceBase - .times(issuanceRate.pow(firstSnapshotBlocks)) - .minus(issuanceBase) - .div(allocatedTokens) - // The final snapshot is when we close the allocation, that happens 8 blocks later: - const endRewardsPerToken = issuanceBase - .times(issuanceRate.pow(lastSnapshotBlocks)) - .minus(issuanceBase) - .div(allocatedTokens) - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * allocatedTokens. - return toGRT( - endRewardsPerToken.minus(startRewardsPerToken).times(allocatedTokens).toPrecision(18), - ) - } - beforeEach(async function () { // 5% minute rate (4 blocks) - await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - await l1Reservoir - .connect(keeper.signer) - ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) - dripBlock = await latestBlock() + await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) }) describe('getNewRewardsPerSignal', function () { @@ -299,7 +262,7 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, tokensToSignal, 0) // Check - await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) + await shouldGetNewRewardsPerSignal() }) it('accrued per signal when signalled tokens w/ many subgraphs', async function () { @@ -307,112 +270,78 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) // Check - await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) + await shouldGetNewRewardsPerSignal() // Update total signalled await curation.connect(curator2.signer).mint(subgraphDeploymentID2, toGRT('250'), 0) // Check - await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) + await shouldGetNewRewardsPerSignal() }) }) describe('updateAccRewardsPerSignal', function () { it('update the accumulated rewards per signal state', async function () { - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - ISSUANCE_RATE_PER_BLOCK, - dripBlock, - ) - // Snapshot - const prevSignal = await grt.balanceOf(curation.address) // Update total signalled await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) - // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, - // so we snapshot using the previous value - await tracker.snapshotPerSignal(prevSignal) + // Snapshot + const tracker = await RewardsTracker.create() // Update await rewardsManager.updateAccRewardsPerSignal() - await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) - const contractAccrued = await rewardsManager.accRewardsPerSignal() // Check - const blockNum = await latestBlock() - const expectedAccrued = await tracker.accRewardsPerSignal( - await grt.balanceOf(curation.address), - blockNum, - ) - expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) + const expectedAccrued = await tracker.accrued() + expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) }) it('update the accumulated rewards per signal state after many blocks', async function () { - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - ISSUANCE_RATE_PER_BLOCK, - dripBlock, - ) - // Snapshot - const prevSignal = await grt.balanceOf(curation.address) // Update total signalled await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) - // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, - // so we snapshot using the previous value - await tracker.snapshotPerSignal(prevSignal) + // Snapshot + const tracker = await RewardsTracker.create() // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.updateAccRewardsPerSignal() - await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) const contractAccrued = await rewardsManager.accRewardsPerSignal() - const blockNum = await latestBlock() - const expectedAccrued = await tracker.accRewardsPerSignal( - await grt.balanceOf(curation.address), - blockNum.add(0), - ) - expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) + // Check + const expectedAccrued = await tracker.accrued() + expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) }) }) describe('getAccRewardsForSubgraph', function () { it('accrued for each subgraph', async function () { - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - ISSUANCE_RATE_PER_BLOCK, - dripBlock, - ) - // Snapshot - let prevSignal = await grt.balanceOf(curation.address) // Curator1 - Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - const sg1Snapshot = await tracker.snapshotPerSignal(prevSignal) + const tracker1 = await RewardsTracker.create() // Curator2 - Update total signalled const signalled2 = toGRT('500') - prevSignal = await grt.balanceOf(curation.address) await curation.connect(curator2.signer).mint(subgraphDeploymentID2, signalled2, 0) - const sg2Snapshot = await tracker.snapshotPerSignal(prevSignal) + + // Snapshot + const tracker2 = await RewardsTracker.create() + await tracker1.snapshot() // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) + // Snapshot + await tracker1.snapshot() + await tracker2.snapshot() + // Calculate rewards - const rewardsPerSignal = await tracker.accRewardsPerSignal( - await grt.balanceOf(curation.address), - ) - const expectedRewardsSG1 = rewardsPerSignal - .sub(sg1Snapshot) - .mul(signalled1) - .div(WeiPerEther) - const expectedRewardsSG2 = rewardsPerSignal - .sub(sg2Snapshot) - .mul(signalled2) - .div(WeiPerEther) + const rewardsPerSignal1 = await tracker1.accumulated + const rewardsPerSignal2 = await tracker2.accumulated + const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) + const expectedRewardsSG2 = rewardsPerSignal2.mul(signalled2).div(WeiPerEther) // Get rewards from contract const contractRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph( @@ -430,35 +359,27 @@ describe('Rewards', () => { describe('onSubgraphSignalUpdate', function () { it('update the accumulated rewards for subgraph state', async function () { - const tracker = await RewardsTracker.create( - supplyBeforeDrip, - ISSUANCE_RATE_PER_BLOCK, - dripBlock, - ) - // Snapshot - const prevSignal = await grt.balanceOf(curation.address) // Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) // Snapshot - await tracker.snapshotPerSignal(prevSignal) + const tracker1 = await RewardsTracker.create() // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.onSubgraphSignalUpdate(subgraphDeploymentID1) - const snapshot = await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + // Check const contractRewardsSG1 = (await rewardsManager.subgraphs(subgraphDeploymentID1)) .accRewardsForSubgraph - const expectedRewardsSG1 = snapshot.mul(signalled1).div(WeiPerEther) + const rewardsPerSignal1 = await tracker1.accrued() + const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1)) const contractAccrued = await rewardsManager.accRewardsPerSignal() - const expectedAccrued = await tracker.accRewardsPerSignal( - await grt.balanceOf(curation.address), - ) + const expectedAccrued = await tracker1.accrued() expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) const contractBlockUpdated = await rewardsManager.accRewardsPerSignalLastBlockUpdated() @@ -509,14 +430,11 @@ describe('Rewards', () => { it('update the accumulated rewards for allocated tokens state', async function () { // Update total signalled const signalled1 = toGRT('1500') - // block = dripBlock await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - // block = dripBlock + 1 // Allocate const tokensToAllocate = toGRT('12500') await staking.connect(indexer1.signer).stake(tokensToAllocate) - // block = dripBlock + 2 await staking .connect(indexer1.signer) .allocateFrom( @@ -527,28 +445,19 @@ describe('Rewards', () => { metadata, await channelKey1.generateProof(indexer1.address), ) - // block = dripBlock + 3 + // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) - // block = dripBlock + 7 + + // Prepare expected results + // NOTE: calculated the expected result manually as the above code has 1 off block difference + // replace with a RewardsManagerMock + const expectedSubgraphRewards = toGRT('891695470') + const expectedRewardsAT = toGRT('51571') // Update await rewardsManager.onSubgraphAllocationUpdate(subgraphDeploymentID1) - // block = dripBlock + 8 - // Prepare expected results - // Expected total rewards: - // DeltaR_end = supplyBeforeDrip * r ^ 8 - supplyBeforeDrip - // DeltaR_end = 10004000000 GRT * (1000122722344290393 / 1e18)^8 - 10004000000 GRT = 9825934.397 - // The signal was minted at dripBlock + 1, so: - // DeltaR_start = supplyBeforeDrip * r ^ 1 - supplyBeforeDrip = 1227714.332 - - // And they all go to this subgraph, so subgraph rewards = DeltaR_end - DeltaR_start = 8598220.065 - const expectedSubgraphRewards = toGRT('8598220') - - // The allocation happened at dripBlock + 3, so rewards per allocated token are: - // ((supplyBeforeDrip * r ^ 8 - supplyBeforeDrip) - (supplyBeforeDrip * r ^ 3 - supplyBeforeDrip)) / 12500 = 491.387 - const expectedRewardsAT = toGRT('491') // Check on demand results saved const subgraph = await rewardsManager.subgraphs(subgraphDeploymentID1) const contractSubgraphRewards = await rewardsManager.getAccRewardsForSubgraph( @@ -598,77 +507,107 @@ describe('Rewards', () => { }) }) - describe('takeAndBurnRewards', function () { - it('should burn rewards on closed allocation with POI zero', async function () { - // Align with the epoch boundary + describe('takeRewards', function () { + interface DelegationParameters { + indexingRewardCut: BigNumber + queryFeeCut: BigNumber + cooldownBlocks: number + } + + async function setupIndexerAllocation() { + // Setup await epochManager.setEpochLength(10) - await advanceToNextEpoch(epochManager) + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + async function setupIndexerAllocationSignalingAfter() { // Setup - await setupIndexerAllocation() - const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + await epochManager.setEpochLength(10) - // Jump - await advanceToNextEpoch(epochManager) + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) - // Before state - const beforeTokenSupply = await grt.totalSupply() - const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) - const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) - const beforeStakingBalance = await grt.balanceOf(staking.address) + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + } - // Close allocation with POI zero, which should burn the rewards - const tx = await staking.connect(indexer1.signer).closeAllocation(allocationID1, HashZero) - const receipt = await tx.wait() + async function setupIndexerAllocationWithDelegation( + tokensToDelegate: BigNumber, + delegationParams: DelegationParameters, + ) { + const tokensToAllocate = toGRT('12500') - const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + // Setup + await epochManager.setEpochLength(10) - const expectedIndexingRewards = calculatedExpectedRewards( - firstSnapshotBlocks, - lastSnapshotBlocks, - new BN(12500), - ) + // Transfer some funds from the curator, I don't want to mint new tokens + await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate) + await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate) - const log = findRewardsManagerEvents(receipt)[0] - const event = log.args - expect(log.name).eq('RewardsBurned') - expect(event.indexer).eq(indexer1.address) - expect(event.allocationID).eq(allocationID1) - expect(event.epoch).eq(await epochManager.currentEpoch()) - expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) + // Stake and set delegation parameters + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .setDelegationParameters( + delegationParams.indexingRewardCut, + delegationParams.queryFeeCut, + delegationParams.cooldownBlocks, + ) - // After state - const afterTokenSupply = await grt.totalSupply() - const afterIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) - const afterIndexer1Balance = await grt.balanceOf(indexer1.address) - const afterStakingBalance = await grt.balanceOf(staking.address) + // Delegate + await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate) - // Check that rewards are NOT put into indexer stake - const expectedIndexerStake = beforeIndexer1Stake + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - // Check stake should NOT have increased with the rewards staked - expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) - // Check indexer balance remains the same - expect(afterIndexer1Balance).eq(beforeIndexer1Balance) - // Check indexing rewards are kept in the staking contract - expect(toRound(afterStakingBalance)).eq(toRound(beforeStakingBalance)) - // Check that tokens have been burned - // We divide by 10 to accept numeric errors up to 10 GRT - expect(toRound(afterTokenSupply.div(10))).eq( - toRound(beforeTokenSupply.sub(expectedIndexingRewards).div(10)), - ) - }) - }) + // Allocate + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } - describe('takeRewards', function () { it('should distribute rewards on closed allocation and stake', async function () { // Align with the epoch boundary - await epochManager.setEpochLength(10) await advanceToNextEpoch(epochManager) - // Setup await setupIndexerAllocation() - const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) @@ -679,20 +618,21 @@ describe('Rewards', () => { const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) const beforeStakingBalance = await grt.balanceOf(staking.address) + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted: + // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 + // The final snapshot is when we close the allocation, that happens 9 blocks later: + // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 + // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. + const expectedIndexingRewards = toGRT('913715958') + // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() - - const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) - const expectedIndexingRewards = calculatedExpectedRewards( - firstSnapshotBlocks, - lastSnapshotBlocks, - new BN(12500), - ) - - const event = findRewardsManagerEvents(receipt)[0].args + const event = rewardsManager.interface.parseLog(receipt.logs[1]).args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) @@ -706,7 +646,7 @@ describe('Rewards', () => { // Check that rewards are put into indexer stake const expectedIndexerStake = beforeIndexer1Stake.add(expectedIndexingRewards) - + const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) // Check stake should have increased with the rewards staked expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) // Check indexer balance remains the same @@ -715,20 +655,71 @@ describe('Rewards', () => { expect(toRound(afterStakingBalance)).eq( toRound(beforeStakingBalance.add(expectedIndexingRewards)), ) - // Check that tokens have NOT been minted - expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) + // Check that tokens have been minted + expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + }) + + it('does not revert with an underflow if the minimum signal changes', async function () { + // Align with the epoch boundary + await advanceToNextEpoch(epochManager) + // Setup + await setupIndexerAllocation() + + await rewardsManager.connect(governor.signer).setMinimumSubgraphSignal(toGRT(14000)) + + // Jump + await advanceToNextEpoch(epochManager) + + // Close allocation. At this point rewards should be collected for that indexer + const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx) + .emit(rewardsManager, 'RewardsAssigned') + .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch(), toBN(0)) + }) + + it('does not revert with an underflow if the minimum signal changes, and signal came after allocation', async function () { + // Align with the epoch boundary + await advanceToNextEpoch(epochManager) + // Setup + await setupIndexerAllocationSignalingAfter() + + await rewardsManager.connect(governor.signer).setMinimumSubgraphSignal(toGRT(14000)) + + // Jump + await advanceToNextEpoch(epochManager) + + // Close allocation. At this point rewards should be collected for that indexer + const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx) + .emit(rewardsManager, 'RewardsAssigned') + .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch(), toBN(0)) + }) + + it('does not revert if signal was already under minimum', async function () { + await rewardsManager.connect(governor.signer).setMinimumSubgraphSignal(toGRT(2000)) + // Align with the epoch boundary + await advanceToNextEpoch(epochManager) + // Setup + await setupIndexerAllocation() + + // Jump + await advanceToNextEpoch(epochManager) + // Close allocation. At this point rewards should be collected for that indexer + const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + + await expect(tx) + .emit(rewardsManager, 'RewardsAssigned') + .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch(), toBN(0)) }) it('should distribute rewards on closed allocation and send to destination', async function () { const destinationAddress = randomHexBytes(20) await staking.connect(indexer1.signer).setRewardsDestination(destinationAddress) - await epochManager.setEpochLength(10) // Align with the epoch boundary await advanceToNextEpoch(epochManager) // Setup await setupIndexerAllocation() - const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) @@ -739,22 +730,24 @@ describe('Rewards', () => { const beforeDestinationBalance = await grt.balanceOf(destinationAddress) const beforeStakingBalance = await grt.balanceOf(staking.address) + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted: + // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 + // The final snapshot is when we close the allocation, that happens 9 blocks later: + // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 + // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. + const expectedIndexingRewards = toGRT('913715958') + // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() - const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) - const event = findRewardsManagerEvents(receipt)[0].args + const event = rewardsManager.interface.parseLog(receipt.logs[1]).args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) - - const expectedIndexingRewards = calculatedExpectedRewards( - firstSnapshotBlocks, - lastSnapshotBlocks, - new BN(12500), - ) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) // After state @@ -765,7 +758,7 @@ describe('Rewards', () => { // Check that rewards are properly assigned const expectedIndexerStake = beforeIndexer1Stake - + const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) // Check stake should not have changed expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) // Check indexing rewards are received by the rewards destination @@ -774,8 +767,8 @@ describe('Rewards', () => { ) // Check indexing rewards were not sent to the staking contract expect(afterStakingBalance).eq(beforeStakingBalance) - // Check that tokens have NOT been minted - expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) + // Check that tokens have been minted + expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) }) it('should distribute rewards on closed allocation w/delegators', async function () { @@ -786,18 +779,14 @@ describe('Rewards', () => { cooldownBlocks: 5, } const tokensToDelegate = toGRT('2000') - await epochManager.setEpochLength(10) // Align with the epoch boundary await advanceToNextEpoch(epochManager) - // Setup the allocation and delegators await setupIndexerAllocationWithDelegation(tokensToDelegate, delegationParams) - const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) - // dripBlock + 13 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -806,7 +795,6 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // After state const afterTokenSupply = await grt.totalSupply() @@ -816,12 +804,14 @@ describe('Rewards', () => { // Check that rewards are put into indexer stake (only indexer cut) // Check that rewards are put into delegators pool accordingly - const expectedIndexingRewards = calculatedExpectedRewards( - firstSnapshotBlocks, - lastSnapshotBlocks, - new BN(14500), - ) - + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted: + // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 14500 = 8466.995 + // The final snapshot is when we close the allocation, that happens 4 blocks later: + // endRewardsPerToken = (10004000000 * 1.01227 ^ 4 - 10004000000) / 14500 = 34496.55 + // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. + const expectedIndexingRewards = toGRT('377428566.77') // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards) @@ -831,162 +821,101 @@ describe('Rewards', () => { // Check const expectedIndexerStake = beforeIndexer1Stake.add(indexerRewards) const expectedDelegatorsPoolTokens = beforeDelegationPool.tokens.add(delegatorsRewards) + const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) expect(toRound(afterDelegationPool.tokens)).eq(toRound(expectedDelegatorsPoolTokens)) - // Check that tokens have NOT been minted - expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) + // Check that tokens have been minted + expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) }) - it('should deny and burn rewards if subgraph on denylist', async function () { + it('should deny rewards if subgraph on denylist', async function () { // Setup - // dripBlock (82) - await epochManager.setEpochLength(10) - // dripBlock + 1 await rewardsManager .connect(governor.signer) .setSubgraphAvailabilityOracle(governor.address) - // dripBlock + 2 await rewardsManager.connect(governor.signer).setDenied(subgraphDeploymentID1, true) - // dripBlock + 3 (epoch boundary!) - await advanceToNextEpoch(epochManager) - // dripBlock + 13 await setupIndexerAllocation() - const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) - // dripBlock + 23 - const supplyBefore = await grt.totalSupply() // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx).emit(rewardsManager, 'RewardsDenied') - const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) - const receipt = await (await tx).wait() - const logs = findRewardsManagerEvents(receipt) - expect(logs.length).to.eq(1) - expect(logs[0].name).to.eq('RewardsDenied') - const ev = logs[0].args - expect(ev.indexer).to.eq(indexer1.address) - expect(ev.allocationID).to.eq(allocationID1) - expect(ev.epoch).to.eq(await epochManager.currentEpoch()) - - const expectedIndexingRewards = calculatedExpectedRewards( - firstSnapshotBlocks, - lastSnapshotBlocks, - new BN(12500), - ) - expect(toRound(ev.amount)).to.eq(toRound(expectedIndexingRewards)) - // Check that the rewards were burned - // We divide by 10 to accept numeric errors up to 10 GRT - expect(toRound((await grt.totalSupply()).div(10))).to.eq( - toRound(supplyBefore.sub(expectedIndexingRewards).div(10)), - ) + await expect(tx) + .emit(rewardsManager, 'RewardsDenied') + .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch()) }) }) + }) - describe('edge scenarios', function () { - it('close allocation on a subgraph that no longer have signal', async function () { - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) + describe('pow', function () { + it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { + const baseRatio = toGRT('0.000000004641377923') // 1% annual rate + const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks + for (let i = 0; i < 50; i = i + 4) { + const r = baseRatio.mul(i * 4).add(toGRT('1')) + const h = await rewardsManagerMock.pow(r, timePeriods, toGRT('1')) + console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) + } + }) + }) - // Jump - await advanceToNextEpoch(epochManager) + describe('edge scenarios', function () { + it('close allocation on a subgraph that no longer have signal', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - // Remove all signal from the subgraph - const curatorShares = await curation.getCuratorSignal( - curator1.address, + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), ) - await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) - // Close allocation. At this point rewards should be collected for that indexer - await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - }) - }) + // Jump + await advanceToNextEpoch(epochManager) - describe('multiple allocations', function () { - it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { - // If rewards are not monotonically increasing, this can trigger - // a subtraction overflow error as seen in mainnet tx: - // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab - await advanceToNextEpoch(epochManager) + // Remove all signal from the subgraph + const curatorShares = await curation.getCuratorSignal(curator1.address, subgraphDeploymentID1) + await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) - // Setup - await epochManager.setEpochLength(10) - - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - - // Stake - const tokensToStake = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToStake) - - // Allocate simultaneously, burning in the middle - const tokensToAlloc = toGRT('5000') - await provider().send('evm_setAutomine', [false]) - const tx1 = await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAlloc, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) - const tx3 = await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAlloc, - allocationID2, - metadata, - await channelKey2.generateProof(indexer1.address), - ) + // Close allocation. At this point rewards should be collected for that indexer + await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + }) + }) - await provider().send('evm_mine', []) - await provider().send('evm_setAutomine', [true]) + describe('multiple allocations', function () { + it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { + // If rewards are not monotonically increasing, this can trigger + // a subtraction overflow error as seen in mainnet tx: + // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab + await advanceToNextEpoch(epochManager) - await expect(tx1).emit(staking, 'AllocationCreated') - await expect(tx2).emit(grt, 'Transfer') - await expect(tx3).emit(staking, 'AllocationCreated') - }) - it('two simultanous-similar allocations should get same amount of rewards', async function () { - await advanceToNextEpoch(epochManager) - - // Setup - await epochManager.setEpochLength(10) + // Setup + await epochManager.setEpochLength(10) - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - // Stake - const tokensToStake = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToStake) + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) - // Allocate simultaneously - const tokensToAlloc = toGRT('5000') - const tx1 = await staking.populateTransaction.allocateFrom( + // Allocate simultaneously, burning in the middle + const tokensToAlloc = toGRT('5000') + await provider().send('evm_setAutomine', [false]) + const tx1 = await staking + .connect(indexer1.signer) + .allocateFrom( indexer1.address, subgraphDeploymentID1, tokensToAlloc, @@ -994,7 +923,10 @@ describe('Rewards', () => { metadata, await channelKey1.generateProof(indexer1.address), ) - const tx2 = await staking.populateTransaction.allocateFrom( + const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) + const tx3 = await staking + .connect(indexer1.signer) + .allocateFrom( indexer1.address, subgraphDeploymentID1, tokensToAlloc, @@ -1002,31 +934,61 @@ describe('Rewards', () => { metadata, await channelKey2.generateProof(indexer1.address), ) - await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) - // Jump - await advanceToNextEpoch(epochManager) + await provider().send('evm_mine', []) + await provider().send('evm_setAutomine', [true]) - // Close allocations simultaneously - const tx3 = await staking.populateTransaction.closeAllocation( - allocationID1, - randomHexBytes(), - ) - const tx4 = await staking.populateTransaction.closeAllocation( - allocationID2, - randomHexBytes(), - ) - const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) - - // Both allocations should receive the same amount of rewards - const receipt = await tx5.wait() - const rewardsMgrEvents = findRewardsManagerEvents(receipt) - expect(rewardsMgrEvents.length).to.eq(2) - const event1 = rewardsMgrEvents[0].args - const event2 = rewardsMgrEvents[1].args - expect(event1.amount).to.not.eq(toBN(0)) - expect(event1.amount).to.eq(event2.amount) - }) + await expect(tx1).emit(staking, 'AllocationCreated') + await expect(tx2).emit(grt, 'Transfer') + await expect(tx3).emit(staking, 'AllocationCreated') + }) + it('two simultanous-similar allocations should get same amount of rewards', async function () { + await advanceToNextEpoch(epochManager) + + // Setup + await epochManager.setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) + + // Allocate simultaneously + const tokensToAlloc = toGRT('5000') + const tx1 = await staking.populateTransaction.allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + const tx2 = await staking.populateTransaction.allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) + await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) + + // Jump + await advanceToNextEpoch(epochManager) + + // Close allocations simultaneously + const tx3 = await staking.populateTransaction.closeAllocation(allocationID1, randomHexBytes()) + const tx4 = await staking.populateTransaction.closeAllocation(allocationID2, randomHexBytes()) + const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) + + // Both allocations should receive the same amount of rewards + const receipt = await tx5.wait() + const event1 = rewardsManager.interface.parseLog(receipt.logs[1]).args + const event2 = rewardsManager.interface.parseLog(receipt.logs[5]).args + expect(event1.amount).eq(event2.amount) }) }) }) From 5ed2905f070cd2ba958f383dd2f0f86e848bd25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 30 Aug 2022 18:02:45 -0300 Subject: [PATCH 73/78] chore: remove reservoirs from configs --- config/graph.arbitrum-one.yml | 15 ++++++--------- config/graph.mainnet.yml | 13 +------------ 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index 9bcbeeed1..1dc59abdb 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -30,9 +30,6 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L2GraphTokenGateway.address}}" - - fn: "setContractProxy" - id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') - contractAddress: "${{L2Reservoir.address}}" ServiceRegistry: proxy: true init: @@ -46,6 +43,9 @@ contracts: proxy: true init: owner: *governor + calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" Curation: proxy: true init: @@ -107,6 +107,9 @@ contracts: proxy: true init: controller: "${{Controller.address}}" + calls: + - fn: "setIssuanceRate" + issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 AllocationExchange: init: graphToken: "${{GraphToken.address}}" @@ -119,9 +122,3 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - L2Reservoir: - proxy: true - init: - controller: "${{Controller.address}}" - calls: - - fn: "approveRewardsManager" diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index f49cb6b92..cf6331fd1 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -33,9 +33,6 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L1GraphTokenGateway.address}}" - - fn: "setContractProxy" - id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') - contractAddress: "${{L1Reservoir.address}}" ServiceRegistry: proxy: true init: @@ -50,7 +47,7 @@ contracts: initialSupply: "10000000000000000000000000000" # in wei calls: - fn: "addMinter" - minter: "${{L1Reservoir.address}}" + minter: "${{RewardsManager.address}}" Curation: proxy: true init: @@ -131,11 +128,3 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - L1Reservoir: - proxy: true - init: - controller: "${{Controller.address}}" - dripInterval: 50400 - calls: - - fn: "approveRewardsManager" - - fn: "initialSnapshot" From c91a482dcda47002c365198d22cb2161cf7cf1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 31 Aug 2022 11:46:01 -0300 Subject: [PATCH 74/78] fix: chainId in loadContracts call in gre --- gre/gre.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gre/gre.ts b/gre/gre.ts index 97520a16a..90520f80f 100644 --- a/gre/gre.ts +++ b/gre/gre.ts @@ -111,7 +111,7 @@ function buildGraphNetworkEnvironment( addressBook: lazyObject(() => getAddressBook(addressBookPath, chainId.toString())), graphConfig: lazyObject(() => readConfig(graphConfigPath, true)), contracts: lazyObject(() => - loadContracts(getAddressBook(addressBookPath, chainId.toString()), provider), + loadContracts(getAddressBook(addressBookPath, chainId.toString()), chainId, provider), ), getDeployer: lazyFunction(() => () => getDeployer(provider)), getNamedAccounts: lazyFunction(() => () => getNamedAccounts(provider, graphConfigPath)), From 5ddfb0b091958f823c6ebfcf733376c2437d179b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 31 Aug 2022 15:54:53 -0300 Subject: [PATCH 75/78] fix: assume this will be deployed after RM storage was at V4 --- contracts/rewards/RewardsManagerStorage.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index 7626992da..cbb500a19 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -29,5 +29,10 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage { contract RewardsManagerV3Storage is RewardsManagerV2Storage { // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated - uint256 public tokenSupplySnapshot; + uint256 public tokenSupplySnapshotDeprecated; +} + +contract RewardsManagerV4Storage is RewardsManagerV3Storage { + // Accumulated rewards at accRewardsPerSignalLastBlockUpdated + uint256 public accRewardsOnLastSignalUpdateDeprecated; } From ffcf1c728673f13326862b12bfad86f13293e511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 31 Aug 2022 15:55:50 -0300 Subject: [PATCH 76/78] feat: linear indexer rewards --- contracts/rewards/RewardsManager.sol | 50 ++++++++-------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 0c68adf49..555b48e9e 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -27,11 +27,10 @@ import "./IRewardsManager.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; - uint256 private constant TOKEN_DECIMALS = 1e18; - uint256 private constant MIN_ISSUANCE_RATE = 1e18; + uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; // -- Events -- @@ -76,11 +75,8 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Sets the issuance rate. - * The issuance rate is defined as a percentage increase of the total supply per block. - * This means that it needs to be greater than 1.0, any number under 1.0 is not - * allowed and an issuance rate of 1.0 means no issuance. - * To accommodate a high precision the issuance rate is expressed in wei. - * @param _issuanceRate Issuance rate expressed in wei + * The issuance rate is defined as a fixed amount of rewards per block in GRT. + * @param _issuanceRate Issuance rate expressed in GRT per block */ function setIssuanceRate(uint256 _issuanceRate) external override onlyGovernor { _setIssuanceRate(_issuanceRate); @@ -91,8 +87,6 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa * @param _issuanceRate Issuance rate */ function _setIssuanceRate(uint256 _issuanceRate) private { - require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); - // Called since `issuance rate` will change updateAccRewardsPerSignal(); @@ -187,18 +181,13 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Gets the issuance of rewards per signal since last updated. * - * Compound interest formula: `a = p(1 + r/n)^nt` - * The formula is simplified with `n = 1` as we apply the interest once every time step. - * The `r` is passed with +1 included. So for 10% instead of 0.1 it is 1.1 - * The simplified formula is `a = p * r^t` + * Linear formula: `x = r * t` * * Notation: * t: time steps are in blocks since last updated - * p: total supply of GRT tokens - * a: inflated amount of total supply for the period `t` when interest `r` is applied - * x: newly accrued rewards token for the period `t` + * x: newly accrued rewards tokens for the period `t` * - * @return newly accrued rewards per signal since last update + * @return newly accrued rewards per signal since last update, scaled by FIXED_POINT_SCALING_FACTOR */ function getNewRewardsPerSignal() public view override returns (uint256) { // Calculate time steps @@ -208,11 +197,6 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return 0; } - // Zero issuance under a rate of 1.0 - if (issuanceRate <= MIN_ISSUANCE_RATE) { - return 0; - } - // Zero issuance if no signalled tokens IGraphToken graphToken = graphToken(); uint256 signalledTokens = graphToken.balanceOf(address(curation())); @@ -220,16 +204,11 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return 0; } - uint256 r = issuanceRate; - uint256 p = tokenSupplySnapshot; - uint256 a = p.mul(_pow(r, t, TOKEN_DECIMALS)).div(TOKEN_DECIMALS); - - // New issuance of tokens during time steps - uint256 x = a.sub(p); + uint256 x = issuanceRate.mul(t); // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number - return x.mul(TOKEN_DECIMALS).div(signalledTokens); + return x.mul(FIXED_POINT_SCALING_FACTOR).div(signalledTokens); } /** @@ -261,7 +240,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa ? getAccRewardsPerSignal() .sub(subgraph.accRewardsPerSignalSnapshot) .mul(subgraphSignalledTokens) - .div(TOKEN_DECIMALS) + .div(FIXED_POINT_SCALING_FACTOR) : 0; return subgraph.accRewardsForSubgraph.add(newRewards); } @@ -295,9 +274,9 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return (0, accRewardsForSubgraph); } - uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph.mul(TOKEN_DECIMALS).div( - subgraphAllocatedTokens - ); + uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph + .mul(FIXED_POINT_SCALING_FACTOR) + .div(subgraphAllocatedTokens); return ( subgraph.accRewardsPerAllocatedToken.add(newRewardsPerAllocatedToken), accRewardsForSubgraph @@ -315,7 +294,6 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; - tokenSupplySnapshot = graphToken().totalSupply(); return accRewardsPerSignal; } @@ -396,7 +374,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa uint256 _endAccRewardsPerAllocatedToken ) private pure returns (uint256) { uint256 newAccrued = _endAccRewardsPerAllocatedToken.sub(_startAccRewardsPerAllocatedToken); - return newAccrued.mul(_tokens).div(TOKEN_DECIMALS); + return newAccrued.mul(_tokens).div(FIXED_POINT_SCALING_FACTOR); } /** From d7fc98cbade9eaad8d237b85008912c3717a6ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 31 Aug 2022 16:10:22 -0300 Subject: [PATCH 77/78] fix: use different issuance var to avoid upgrade risks --- contracts/rewards/RewardsManager.sol | 29 +++++++++++---------- contracts/rewards/RewardsManagerStorage.sol | 7 ++++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 555b48e9e..e91586498 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -27,7 +27,7 @@ import "./IRewardsManager.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; @@ -74,24 +74,25 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa // -- Config -- /** - * @dev Sets the issuance rate. - * The issuance rate is defined as a fixed amount of rewards per block in GRT. - * @param _issuanceRate Issuance rate expressed in GRT per block + * @dev Sets the GRT issuance per block. + * The issuance is defined as a fixed amount of rewards per block in GRT. + * @param _issuancePerBlock Issuance expressed in GRT per block */ - function setIssuanceRate(uint256 _issuanceRate) external override onlyGovernor { - _setIssuanceRate(_issuanceRate); + function setIssuancePerBlock(uint256 _issuancePerBlock) external override onlyGovernor { + _setIssuancePerBlock(_issuancePerBlock); } /** - * @dev Sets the issuance rate. - * @param _issuanceRate Issuance rate + * @dev Sets the GRT issuance per block. + * The issuance is defined as a fixed amount of rewards per block in GRT. + * @param _issuancePerBlock Issuance expressed in GRT per block */ - function _setIssuanceRate(uint256 _issuanceRate) private { - // Called since `issuance rate` will change + function _setIssuancePerBlock(uint256 _issuancePerBlock) private { + // Called since `issuance per block` will change updateAccRewardsPerSignal(); - issuanceRate = _issuanceRate; - emit ParameterUpdated("issuanceRate"); + issuancePerBlock = _issuancePerBlock; + emit ParameterUpdated("issuancePerBlock"); } /** @@ -204,7 +205,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa return 0; } - uint256 x = issuanceRate.mul(t); + uint256 x = issuancePerBlock.mul(t); // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number @@ -287,7 +288,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa /** * @dev Updates the accumulated rewards per signal and save checkpoint block number. - * Must be called before `issuanceRate` or `total signalled GRT` changes + * Must be called before `issuancePerBlock` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() * @return Accumulated rewards per signal */ diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index cbb500a19..d32c04546 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -8,7 +8,7 @@ import "../governance/Managed.sol"; contract RewardsManagerV1Storage is Managed { // -- State -- - uint256 public issuanceRate; + uint256 public issuanceRateDeprecated; uint256 public accRewardsPerSignal; uint256 public accRewardsPerSignalLastBlockUpdated; @@ -36,3 +36,8 @@ contract RewardsManagerV4Storage is RewardsManagerV3Storage { // Accumulated rewards at accRewardsPerSignalLastBlockUpdated uint256 public accRewardsOnLastSignalUpdateDeprecated; } + +contract RewardsManagerV5Storage is RewardsManagerV4Storage { + // GRT issued for indexer rewards per block + uint256 public issuancePerBlock; +} From af4a793c6ffb1d4912fe198073970c6a6b49c56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 31 Aug 2022 17:57:56 -0300 Subject: [PATCH 78/78] fix: some details and get tests for linear rewards --- config/graph.arbitrum-one.yml | 3 - config/graph.mainnet.yml | 4 +- contracts/rewards/IRewardsManager.sol | 2 +- contracts/rewards/RewardsManager.sol | 4 +- test/lib/testHelpers.ts | 86 ------------------------- test/rewards/rewards.test.ts | 91 +++++++++++---------------- 6 files changed, 42 insertions(+), 148 deletions(-) diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index 1dc59abdb..53117372b 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -107,9 +107,6 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 AllocationExchange: init: graphToken: "${{GraphToken.address}}" diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index cf6331fd1..f6c6ed12f 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -110,8 +110,8 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + - fn: "setIssuancePerBlock" + issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 AllocationExchange: init: graphToken: "${{GraphToken.address}}" diff --git a/contracts/rewards/IRewardsManager.sol b/contracts/rewards/IRewardsManager.sol index dc17c8ba8..87d3c2455 100644 --- a/contracts/rewards/IRewardsManager.sol +++ b/contracts/rewards/IRewardsManager.sol @@ -15,7 +15,7 @@ interface IRewardsManager { // -- Config -- - function setIssuanceRate(uint256 _issuanceRate) external; + function setIssuancePerBlock(uint256 _issuancePerBlock) external; function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index e91586498..2abede602 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -76,7 +76,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa /** * @dev Sets the GRT issuance per block. * The issuance is defined as a fixed amount of rewards per block in GRT. - * @param _issuancePerBlock Issuance expressed in GRT per block + * @param _issuancePerBlock Issuance expressed in GRT per block (scaled by 1e18) */ function setIssuancePerBlock(uint256 _issuancePerBlock) external override onlyGovernor { _setIssuancePerBlock(_issuancePerBlock); @@ -85,7 +85,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa /** * @dev Sets the GRT issuance per block. * The issuance is defined as a fixed amount of rewards per block in GRT. - * @param _issuancePerBlock Issuance expressed in GRT per block + * @param _issuancePerBlock Issuance expressed in GRT per block (scaled by 1e18) */ function _setIssuancePerBlock(uint256 _issuancePerBlock) private { // Called since `issuance per block` will change diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index ea4f20264..a9fb857c4 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -139,92 +139,6 @@ const getRewards = (p: BN, r: BN, t: BN): string => { return p.times(r.pow(t)).minus(p).precision(18).toString(10) } -// Tracks the accumulated rewards as supply changes across snapshots -// both at a global level and per signal -export class RewardsTracker { - totalSupply = BigNumber.from(0) - lastUpdatedBlock = BigNumber.from(0) - lastPerSignalUpdatedBlock = BigNumber.from(0) - accumulated = BigNumber.from(0) - accumulatedPerSignal = BigNumber.from(0) - accumulatedAtLastPerSignalUpdatedBlock = BigNumber.from(0) - issuanceRate = BigNumber.from(0) - - static async create( - initialSupply: BigNumber, - issuanceRate: BigNumber, - updatedBlock?: BigNumber, - ): Promise { - const lastUpdatedBlock = updatedBlock || (await latestBlock()) - const tracker = new RewardsTracker(initialSupply, issuanceRate, lastUpdatedBlock) - return tracker - } - - constructor(initialSupply: BigNumber, issuanceRate: BigNumber, updatedBlock: BigNumber) { - this.issuanceRate = issuanceRate - this.totalSupply = initialSupply - this.lastUpdatedBlock = updatedBlock - this.lastPerSignalUpdatedBlock = updatedBlock - } - - async snapshotRewards(initialSupply?: BigNumber, atBlock?: BigNumber): Promise { - const newRewards = await this.newRewards(atBlock) - this.accumulated = this.accumulated.add(newRewards) - this.totalSupply = initialSupply || this.totalSupply.add(newRewards) - this.lastUpdatedBlock = atBlock || (await latestBlock()) - return this.accumulated - } - - async snapshotPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { - this.accumulatedPerSignal = await this.accRewardsPerSignal(totalSignal, atBlock) - this.accumulatedAtLastPerSignalUpdatedBlock = await this.accRewards(atBlock) - this.lastPerSignalUpdatedBlock = atBlock || (await latestBlock()) - return this.accumulatedPerSignal - } - - async elapsedBlocks(): Promise { - const currentBlock = await latestBlock() - return currentBlock.sub(this.lastUpdatedBlock) - } - - async newRewardsPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { - const accRewards = await this.accRewards(atBlock) - const diff = accRewards.sub(this.accumulatedAtLastPerSignalUpdatedBlock) - if (totalSignal.eq(0)) { - return BigNumber.from(0) - } - return diff.mul(toGRT(1)).div(totalSignal) - } - - async accRewardsPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { - return this.accumulatedPerSignal.add(await this.newRewardsPerSignal(totalSignal, atBlock)) - } - - async newRewards(atBlock?: BigNumber): Promise { - if (!atBlock) { - atBlock = await latestBlock() - } - const nBlocks = atBlock.sub(this.lastUpdatedBlock) - return this.accruedByElapsed(nBlocks) - } - - async accRewards(atBlock?: BigNumber): Promise { - if (!atBlock) { - atBlock = await latestBlock() - } - return this.accumulated.add(await this.newRewards(atBlock)) - } - - async accruedByElapsed(nBlocks: BigNumber | number): Promise { - const n = getRewards( - new BN(this.totalSupply.toString()), - new BN(this.issuanceRate.toString()).div(1e18), - new BN(nBlocks.toString()), - ) - return BigNumber.from(n) - } -} - // Adapted from: // https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/test/utils/messaging.ts#L5 export async function getL2SignerFromL1(l1Address: string): Promise { diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 23aee0dfa..28d93a4a5 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -30,7 +30,7 @@ const MAX_PPM = 1000000 const { HashZero, WeiPerEther } = constants -const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] +const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] describe('Rewards', () => { let delegator: Account @@ -62,20 +62,19 @@ describe('Rewards', () => { const metadata = HashZero - const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 5% rewards - const ISSUANCE_RATE_PER_BLOCK = toBN('1012272234429039270') // % increase every block + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 800 GRT rewards + const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT every block // Core formula that gets accumulated rewards per signal for a period of time - const getRewardsPerSignal = (p: BN, r: BN, t: BN, s: BN): string => { + const getRewardsPerSignal = (k: BN, t: BN, s: BN): string => { if (s.eq(0)) { return '0' } - return p.times(r.pow(t)).minus(p).div(s).toPrecision(18).toString() + return k.times(t).div(s).toPrecision(18).toString() } // Tracks the accumulated rewards as totalSignalled or supply changes across snapshots class RewardsTracker { - totalSupply = BigNumber.from(0) totalSignalled = BigNumber.from(0) lastUpdatedBlock = BigNumber.from(0) accumulated = BigNumber.from(0) @@ -88,7 +87,6 @@ describe('Rewards', () => { async snapshot() { this.accumulated = this.accumulated.add(await this.accrued()) - this.totalSupply = await grt.totalSupply() this.totalSignalled = await grt.balanceOf(curation.address) this.lastUpdatedBlock = await latestBlock() return this @@ -106,8 +104,7 @@ describe('Rewards', () => { async accruedByElapsed(nBlocks: BigNumber | number) { const n = getRewardsPerSignal( - new BN(this.totalSupply.toString()), - new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18), + new BN(ISSUANCE_PER_BLOCK.toString()), new BN(nBlocks.toString()), new BN(this.totalSignalled.toString()), ) @@ -148,8 +145,8 @@ describe('Rewards', () => { governor.signer, )) as unknown as RewardsManagerMock - // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + // 200 GRT per block + await rewardsManager.connect(governor.signer).setIssuancePerBlock(ISSUANCE_PER_BLOCK) // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { @@ -168,28 +165,22 @@ describe('Rewards', () => { }) describe('configuration', function () { - describe('issuance rate update', function () { - it('reject set issuance rate if unauthorized', async function () { - const tx = rewardsManager.connect(indexer1.signer).setIssuanceRate(toGRT('1.025')) + describe('issuance per block update', function () { + it('reject set issuance per block if unauthorized', async function () { + const tx = rewardsManager.connect(indexer1.signer).setIssuancePerBlock(toGRT('1.025')) await expect(tx).revertedWith('Caller must be Controller governor') }) - it('reject set issuance rate to less than minimum allowed', async function () { - const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 - const tx = rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - await expect(tx).revertedWith('Issuance rate under minimum allowed') - }) - - it('should set issuance rate to minimum allowed', async function () { - const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) + it('should set issuance rate to minimum allowed (0)', async function () { + const newIssuancePerBlock = toGRT('0') + await rewardsManager.connect(governor.signer).setIssuancePerBlock(newIssuancePerBlock) + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) }) it('should set issuance rate', async function () { - const newIssuanceRate = toGRT('1.025') - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) + const newIssuancePerBlock = toGRT('100.025') + await rewardsManager.connect(governor.signer).setIssuancePerBlock(newIssuancePerBlock) + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await latestBlock()) }) }) @@ -245,7 +236,7 @@ describe('Rewards', () => { context('issuing rewards', async function () { beforeEach(async function () { // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await rewardsManager.connect(governor.signer).setIssuancePerBlock(ISSUANCE_PER_BLOCK) }) describe('getNewRewardsPerSignal', function () { @@ -450,10 +441,8 @@ describe('Rewards', () => { await advanceBlocks(ISSUANCE_RATE_PERIODS) // Prepare expected results - // NOTE: calculated the expected result manually as the above code has 1 off block difference - // replace with a RewardsManagerMock - const expectedSubgraphRewards = toGRT('891695470') - const expectedRewardsAT = toGRT('51571') + const expectedSubgraphRewards = toGRT('1400') // 7 blocks since signaling to when we do getAccRewardsForSubgraph + const expectedRewardsAT = toGRT('0.08') // allocated during 5 blocks: 1000 GRT, divided by 12500 allocated tokens // Update await rewardsManager.onSubgraphAllocationUpdate(subgraphDeploymentID1) @@ -466,7 +455,7 @@ describe('Rewards', () => { const contractRewardsAT = subgraph.accRewardsPerAllocatedToken expect(toRound(expectedSubgraphRewards)).eq(toRound(contractSubgraphRewards)) - expect(toRound(expectedRewardsAT)).eq(toRound(contractRewardsAT)) + expect(toRound(expectedRewardsAT.mul(1000))).eq(toRound(contractRewardsAT.mul(1000))) }) }) @@ -619,13 +608,11 @@ describe('Rewards', () => { const beforeStakingBalance = await grt.balanceOf(staking.address) // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted. + // The final snapshot is when we close the allocation, that happens 9 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 7) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('1400') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking @@ -731,13 +718,11 @@ describe('Rewards', () => { const beforeStakingBalance = await grt.balanceOf(staking.address) // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted. + // The final snapshot is when we close the allocation, that happens 9 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 7) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('1400') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking @@ -805,13 +790,11 @@ describe('Rewards', () => { // Check that rewards are put into delegators pool accordingly // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 14500 = 8466.995 - // The final snapshot is when we close the allocation, that happens 4 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 4 - 10004000000) / 14500 = 34496.55 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. - const expectedIndexingRewards = toGRT('377428566.77') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 1 block after the signal is minted. + // The final snapshot is when we close the allocation, that happens 4 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 3) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('600') // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards)