diff --git a/.github/workflows/deploy-market.yaml b/.github/workflows/deploy-market.yaml index ade8fa3dd..e0f3ffa6e 100644 --- a/.github/workflows/deploy-market.yaml +++ b/.github/workflows/deploy-market.yaml @@ -12,6 +12,8 @@ on: - goerli - mumbai - polygon + - arbitrum + - arbitrum-goerli deployment: description: Deployment Name (e.g. "usdc") required: true @@ -29,11 +31,12 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} steps: - name: Seacrest uses: hayesgm/seacrest@v1 with: - ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"kovan\":\"https://kovan-eth.compound.finance\",\"mainnet\":\"https://mainnet-eth.compound.finance\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"kovan\":\"https://kovan-eth.compound.finance\",\"mainnet\":\"https://mainnet-eth.compound.finance\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' diff --git a/.github/workflows/enact-migration.yaml b/.github/workflows/enact-migration.yaml index d5d417ef1..fbf165c47 100644 --- a/.github/workflows/enact-migration.yaml +++ b/.github/workflows/enact-migration.yaml @@ -12,6 +12,8 @@ on: - goerli - mumbai - polygon + - arbitrum + - arbitrum-goerli deployment: description: Deployment Name (e.g. "usdc") required: true @@ -37,13 +39,14 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} steps: - name: Get governance network run: | case ${{ github.event.inputs.network }} in - polygon) + polygon | arbitrum) echo "GOV_NETWORK=mainnet" >> $GITHUB_ENV ;; - mumbai) + mumbai | arbitrum-goerli) echo "GOV_NETWORK=goerli" >> $GITHUB_ENV ;; *) echo "No governance network for selected network" ;; @@ -52,14 +55,14 @@ jobs: - name: Seacrest uses: hayesgm/seacrest@v1 with: - ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"kovan\":\"https://kovan.infura.io/v3/$INFURA_KEY\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"kovan\":\"https://kovan.infura.io/v3/$INFURA_KEY\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' - name: Seacrest (governance network) uses: hayesgm/seacrest@v1 with: - ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"kovan\":\"https://kovan.infura.io/v3/$INFURA_KEY\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\"}')[env.GOV_NETWORK] }}" + ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"kovan\":\"https://kovan.infura.io/v3/$INFURA_KEY\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\"}')[env.GOV_NETWORK] }}" port: 8685 if: github.event.inputs.eth_pk == '' && env.GOV_NETWORK != '' diff --git a/.github/workflows/prepare-migration.yaml b/.github/workflows/prepare-migration.yaml index fb0553e93..afd69a85f 100644 --- a/.github/workflows/prepare-migration.yaml +++ b/.github/workflows/prepare-migration.yaml @@ -12,6 +12,8 @@ on: - goerli - mumbai - polygon + - arbitrum + - arbitrum-goerli deployment: description: Deployment Name (e.g. "usdc") required: true @@ -32,11 +34,12 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} steps: - name: Seacrest uses: hayesgm/seacrest@v1 with: - ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"kovan\":\"https://kovan.infura.io/v3/$INFURA_KEY\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"kovan\":\"https://kovan.infura.io/v3/$INFURA_KEY\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' diff --git a/.github/workflows/run-contract-linter.yaml b/.github/workflows/run-contract-linter.yaml index 92be2847c..02d80c2d3 100644 --- a/.github/workflows/run-contract-linter.yaml +++ b/.github/workflows/run-contract-linter.yaml @@ -11,6 +11,7 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/run-coverage.yaml b/.github/workflows/run-coverage.yaml index d8aef9d9a..cd523db05 100644 --- a/.github/workflows/run-coverage.yaml +++ b/.github/workflows/run-coverage.yaml @@ -13,6 +13,7 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/.github/workflows/run-eslint.yaml b/.github/workflows/run-eslint.yaml index 902baa583..f48e57c29 100644 --- a/.github/workflows/run-eslint.yaml +++ b/.github/workflows/run-eslint.yaml @@ -11,6 +11,7 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/.github/workflows/run-forge-tests.yaml b/.github/workflows/run-forge-tests.yaml index 6d96d34bf..feb47235a 100644 --- a/.github/workflows/run-forge-tests.yaml +++ b/.github/workflows/run-forge-tests.yaml @@ -27,6 +27,7 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} - name: Build Comet with older solc versions run: | diff --git a/.github/workflows/run-gas-profiler.yaml b/.github/workflows/run-gas-profiler.yaml index 2c6ee4f79..613af7ecd 100644 --- a/.github/workflows/run-gas-profiler.yaml +++ b/.github/workflows/run-gas-profiler.yaml @@ -12,6 +12,7 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/.github/workflows/run-scenarios.yaml b/.github/workflows/run-scenarios.yaml index 429e662b7..23983dca8 100644 --- a/.github/workflows/run-scenarios.yaml +++ b/.github/workflows/run-scenarios.yaml @@ -7,13 +7,14 @@ jobs: strategy: fail-fast: false matrix: - bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, fuji, mumbai, polygon ] + bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, fuji, mumbai, polygon, arbitrum, arbitrum-goerli ] name: Run scenarios env: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }} SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/run-unit-tests.yaml b/.github/workflows/run-unit-tests.yaml index 032562482..ad6e9e839 100644 --- a/.github/workflows/run-unit-tests.yaml +++ b/.github/workflows/run-unit-tests.yaml @@ -11,6 +11,7 @@ jobs: SNOWTRACE_KEY: ${{ secrets.SNOWTRACE_KEY }} INFURA_KEY: ${{ secrets.INFURA_KEY }} POLYGONSCAN_KEY: ${{ secrets.POLYGONSCAN_KEY }} + ARBISCAN_KEY: ${{ secrets.ARBISCAN_KEY }} steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/contracts/CometRewards.sol b/contracts/CometRewards.sol index 611a6bf1f..f18e718ff 100644 --- a/contracts/CometRewards.sol +++ b/contracts/CometRewards.sol @@ -14,6 +14,8 @@ contract CometRewards { address token; uint64 rescaleFactor; bool shouldUpscale; + // Note: We define new variables after existing variables to keep interface backwards-compatible + uint256 multiplier; } struct RewardOwed { @@ -30,14 +32,19 @@ contract CometRewards { /// @notice Rewards claimed per Comet instance and user account mapping(address => mapping(address => uint)) public rewardsClaimed; + /// @dev The scale for factors + uint256 internal constant FACTOR_SCALE = 1e18; + /** Custom events **/ event GovernorTransferred(address indexed oldGovernor, address indexed newGovernor); + event RewardsClaimedSet(address indexed user, address indexed comet, uint256 amount); event RewardClaimed(address indexed src, address indexed recipient, address indexed token, uint256 amount); /** Custom errors **/ error AlreadyConfigured(address); + error BadData(); error InvalidUInt64(uint); error NotPermitted(address); error NotSupported(address); @@ -55,8 +62,9 @@ contract CometRewards { * @notice Set the reward token for a Comet instance * @param comet The protocol instance * @param token The reward token address + * @param multiplier The multiplier for converting a unit of accrued tracking to a unit of the reward token */ - function setRewardConfig(address comet, address token) external { + function setRewardConfigWithMultiplier(address comet, address token, uint256 multiplier) public { if (msg.sender != governor) revert NotPermitted(msg.sender); if (rewardConfig[comet].token != address(0)) revert AlreadyConfigured(comet); @@ -67,17 +75,45 @@ contract CometRewards { rewardConfig[comet] = RewardConfig({ token: token, rescaleFactor: accrualScale / tokenScale, - shouldUpscale: false + shouldUpscale: false, + multiplier: multiplier }); } else { rewardConfig[comet] = RewardConfig({ token: token, rescaleFactor: tokenScale / accrualScale, - shouldUpscale: true + shouldUpscale: true, + multiplier: multiplier }); } } + /** + * @notice Set the reward token for a Comet instance + * @param comet The protocol instance + * @param token The reward token address + */ + function setRewardConfig(address comet, address token) external { + setRewardConfigWithMultiplier(comet, token, FACTOR_SCALE); + } + + /** + * @notice Set the rewards claimed for a list of users + * @param comet The protocol instance to populate the data for + * @param users The list of users to populate the data for + * @param claimedAmounts The list of claimed amounts to populate the data with + */ + function setRewardsClaimed(address comet, address[] calldata users, uint[] calldata claimedAmounts) external { + if (msg.sender != governor) revert NotPermitted(msg.sender); + if (users.length != claimedAmounts.length) revert BadData(); + + for (uint i = 0; i < users.length; ) { + rewardsClaimed[comet][users[i]] = claimedAmounts[i]; + emit RewardsClaimedSet(users[i], comet, claimedAmounts[i]); + unchecked { i++; } + } + } + /** * @notice Withdraw tokens from the contract * @param token The reward token address @@ -176,7 +212,7 @@ contract CometRewards { } else { accrued /= config.rescaleFactor; } - return accrued; + return accrued * config.multiplier / FACTOR_SCALE; } /** diff --git a/contracts/IGovernorBravo.sol b/contracts/IGovernorBravo.sol index 808c050a0..99e0f00f1 100644 --- a/contracts/IGovernorBravo.sol +++ b/contracts/IGovernorBravo.sol @@ -62,8 +62,8 @@ interface IGovernorBravo { bytes[] memory calldatas, string memory description ) external returns (uint256 proposalId); - function queue(uint256 proposalId) external payable; - function execute(uint256 proposalId) external payable; + function queue(uint256 proposalId) external; + function execute(uint256 proposalId) external; function castVote(uint256 proposalId, uint8 support) external returns (uint256 balance); function getActions(uint proposalId) external view returns ( address[] memory targets, diff --git a/contracts/bridges/SweepableBridgeReceiver.sol b/contracts/bridges/SweepableBridgeReceiver.sol new file mode 100644 index 000000000..4d0aceacd --- /dev/null +++ b/contracts/bridges/SweepableBridgeReceiver.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "../IERC20NonStandard.sol"; +import "./BaseBridgeReceiver.sol"; + +contract SweepableBridgeReceiver is BaseBridgeReceiver { + error FailedToSendNativeToken(); + error TransferOutFailed(); + + /** + * @notice A public function to sweep accidental ERC-20 transfers to this contract + * @dev Note: Make sure to check that the asset being swept out is not malicious + * @param recipient The address that will receive the swept funds + * @param asset The address of the ERC-20 token to sweep + */ + function sweepToken(address recipient, address asset) external { + if (msg.sender != localTimelock) revert Unauthorized(); + + uint256 balance = IERC20NonStandard(asset).balanceOf(address(this)); + doTransferOut(asset, recipient, balance); + } + + /** + * @notice A public function to sweep accidental native token transfers to this contract + * @param recipient The address that will receive the swept funds + */ + function sweepNativeToken(address recipient) external { + if (msg.sender != localTimelock) revert Unauthorized(); + + uint256 balance = address(this).balance; + (bool success, ) = recipient.call{ value: balance }(""); + if (!success) revert FailedToSendNativeToken(); + } + + /** + * @notice Similar to ERC-20 transfer, except it properly handles `transfer` from non-standard ERC-20 tokens + * @param asset The ERC-20 token to transfer out + * @param to The recipient of the token transfer + * @param amount The amount of the token to transfer + * @dev Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca + */ + function doTransferOut(address asset, address to, uint amount) internal { + IERC20NonStandard(asset).transfer(to, amount); + + bool success; + assembly { + switch returndatasize() + case 0 { // This is a non-standard ERC-20 + success := not(0) // set success to true + } + case 32 { // This is a compliant ERC-20 + returndatacopy(0, 0, 32) + success := mload(0) // Set `success = returndata` of override external call + } + default { // This is an excessively non-compliant ERC-20, revert. + revert(0, 0) + } + } + if (!success) revert TransferOutFailed(); + } +} \ No newline at end of file diff --git a/contracts/bridges/arbitrum/AddressAliasHelper.sol b/contracts/bridges/arbitrum/AddressAliasHelper.sol new file mode 100644 index 000000000..e09b42d3f --- /dev/null +++ b/contracts/bridges/arbitrum/AddressAliasHelper.sol @@ -0,0 +1,39 @@ +// 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. + */ + +pragma solidity 0.8.15; + +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); + } +} \ No newline at end of file diff --git a/contracts/bridges/arbitrum/ArbitrumBridgeReceiver.sol b/contracts/bridges/arbitrum/ArbitrumBridgeReceiver.sol new file mode 100644 index 000000000..dd21108e5 --- /dev/null +++ b/contracts/bridges/arbitrum/ArbitrumBridgeReceiver.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "../SweepableBridgeReceiver.sol"; +import "./AddressAliasHelper.sol"; + +contract ArbitrumBridgeReceiver is SweepableBridgeReceiver { + fallback() external payable { + processMessage(AddressAliasHelper.undoL1ToL2Alias(msg.sender), msg.data); + } +} \ No newline at end of file diff --git a/contracts/bridges/test/SweepableBridgeReceiverHarness.sol b/contracts/bridges/test/SweepableBridgeReceiverHarness.sol new file mode 100644 index 000000000..0835b74b3 --- /dev/null +++ b/contracts/bridges/test/SweepableBridgeReceiverHarness.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "../SweepableBridgeReceiver.sol"; + +contract SweepableBridgeReceiverHarness is SweepableBridgeReceiver { + function processMessageExternal( + address rootMessageSender, + bytes calldata data + ) external { + processMessage(rootMessageSender, data); + } + + fallback() external payable { } +} diff --git a/deployments/arbitrum-goerli/usdc/configuration.json b/deployments/arbitrum-goerli/usdc/configuration.json new file mode 100644 index 000000000..9281b2445 --- /dev/null +++ b/deployments/arbitrum-goerli/usdc/configuration.json @@ -0,0 +1,56 @@ +{ + "name": "Compound USDC", + "symbol": "cUSDCv3", + "baseToken": "USDC", + "baseTokenAddress": "0x8FB1E3fC51F3b789dED7557E680551d93Ea9d892", + "baseTokenPriceFeed": "0x1692Bdd32F31b831caAc1b0c9fAF68613682813b", + "borrowMin": "100e6", + "pauseGuardian": "0x6C2fD6738e43ce0cc4A6f23a37e3a733516794A0", + "storeFrontPriceFactor": 0.5, + "targetReserves": "5000000e6", + "rates": { + "supplyKink": 0.8, + "supplySlopeLow": 0.0325, + "supplySlopeHigh": 0.4, + "supplyBase": 0, + "borrowKink": 0.8, + "borrowSlopeLow": 0.035, + "borrowSlopeHigh": 0.25, + "borrowBase": 0.015 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "0e15", + "baseBorrowSpeed": "0e15", + "baseMinForRewards": "10000e6" + }, + "assets": { + "LINK": { + "address": "0xbb7303602be1b9149b097aafb094ffce1860e532", + "priceFeed": "0xd28Ba6CA3bB72bF371b80a2a0a33cBcf9073C954", + "decimals": "18", + "borrowCF": 0.775, + "liquidateCF": 0.825, + "liquidationFactor": 0.95, + "supplyCap": "5000000e18" + }, + "WETH": { + "address": "0xe39ab88f8a4777030a534146a9ca3b52bd5d43a3", + "priceFeed": "0x62CAe0FA2da220f43a51F86Db2EDb36DcA9A5A08", + "decimals": "18", + "borrowCF": 0.775, + "liquidateCF": 0.825, + "liquidationFactor": 0.95, + "supplyCap": "5000e18" + }, + "WBTC": { + "address": "0x22d5e2dE578677791f6c90e0110Ec629be9d5Fb5", + "priceFeed": "0x6550bc2301936011c1334555e62A87705A81C12C", + "decimals": "8", + "borrowCF": 0.7, + "liquidateCF": 0.75, + "liquidationFactor": 0.93, + "supplyCap": "300e8" + } + } +} diff --git a/deployments/arbitrum-goerli/usdc/deploy.ts b/deployments/arbitrum-goerli/usdc/deploy.ts new file mode 100644 index 000000000..4d6e0cf67 --- /dev/null +++ b/deployments/arbitrum-goerli/usdc/deploy.ts @@ -0,0 +1,70 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet } from '../../../src/deploy'; + +const SECONDS_PER_DAY = 24 * 60 * 60; + +const GOERLI_TIMELOCK = '0x8Fa336EB4bF58Cfc508dEA1B0aeC7336f55B1399'; + +export default async function deploy(deploymentManager: DeploymentManager, deploySpec: DeploySpec): Promise { + const trace = deploymentManager.tracer() + const ethers = deploymentManager.hre.ethers; + + // pull in existing assets + const USDC = await deploymentManager.existing('USDC', '0x8FB1E3fC51F3b789dED7557E680551d93Ea9d892', 'arbitrum-goerli'); + const LINK = await deploymentManager.existing('LINK', '0xbb7303602be1b9149b097aafb094ffce1860e532', 'arbitrum-goerli'); + const WETH = await deploymentManager.existing('WETH', '0xe39ab88f8a4777030a534146a9ca3b52bd5d43a3', 'arbitrum-goerli'); + const WBTC = await deploymentManager.existing('WBTC', '0x22d5e2dE578677791f6c90e0110Ec629be9d5Fb5', 'arbitrum-goerli'); + + // Deploy ArbitrumBridgeReceiver + const bridgeReceiver = await deploymentManager.deploy( + 'bridgeReceiver', + 'bridges/arbitrum/ArbitrumBridgeReceiver.sol', + [] + ); + + // Deploy Local Timelock + const localTimelock = await deploymentManager.deploy( + 'timelock', + 'vendor/Timelock.sol', + [ + bridgeReceiver.address, // admin + 10 * 60, // delay + 14 * SECONDS_PER_DAY, // grace period + 10 * 60, // minimum delay + 30 * SECONDS_PER_DAY // maximum delay + ] + ); + + // Initialize ArbitrumBridgeReceiver + await deploymentManager.idempotent( + async () => !(await bridgeReceiver.initialized()), + async () => { + trace(`Initializing BridgeReceiver`); + await bridgeReceiver.initialize( + GOERLI_TIMELOCK, // govTimelock + localTimelock.address // localTimelock + ); + trace(`BridgeReceiver initialized`); + } + ); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + const { comet } = deployed; + + // Deploy Bulker + const bulker = await deploymentManager.deploy( + 'bulker', + 'bulkers/BaseBulker.sol', + [ + await comet.governor(), // admin + WETH.address // weth + ] + ); + + return { + ...deployed, + bridgeReceiver, + bulker + }; +} \ No newline at end of file diff --git a/deployments/arbitrum-goerli/usdc/migrations/1679518383_configurate_and_ens.ts b/deployments/arbitrum-goerli/usdc/migrations/1679518383_configurate_and_ens.ts new file mode 100644 index 000000000..7a05a2023 --- /dev/null +++ b/deployments/arbitrum-goerli/usdc/migrations/1679518383_configurate_and_ens.ts @@ -0,0 +1,296 @@ +import { Contract } from 'ethers'; +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { diffState, getCometConfig } from '../../../../plugins/deployment_manager/DiffState'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, getConfigurationStruct, proposal } from '../../../../src/deploy'; +import { applyL1ToL2Alias, estimateL2Transaction, estimateTokenBridge } from '../../../../scenario/utils/arbitrumUtils'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x19c2d5D0f035563344dBB7bE5fD09c8dad62b001'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; + +const arbitrumCOMPAddress = '0xf03370d2aCf26Dde26389B66498B7c293038F5aF'; + +export default migration('1679518383_configurate_and_ens', { + prepare: async (_deploymentManager: DeploymentManager) => { + return {}; + }, + + enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager) => { + const trace = deploymentManager.tracer(); + const ethers = deploymentManager.hre.ethers; + const { utils } = ethers; + + const { + bridgeReceiver, + timelock: l2Timelock, + comet, + cometAdmin, + configurator, + rewards, + } = await deploymentManager.getContracts(); + + const { + arbitrumInbox, + arbitrumL1GatewayRouter, + timelock, + governor, + USDC, + COMP, + } = await govDeploymentManager.getContracts(); + + const USDCAmountToBridge = exp(10, 6); + const COMPAmountToBridge = exp(2_500, 18); + const usdcGatewayAddress = await arbitrumL1GatewayRouter.getGateway(USDC.address); + const compGatewayAddress = await arbitrumL1GatewayRouter.getGateway(COMP.address); + const refundAddress = l2Timelock.address; + + const compGasParams = await estimateTokenBridge( + { + token: COMP.address, + from: timelock.address, + to: rewards.address, + amount: COMPAmountToBridge + }, + govDeploymentManager, + deploymentManager + ); + + const usdcGasParams = await estimateTokenBridge( + { + token: USDC.address, + from: timelock.address, + to: comet.address, + amount: USDCAmountToBridge + }, + govDeploymentManager, + deploymentManager + ); + + const configuration = await getConfigurationStruct(deploymentManager); + + const setConfigurationCalldata = await calldata( + configurator.populateTransaction.setConfiguration(comet.address, configuration) + ); + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + const setRewardConfigCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [comet.address, arbitrumCOMPAddress] + ); + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, cometAdmin.address, rewards.address], + [0, 0, 0], + [ + 'setConfiguration(address,(address,address,address,address,address,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint104,uint104,uint104,(address,address,uint8,uint64,uint64,uint64,uint128)[]))', + 'deployAndUpgradeTo(address,address)', + 'setRewardConfig(address,address)' + ], + [setConfigurationCalldata, deployAndUpgradeToCalldata, setRewardConfigCalldata] + ] + ); + + const createRetryableTicketGasParams = await estimateL2Transaction( + { + from: applyL1ToL2Alias(timelock.address), + to: bridgeReceiver.address, + data: l2ProposalData + }, + deploymentManager + ); + + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress, 'goerli'); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + const officialMarkets = JSON.parse(officialMarketsJSON); + // XXX + const updatedMarkets = { + ...officialMarkets, + 421613: [ + { + baseSymbol: 'USDC', + cometAddress: comet.address, + } + ], + }; + + const mainnetActions = [ + // 1. Set Comet configuration and deployAndUpgradeTo new Comet on Arbitrum. + { + contract: arbitrumInbox, + signature: 'createRetryableTicket(address,uint256,uint256,address,address,uint256,uint256,bytes)', + args: [ + bridgeReceiver.address, // address to, + 0, // uint256 l2CallValue, + createRetryableTicketGasParams.maxSubmissionCost, // uint256 maxSubmissionCost, + refundAddress, // address excessFeeRefundAddress, + refundAddress, // address callValueRefundAddress, + createRetryableTicketGasParams.gasLimit, // uint256 gasLimit, + createRetryableTicketGasParams.maxFeePerGas, // uint256 maxFeePerGas, + l2ProposalData, // bytes calldata data + ], + value: createRetryableTicketGasParams.deposit + }, + // 2. Approve the USDC gateway to take Timelock's USDC for bridging + { + contract: USDC, + signature: 'approve(address,uint256)', + args: [usdcGatewayAddress, USDCAmountToBridge] + }, + // 3. Bridge USDC from mainnet to Arbitrum Comet + { + contract: arbitrumL1GatewayRouter, + signature: 'outboundTransferCustomRefund(address,address,address,uint256,uint256,uint256,bytes)', + args: [ + USDC.address, // address _token, + refundAddress, // address _refundTo + comet.address, // address _to, + USDCAmountToBridge, // uint256 _amount, + usdcGasParams.gasLimit, // uint256 _maxGas, + usdcGasParams.maxFeePerGas, // uint256 _gasPriceBid, + utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [usdcGasParams.maxSubmissionCost, '0x'] + ) // bytes calldata _data + ], + value: usdcGasParams.deposit + }, + // 4. Approve the COMP gateway to take Timelock's COMP for bridging + { + contract: COMP, + signature: 'approve(address,uint256)', + args: [compGatewayAddress, COMPAmountToBridge] + }, + // 5. Bridge COMP from mainnet to Arbitrum rewards + { + contract: arbitrumL1GatewayRouter, + signature: 'outboundTransferCustomRefund(address,address,address,uint256,uint256,uint256,bytes)', + args: [ + COMP.address, // address _token, + refundAddress, // address _refundTo, + rewards.address, // address _to, + COMPAmountToBridge, // uint256 _amount, + compGasParams.gasLimit, // uint256 _maxGas, + compGasParams.maxFeePerGas, // uint256 _gasPriceBid, + utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [compGasParams.maxSubmissionCost, '0x'] + ) // bytes calldata _data + ], + value: compGasParams.deposit + }, + // 6. Update the list of official markets + { + target: ENSResolverAddress, + signature: 'setText(bytes32,string,string)', + calldata: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'string', 'string'], + [subdomainHash, ENSTextRecordKey, JSON.stringify(updatedMarkets)] + ) + }, + ]; + + const description = 'XXX'; // XXX add description + const txn = await govDeploymentManager.retry(async () => + trace(await governor.propose(...(await proposal(mainnetActions, description)))) + ); + + const event = txn.events.find(event => event.event === 'ProposalCreated'); + const [proposalId] = event.args; + + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify(deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, preMigrationBlockNumber: number) { + const ethers = deploymentManager.hre.ethers; + await deploymentManager.spider(); // await deploymentManager.spider(); // Pull in Arbitrum COMP now that reward config has been set + + const { + comet, + rewards, + WBTC, + WETH, + LINK + } = await deploymentManager.getContracts(); + + // 1. + const stateChanges = await diffState(comet, getCometConfig, preMigrationBlockNumber); + expect(stateChanges).to.deep.equal({ + LINK: { + supplyCap: exp(5_000_000, 18) + }, + WBTC: { + supplyCap: exp(300, 8) + }, + WETH: { + supplyCap: exp(5_000, 18) + } + }); + + const config = await rewards.rewardConfig(comet.address); + expect(config.token).to.be.equal(arbitrumCOMPAddress); + expect(config.rescaleFactor).to.be.equal(exp(1, 12)); + expect(config.shouldUpscale).to.be.equal(true); + + // 2. & 3. + expect(await comet.getReserves()).to.be.equal(exp(10, 6)); + + // 4. & 5. + const arbitrumCOMP = new Contract( + arbitrumCOMPAddress, + ['function balanceOf(address account) external view returns (uint256)'], + deploymentManager.hre.ethers.provider + ); + expect(await arbitrumCOMP.balanceOf(rewards.address)).to.be.equal(exp(2_500, 18)); + + // 6. + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + const officialMarkets = JSON.parse(officialMarketsJSON); + expect(officialMarkets).to.deep.equal({ + 5: [ + { + baseSymbol: 'USDC', + cometAddress: '0x3EE77595A8459e93C2888b13aDB354017B198188', + }, + { + baseSymbol: 'WETH', + cometAddress: '0x9A539EEc489AAA03D588212a164d0abdB5F08F5F', + }, + ], + + 420: [ + { + baseSymbol: 'USDC', + cometAddress: '0xb8F2f9C84ceD7bBCcc1Db6FB7bb1F19A9a4adfF4' + } + ], + + 421613: [ + { + baseSymbol: 'USDC', + cometAddress: comet.address + }, + ], + + 80001: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF09F0369aB0a875254fB565E52226c88f10Bc839' + }, + ] + }); + } +}); diff --git a/deployments/arbitrum-goerli/usdc/relations.ts b/deployments/arbitrum-goerli/usdc/relations.ts new file mode 100644 index 000000000..26e0d86c9 --- /dev/null +++ b/deployments/arbitrum-goerli/usdc/relations.ts @@ -0,0 +1,20 @@ +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + governor: { + artifact: 'contracts/bridges/arbitrum/ArbitrumBridgeReceiver.sol:ArbitrumBridgeReceiver' + }, + ClonableBeaconProxy: { + artifact: 'contracts/ERC20.sol:ERC20' + }, + // WETH + '0xe39ab88f8a4777030a534146a9ca3b52bd5d43a3': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + } +}; diff --git a/deployments/arbitrum-goerli/usdc/roots.json b/deployments/arbitrum-goerli/usdc/roots.json new file mode 100644 index 000000000..1ef25d7d2 --- /dev/null +++ b/deployments/arbitrum-goerli/usdc/roots.json @@ -0,0 +1,7 @@ +{ + "comet": "0x1d573274E19174260c5aCE3f2251598959d24456", + "configurator": "0x1Ead344570F0f0a0cD86d95d8adDC7855C8723Fb", + "rewards": "0x8DA65F8E3Aa22A498211fc4204C498ae9050DAE4", + "bridgeReceiver": "0xAC9fC1a9532BC92a9f33eD4c6Ce4A7a54930F376", + "bulker": "0x987350Af5a17b6DdafeB95E6e329c178f44841d7" +} \ No newline at end of file diff --git a/deployments/arbitrum/usdc/configuration.json b/deployments/arbitrum/usdc/configuration.json new file mode 100644 index 000000000..e7d68ae12 --- /dev/null +++ b/deployments/arbitrum/usdc/configuration.json @@ -0,0 +1,65 @@ +{ + "name": "Compound USDC", + "symbol": "cUSDCv3", + "baseToken": "USDC", + "baseTokenAddress": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8", + "baseTokenPriceFeed": "0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3", + "borrowMin": "100e6", + "pauseGuardian": "0x78E6317DD6D43DdbDa00Dce32C2CbaFc99361a9d", + "storeFrontPriceFactor": 0.8, + "targetReserves": "5000000e6", + "rates": { + "supplyKink": 0.8, + "supplySlopeLow": 0.0325, + "supplySlopeHigh": 0.4, + "supplyBase": 0, + "borrowKink": 0.8, + "borrowSlopeLow": 0.035, + "borrowSlopeHigh": 0.25, + "borrowBase": 0.015 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "0.000402083333333e15", + "baseBorrowSpeed": "0e15", + "baseMinForRewards": "10000e6" + }, + "assets": { + "ARB": { + "address": "0x912ce59144191c1204e64559fe8253a0e49e6548", + "priceFeed": "0xb2A824043730FE05F3DA2efaFa1CBbe83fa548D6", + "decimals": "18", + "borrowCF": 0.55, + "liquidateCF": 0.60, + "liquidationFactor": 0.93, + "supplyCap": "4000000e18" + }, + "GMX": { + "address": "0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a", + "priceFeed": "0xDB98056FecFff59D032aB628337A4887110df3dB", + "decimals": "18", + "borrowCF": 0.40, + "liquidateCF": 0.45, + "liquidationFactor": 0.93, + "supplyCap": "50000e18" + }, + "WETH": { + "address": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "priceFeed": "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612", + "decimals": "18", + "borrowCF": 0.78, + "liquidateCF": 0.85, + "liquidationFactor": 0.95, + "supplyCap": "5000e18" + }, + "WBTC": { + "address": "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", + "priceFeed": "0xd0C7101eACbB49F3deCcCc166d238410D6D46d57", + "decimals": "8", + "borrowCF": 0.70, + "liquidateCF": 0.77, + "liquidationFactor": 0.95, + "supplyCap": "300e8" + } + } +} \ No newline at end of file diff --git a/deployments/arbitrum/usdc/deploy.ts b/deployments/arbitrum/usdc/deploy.ts new file mode 100644 index 000000000..e2456c03c --- /dev/null +++ b/deployments/arbitrum/usdc/deploy.ts @@ -0,0 +1,72 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet } from '../../../src/deploy'; + +const HOUR = 60 * 60; +const DAY = 24 * HOUR; + +const MAINNET_TIMELOCK = '0x6d903f6003cca6255d85cca4d3b5e5146dc33925'; + +export default async function deploy(deploymentManager: DeploymentManager, deploySpec: DeploySpec): Promise { + const trace = deploymentManager.tracer() + const ethers = deploymentManager.hre.ethers; + + // pull in existing assets + const USDC = await deploymentManager.existing('USDC', '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', 'arbitrum'); + const ARB = await deploymentManager.existing('ARB', '0x912ce59144191c1204e64559fe8253a0e49e6548', 'arbitrum'); + const GMX = await deploymentManager.existing('GMX', '0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a', 'arbitrum'); + const WETH = await deploymentManager.existing('WETH', '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', 'arbitrum'); + const WBTC = await deploymentManager.existing('WBTC', '0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f', 'arbitrum'); + + // Deploy ArbitrumBridgeReceiver + const bridgeReceiver = await deploymentManager.deploy( + 'bridgeReceiver', + 'bridges/arbitrum/ArbitrumBridgeReceiver.sol', + [] + ); + + // Deploy Local Timelock + const localTimelock = await deploymentManager.deploy( + 'timelock', + 'vendor/Timelock.sol', + [ + bridgeReceiver.address, // admin + 1 * DAY, // delay + 14 * DAY, // grace period + 12 * HOUR, // minimum delay + 30 * DAY // maxiumum delay + ] + ); + + // Initialize ArbitrumBridgeReceiver + await deploymentManager.idempotent( + async () => !(await bridgeReceiver.initialized()), + async () => { + trace(`Initializing BridgeReceiver`); + await bridgeReceiver.initialize( + MAINNET_TIMELOCK, // govTimelock + localTimelock.address // localTimelock + ); + trace(`BridgeReceiver initialized`); + } + ); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + const { comet } = deployed; + + // Deploy Bulker + const bulker = await deploymentManager.deploy( + 'bulker', + 'bulkers/BaseBulker.sol', + [ + await comet.governor(), // admin + WETH.address // weth + ] + ); + + return { + ...deployed, + bridgeReceiver, + bulker + }; +} \ No newline at end of file diff --git a/deployments/arbitrum/usdc/migrations/1679020486_configurate_and_ens.ts b/deployments/arbitrum/usdc/migrations/1679020486_configurate_and_ens.ts new file mode 100644 index 000000000..170469181 --- /dev/null +++ b/deployments/arbitrum/usdc/migrations/1679020486_configurate_and_ens.ts @@ -0,0 +1,312 @@ +import { Contract } from 'ethers'; +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { diffState, getCometConfig } from '../../../../plugins/deployment_manager/DiffState'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, getConfigurationStruct, proposal } from '../../../../src/deploy'; +import { applyL1ToL2Alias, estimateL2Transaction, estimateTokenBridge } from '../../../../scenario/utils/arbitrumUtils'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; + +const arbitrumCOMPAddress = '0x354A6dA3fcde098F8389cad84b0182725c6C91dE'; + +const cUSDTAddress = '0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9'; + +export default migration('1679020486_configurate_and_ens', { + prepare: async (_deploymentManager: DeploymentManager) => { + return {}; + }, + + enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager) => { + const trace = deploymentManager.tracer(); + const ethers = deploymentManager.hre.ethers; + const { utils } = ethers; + + const { + bridgeReceiver, + timelock: l2Timelock, + comet, + cometAdmin, + configurator, + rewards, + } = await deploymentManager.getContracts(); + + const { + arbitrumInbox, + arbitrumL1GatewayRouter, + timelock, + comptrollerV2, + governor, + USDC, + COMP, + } = await govDeploymentManager.getContracts(); + + const USDCAmountToBridge = exp(10_000, 6); + const COMPAmountToBridge = exp(12_500, 18); + const usdcGatewayAddress = await arbitrumL1GatewayRouter.getGateway(USDC.address); + const compGatewayAddress = await arbitrumL1GatewayRouter.getGateway(COMP.address); + const refundAddress = l2Timelock.address; + + const compGasParams = await estimateTokenBridge( + { + token: COMP.address, + from: timelock.address, + to: rewards.address, + amount: COMPAmountToBridge + }, + govDeploymentManager, + deploymentManager + ); + + const usdcGasParams = await estimateTokenBridge( + { + token: USDC.address, + from: timelock.address, + to: comet.address, + amount: USDCAmountToBridge + }, + govDeploymentManager, + deploymentManager + ); + + const configuration = await getConfigurationStruct(deploymentManager); + + const setConfigurationCalldata = await calldata( + configurator.populateTransaction.setConfiguration(comet.address, configuration) + ); + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + const setRewardConfigCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [comet.address, arbitrumCOMPAddress] + ); + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, cometAdmin.address, rewards.address], + [0, 0, 0], + [ + 'setConfiguration(address,(address,address,address,address,address,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint104,uint104,uint104,(address,address,uint8,uint64,uint64,uint64,uint128)[]))', + 'deployAndUpgradeTo(address,address)', + 'setRewardConfig(address,address)' + ], + [setConfigurationCalldata, deployAndUpgradeToCalldata, setRewardConfigCalldata] + ] + ); + + const createRetryableTicketGasParams = await estimateL2Transaction( + { + from: applyL1ToL2Alias(timelock.address), + to: bridgeReceiver.address, + data: l2ProposalData + }, + deploymentManager + ); + + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + const officialMarkets = JSON.parse(officialMarketsJSON); + const updatedMarkets = { + ...officialMarkets, + 42161: [ + { + baseSymbol: 'USDC', + cometAddress: comet.address, + } + ], + }; + + const mainnetActions = [ + // 1. Set Comet configuration and deployAndUpgradeTo new Comet on Arbitrum. + { + contract: arbitrumInbox, + signature: 'createRetryableTicket(address,uint256,uint256,address,address,uint256,uint256,bytes)', + args: [ + bridgeReceiver.address, // address to, + 0, // uint256 l2CallValue, + createRetryableTicketGasParams.maxSubmissionCost, // uint256 maxSubmissionCost, + refundAddress, // address excessFeeRefundAddress, + refundAddress, // address callValueRefundAddress, + createRetryableTicketGasParams.gasLimit, // uint256 gasLimit, + createRetryableTicketGasParams.maxFeePerGas, // uint256 maxFeePerGas, + l2ProposalData, // bytes calldata data + ], + value: createRetryableTicketGasParams.deposit + }, + // 2. Approve the USDC gateway to take Timelock's USDC for bridging + { + contract: USDC, + signature: 'approve(address,uint256)', + args: [usdcGatewayAddress, USDCAmountToBridge] + }, + // 3. Bridge USDC from mainnet to Arbitrum Comet + { + contract: arbitrumL1GatewayRouter, + signature: 'outboundTransferCustomRefund(address,address,address,uint256,uint256,uint256,bytes)', + args: [ + USDC.address, // address _token, + refundAddress, // address _refundTo + comet.address, // address _to, + USDCAmountToBridge, // uint256 _amount, + usdcGasParams.gasLimit, // uint256 _maxGas, + usdcGasParams.maxFeePerGas, // uint256 _gasPriceBid, + utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [usdcGasParams.maxSubmissionCost, '0x'] + ) // bytes calldata _data + ], + value: usdcGasParams.deposit + }, + // 4. Approve the COMP gateway to take Timelock's COMP for bridging + { + contract: COMP, + signature: 'approve(address,uint256)', + args: [compGatewayAddress, COMPAmountToBridge] + }, + // 5. Bridge COMP from mainnet to Arbitrum rewards + { + contract: arbitrumL1GatewayRouter, + signature: 'outboundTransferCustomRefund(address,address,address,uint256,uint256,uint256,bytes)', + args: [ + COMP.address, // address _token, + refundAddress, // address _refundTo, + rewards.address, // address _to, + COMPAmountToBridge, // uint256 _amount, + compGasParams.gasLimit, // uint256 _maxGas, + compGasParams.maxFeePerGas, // uint256 _gasPriceBid, + utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [compGasParams.maxSubmissionCost, '0x'] + ) // bytes calldata _data + ], + value: compGasParams.deposit + }, + // 6. Update the list of official markets + { + target: ENSResolverAddress, + signature: 'setText(bytes32,string,string)', + calldata: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'string', 'string'], + [subdomainHash, ENSTextRecordKey, JSON.stringify(updatedMarkets)] + ) + }, + // 7. Displace v2 USDT COMP rewards + { + contract: comptrollerV2, + signature: '_setCompSpeeds(address[],uint256[],uint256[])', + args: [ + [cUSDTAddress], + [0], + [0], + ], + }, + ]; + + const description = "# Initialize cUSDCv3 on Arbitrum\n\nThis proposal takes the governance steps recommended and necessary to initialize a Compound III USDC market on Arbitrum; upon execution, cUSDCv3 will be ready for use. Simulations have confirmed the market's readiness, as much as possible, using the [Comet scenario suite](https://github.com/compound-finance/comet/tree/main/scenario). Although real tests have also been run over the Goerli/Arbitrum Goerli bridge, this proposal requires estimating gas costs in advance of executing the bridge proposal, and therefore includes risks not present in previous proposals.\n\nAlthough the proposal sets the entire configuration in the Configurator, the initial deployment already has most of these same parameters already set. The new parameters are limited to increasing the supply caps of the collateral assets from their initial values of 0. The risk parameters and supply caps are based off of [recommendations from Gauntlet](https://www.comp.xyz/t/deploy-compound-v3-on-arbitrum/4100/15).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/719) and [forum discussion](https://www.comp.xyz/t/deploy-compound-v3-on-arbitrum/4100).\n\n\n## Proposal Actions\n\nThe first proposal action sets the Comet configuration and deploys a new Comet implementation on Arbitrum. This sends the encoded `setConfiguration` and `deployAndUpgradeTo` calls across the bridge to the governance receiver on Arbitrum. It also calls `setRewardConfig` on the Arbitrum rewards contract, to establish Arbitrum's bridged version of COMP as the reward token for the deployment and set the initial supply speed to be ~34.74 COMP/day.\n\nThe second action approves Arbitrum's [L1 Arb-Custom Gateway](https://etherscan.io/address/0xcEe284F754E854890e311e3280b767F80797180d) to take Timelock's USDC, in order to seed the market reserves through the bridge.\n\nThe third action bridges USDC from mainnet to the Compound instance on Arbitrum, via Arbitrum's [L1GatewayRouter contract](https://etherscan.io/address/0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef).\n\nThe fourth action approves Arbitrum's [L1 ERC20 Gateway](https://etherscan.io/address/0xa3A7B6F88361F48403514059F1F16C8E78d60EeC) to take Timelock's COMP, in order to seed the rewards contract through the bridge.\n\nThe fifth action transfers COMP from mainnet to the rewards contract on Arbitrum, via Arbitrum's [L1GatewayRouter contract](https://etherscan.io/address/0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef).\n\nThe sixth action updates the ENS TXT record `v3-official-markets` on `v3-additional-grants.compound-community-licenses.eth`, updating the official markets JSON to include the new Arbitrum cUSDCv3 market.\n\nThe seventh action turns off COMP distributions on Compound v2 USDT borrows (~34.74 COMP/day) as they are being shifted to Arbitrum."; + const txn = await govDeploymentManager.retry(async () => + trace(await governor.propose(...(await proposal(mainnetActions, description)))) + ); + + const event = txn.events.find(event => event.event === 'ProposalCreated'); + const [proposalId] = event.args; + + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify(deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, preMigrationBlockNumber: number) { + const ethers = deploymentManager.hre.ethers; + await deploymentManager.spider(); // Pull in Arbitrum COMP now that reward config has been set + + const { + comet, + rewards + } = await deploymentManager.getContracts(); + + const { + comptrollerV2 + } = await govDeploymentManager.getContracts(); + + // 1. + const stateChanges = await diffState(comet, getCometConfig, preMigrationBlockNumber); + expect(stateChanges).to.deep.equal({ + ARB: { + supplyCap: exp(4_000_000, 18) + }, + GMX: { + supplyCap: exp(50_000, 18) + }, + WETH: { + supplyCap: exp(5_000, 18) + }, + WBTC: { + supplyCap: exp(300, 8) + }, + baseTrackingSupplySpeed: exp(34.74 / 86400, 15, 18) + }); + + const config = await rewards.rewardConfig(comet.address); + expect(config.token).to.be.equal(arbitrumCOMPAddress); + expect(config.rescaleFactor).to.be.equal(exp(1, 12)); + expect(config.shouldUpscale).to.be.equal(true); + + // 2. & 3. + expect(await comet.getReserves()).to.be.equal(exp(10_000, 6)); + + // 4. & 5. + const arbitrumCOMP = new Contract( + arbitrumCOMPAddress, + ['function balanceOf(address account) external view returns (uint256)'], + deploymentManager.hre.ethers.provider + ); + expect(await arbitrumCOMP.balanceOf(rewards.address)).to.be.equal(exp(12_500, 18)); + + // 6. + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + const officialMarkets = JSON.parse(officialMarketsJSON); + expect(officialMarkets).to.deep.equal({ + 1: [ + { + baseSymbol: 'USDC', + cometAddress: '0xc3d688B66703497DAA19211EEdff47f25384cdc3', + }, + { + baseSymbol: 'WETH', + cometAddress: '0xA17581A9E3356d9A858b789D68B4d866e593aE94', + }, + ], + + 137: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF25212E676D1F7F89Cd72fFEe66158f541246445', + }, + ], + + 42161: [ + { + baseSymbol: 'USDC', + cometAddress: comet.address, + } + ], + }); + + // 7. + expect(await comptrollerV2.compBorrowSpeeds(cUSDTAddress)).to.be.equal(0); + expect(await comptrollerV2.compSupplySpeeds(cUSDTAddress)).to.be.equal(0); + expect(await comet.baseTrackingSupplySpeed()).to.be.equal(exp(34.74 / 86400, 15, 18) ); + expect(await comet.baseTrackingBorrowSpeed()).to.be.equal(0); + } +}); diff --git a/deployments/arbitrum/usdc/relations.ts b/deployments/arbitrum/usdc/relations.ts new file mode 100644 index 000000000..f8265b45f --- /dev/null +++ b/deployments/arbitrum/usdc/relations.ts @@ -0,0 +1,38 @@ +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + governor: { + artifact: 'contracts/bridges/arbitrum/ArbitrumBridgeReceiver.sol:ArbitrumBridgeReceiver' + }, + ClonableBeaconProxy: { + artifact: 'contracts/ERC20.sol:ERC20' + }, + // USDC + '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + // ARB + '0x912ce59144191c1204e64559fe8253a0e49e6548': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + // WETH + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + } +}; diff --git a/deployments/arbitrum/usdc/roots.json b/deployments/arbitrum/usdc/roots.json new file mode 100644 index 000000000..dce14a07b --- /dev/null +++ b/deployments/arbitrum/usdc/roots.json @@ -0,0 +1,7 @@ +{ + "comet": "0xA5EDBDD9646f8dFF606d7448e414884C7d905dCA", + "configurator": "0xb21b06D71c75973babdE35b49fFDAc3F82Ad3775", + "rewards": "0x88730d254A2f7e6AC8388c3198aFd694bA9f7fae", + "bridgeReceiver": "0x42480C37B249e33aABaf4c22B20235656bd38068", + "bulker": "0xbdE8F31D2DdDA895264e27DD990faB3DC87b372d" +} \ No newline at end of file diff --git a/deployments/goerli/usdc/relations.ts b/deployments/goerli/usdc/relations.ts index 269ffb0f3..ef6586cab 100644 --- a/deployments/goerli/usdc/relations.ts +++ b/deployments/goerli/usdc/relations.ts @@ -3,11 +3,30 @@ import baseRelationConfig from '../../relations'; export default { ...baseRelationConfig, - 'fxRoot': { + fxRoot: { relations: { stateSender: { field: async (fxRoot) => fxRoot.stateSender() } } - } + }, + arbitrumInbox: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + }, + }, + relations: { + arbitrumBridge: { + field: async (inbox) => inbox.bridge() + } + } + }, + arbitrumL1GatewayRouter: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + } + }, + }, }; \ No newline at end of file diff --git a/deployments/goerli/usdc/roots.json b/deployments/goerli/usdc/roots.json index af96e572f..18221c905 100644 --- a/deployments/goerli/usdc/roots.json +++ b/deployments/goerli/usdc/roots.json @@ -5,5 +5,7 @@ "configurator": "0xB28495db3eC65A0e3558F040BC4f98A0d588Ae60", "rewards": "0xef9e070044d62C38D2e316146dDe92AD02CF2c2c", "bulker": "0x69dD076105977c55dC2835951d287f82D54606b4", - "fxRoot": "0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA" + "fxRoot": "0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA", + "arbitrumInbox": "0x6BEbC4925716945D46F0Ec336D5C2564F419682C", + "arbitrumL1GatewayRouter": "0x4c7708168395aEa569453Fc36862D2ffcDaC588c" } \ No newline at end of file diff --git a/deployments/mainnet/usdc/relations.ts b/deployments/mainnet/usdc/relations.ts index 269ffb0f3..ef6586cab 100644 --- a/deployments/mainnet/usdc/relations.ts +++ b/deployments/mainnet/usdc/relations.ts @@ -3,11 +3,30 @@ import baseRelationConfig from '../../relations'; export default { ...baseRelationConfig, - 'fxRoot': { + fxRoot: { relations: { stateSender: { field: async (fxRoot) => fxRoot.stateSender() } } - } + }, + arbitrumInbox: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + }, + }, + relations: { + arbitrumBridge: { + field: async (inbox) => inbox.bridge() + } + } + }, + arbitrumL1GatewayRouter: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + } + }, + }, }; \ No newline at end of file diff --git a/deployments/mainnet/usdc/roots.json b/deployments/mainnet/usdc/roots.json index 0f0c51086..87b6e5d00 100644 --- a/deployments/mainnet/usdc/roots.json +++ b/deployments/mainnet/usdc/roots.json @@ -4,5 +4,7 @@ "configurator": "0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3", "rewards": "0x1B0e765F6224C21223AeA2af16c1C46E38885a40", "bulker": "0xa397a8C2086C554B531c02E29f3291c9704B00c7", - "fxRoot": "0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2" + "fxRoot": "0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2", + "arbitrumInbox": "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f", + "arbitrumL1GatewayRouter": "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef" } \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index c96d215aa..c9b917a0a 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -24,6 +24,8 @@ import mumbaiRelationConfigMap from './deployments/mumbai/usdc/relations'; import mainnetRelationConfigMap from './deployments/mainnet/usdc/relations'; import mainnetWethRelationConfigMap from './deployments/mainnet/weth/relations'; import polygonRelationConfigMap from './deployments/polygon/usdc/relations'; +import arbitrumRelationConfigMap from './deployments/arbitrum/usdc/relations'; +import arbitrumGoerliRelationConfigMap from './deployments/arbitrum-goerli/usdc/relations'; task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => { for (const account of await hre.ethers.getSigners()) console.log(account.address); @@ -35,9 +37,10 @@ const { ETH_PK = '', ETHERSCAN_KEY, SNOWTRACE_KEY, + POLYGONSCAN_KEY, + ARBISCAN_KEY, INFURA_KEY, MNEMONIC = 'myth like bonus scare over problem client lizard pioneer submit female collect', - POLYGONSCAN_KEY, REPORT_GAS = 'false', NETWORK_PROVIDER = '', GOV_NETWORK_PROVIDER = '', @@ -63,8 +66,9 @@ export function requireEnv(varName, msg?: string): string { 'ETHERSCAN_KEY', 'SNOWTRACE_KEY', 'INFURA_KEY', - 'POLYGONSCAN_KEY' -].map(v => requireEnv(v)) + 'POLYGONSCAN_KEY', + 'ARBISCAN_KEY' +].map(v => requireEnv(v)); // Networks interface NetworkConfig { @@ -86,6 +90,11 @@ const networkConfigs: NetworkConfig[] = [ chainId: 137, url: `https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`, }, + { + network: 'arbitrum', + chainId: 42161, + url: `https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`, + }, { network: 'avalanche', chainId: 43114, @@ -101,6 +110,11 @@ const networkConfigs: NetworkConfig[] = [ chainId: 80001, url: `https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`, }, + { + network: 'arbitrum-goerli', + chainId: 421613, + url: `https://arbitrum-goerli.infura.io/v3/${INFURA_KEY}`, + } ]; function getDefaultProviderURL(network: string) { @@ -118,7 +132,7 @@ function setupDefaultNetworkProviders(hardhatConfig: HardhatUserConfig) { getDefaultProviderURL(netConfig.network), gas: netConfig.gas || 'auto', gasPrice: netConfig.gasPrice || 'auto', - accounts: REMOTE_ACCOUNTS ? "remote" : ( ETH_PK ? [...deriveAccounts(ETH_PK)] : { mnemonic: MNEMONIC } ), + accounts: REMOTE_ACCOUNTS ? 'remote' : ( ETH_PK ? [...deriveAccounts(ETH_PK)] : { mnemonic: MNEMONIC } ), }; } } @@ -142,8 +156,8 @@ const config: HardhatUserConfig = { } ), outputSelection: { - "*": { - "*": ["evm.deployedBytecode.sourceMap"] + '*': { + '*': ['evm.deployedBytecode.sourceMap'] }, }, viaIR: process.env['OPTIMIZER_DISABLED'] ? false : true, @@ -162,6 +176,7 @@ const config: HardhatUserConfig = { : { mnemonic: MNEMONIC, accountsBalance: (10n ** 36n).toString() }, // this should only be relied upon for test harnesses and coverage (which does not use viaIR flag) allowUnlimitedContractSize: true, + hardfork: "shanghai" }, }, @@ -180,7 +195,32 @@ const config: HardhatUserConfig = { // Polygon polygon: POLYGONSCAN_KEY, polygonMumbai: POLYGONSCAN_KEY, + // Arbitrum + arbitrumOne: ARBISCAN_KEY, + arbitrumTestnet: ARBISCAN_KEY, + arbitrum: ARBISCAN_KEY, + 'arbitrum-goerli': ARBISCAN_KEY }, + customChains: [ + { + // Hardhat's Etherscan plugin calls the network `arbitrumOne`, so we need to add an entry for our own network name + network: 'arbitrum', + chainId: 42161, + urls: { + apiURL: 'https://api.arbiscan.io/api', + browserURL: 'https://arbiscan.io/' + } + }, + { + // Hardhat's Etherscan plugin calls the network `arbitrumGoerli`, so we need to add an entry for our own network name + network: 'arbitrum-goerli', + chainId: 421613, + urls: { + apiURL: 'https://api-goerli.arbiscan.io/api', + browserURL: 'https://goerli.arbiscan.io/' + } + } + ], }, typechain: { @@ -205,6 +245,12 @@ const config: HardhatUserConfig = { polygon: { usdc: polygonRelationConfigMap }, + arbitrum: { + usdc: arbitrumRelationConfigMap + }, + 'arbitrum-goerli': { + usdc: arbitrumGoerliRelationConfigMap + } }, }, @@ -257,6 +303,18 @@ const config: HardhatUserConfig = { network: 'polygon', deployment: 'usdc', auxiliaryBase: 'mainnet' + }, + { + name: 'arbitrum', + network: 'arbitrum', + deployment: 'usdc', + auxiliaryBase: 'mainnet' + }, + { + name: 'arbitrum-goerli', + network: 'arbitrum-goerli', + deployment: 'usdc', + auxiliaryBase: 'goerli' } ], }, diff --git a/package.json b/package.json index 8be8f53d0..f1630e10d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "clean": "hardhat clean && rm -rf build/ cache/ coverage* dist/", "cover": "hardhat cover && npx istanbul report --include coverage.json html lcov", "gas": "REPORT_GAS=true yarn test", - "lint": "eslint 'plugins/**/*' 'scenario/**/*' 'scripts/**/*' 'src/**/*' 'tasks/**/*' 'test/**/*'", + "lint": "eslint 'plugins/**/*' 'scenario/**/*' 'scripts/**/*' 'src/**/*' 'tasks/**/*' 'test/**/*' hardhat.config.ts", "lint-contracts": "solhint 'contracts/**/*.sol'", "lint-contracts:fix": "solhint --fix 'contracts/**/*.sol'", "scenario": "hardhat scenario", @@ -52,9 +52,10 @@ "chai-as-promised": "^7.1.1", "deep-object-diff": "^1.1.9", "jest-diff": "^27.4.2", - "undici": "^5.21.0" + "undici": "^5.21.2" }, "devDependencies": { + "@arbitrum/sdk": "^3.1.2", "@compound-finance/hardhat-import": "^1.0.3", "@ethersproject/experimental": "^5.6.3", "@nomiclabs/hardhat-ethers": "^2.0.4", @@ -74,7 +75,7 @@ "eslint": "^8.12.0", "ethers": "^5.7.2", "fast-glob": "^3.2.7", - "hardhat": "https://github.com/jflatow/hardhat/releases/download/viaIR/hardhat-v2.12.0.tgz", + "hardhat": "^2.12.2", "hardhat-chai-matchers": "https://github.com/jflatow/hardhat/releases/download/viaIR/nomicfoundation-hardhat-chai-matchers-v1.0.4.tgz", "hardhat-change-network": "^0.0.7", "hardhat-contract-sizer": "^2.4.0", diff --git a/plugins/deployment_manager/DiffState.ts b/plugins/deployment_manager/DiffState.ts index bcbc36154..bd40b0c78 100644 --- a/plugins/deployment_manager/DiffState.ts +++ b/plugins/deployment_manager/DiffState.ts @@ -1,7 +1,6 @@ import { diff as jestDiff } from 'jest-diff'; import { diff } from 'deep-object-diff'; import { BigNumber, Contract } from 'ethers'; -import { stringifyJson } from './Utils'; export async function diffState( contract: Contract, @@ -10,21 +9,21 @@ export async function diffState( newBlockNumber?: number ): Promise { const toBigInt = n => (BigNumber.isBigNumber(n) ? n.toBigInt() : n); - const oldConfig = mapObject(await getState(contract, oldBlockNumber), toBigInt); - const newConfig = mapObject(await getState(contract, newBlockNumber), toBigInt); + const oldState = mapObject(await getState(contract, oldBlockNumber), toBigInt); + const newState = mapObject(await getState(contract, newBlockNumber), toBigInt); // Informational log (can also generate a report if we think is valuable) console.log('State changes after migration'); console.log( - jestDiff(newConfig, oldConfig, { - aAnnotation: 'New config', + jestDiff(newState, oldState, { + aAnnotation: 'New state', aIndicator: '+', - bAnnotation: 'Old config', + bAnnotation: 'Old state', bIndicator: '-' }) ); - return JSON.parse(stringifyJson(diff(oldConfig, newConfig))); // Removes null objects for test comparison + return diff(oldState, newState); } export async function getCometConfig(comet: Contract, blockNumber?: number): Promise { diff --git a/plugins/deployment_manager/Spider.ts b/plugins/deployment_manager/Spider.ts index 0f0a37d9c..58c86cd16 100644 --- a/plugins/deployment_manager/Spider.ts +++ b/plugins/deployment_manager/Spider.ts @@ -173,10 +173,10 @@ async function crawl( const addressConfig = relations[address.toLowerCase()]; if (addressConfig) { - //trace(' ... has an address config (${address})'); + //trace(` ... has an address config (${address})`); if (addressConfig.artifact) { //trace(` ... has artifact specified (${addressConfig.artifact})`); - const build = await localBuild(cache, hre, addressConfig.artifact, network, address); + const build = await localBuild(null, hre, addressConfig.artifact, network, address); const alias = await readAlias(build.contract, aliasRender, context, path); return maybeProcess(alias, build, addressConfig); } else { @@ -196,7 +196,7 @@ async function crawl( // } if (aliasTemplateConfig.artifact) { //trace(` ... has artifact specified (${aliasTemplateConfig.artifact})`); - const build = await localBuild(cache, hre, aliasTemplateConfig.artifact, network, address); + const build = await localBuild(null, hre, aliasTemplateConfig.artifact, network, address); const alias = await readAlias(build.contract, aliasRender, context, path); return maybeProcess(alias, build, aliasTemplateConfig); } else { diff --git a/plugins/import/etherscan.ts b/plugins/import/etherscan.ts index fa86a4563..3cb1b7adb 100644 --- a/plugins/import/etherscan.ts +++ b/plugins/import/etherscan.ts @@ -16,7 +16,9 @@ export function getEtherscanApiUrl(network: string): string { fuji: 'api-testnet.snowtrace.io', avalanche: 'api.snowtrace.io', mumbai: 'api-mumbai.polygonscan.com', - polygon: 'api.polygonscan.com' + polygon: 'api.polygonscan.com', + arbitrum: 'api.arbiscan.io', + 'arbitrum-goerli': 'api-goerli.arbiscan.io' }[network]; if (!host) { @@ -37,6 +39,8 @@ export function getEtherscanUrl(network: string): string { avalanche: 'snowtrace.io', mumbai: 'mumbai.polygonscan.com', polygon: 'polygonscan.com', + arbitrum: 'arbiscan.io', + 'arbitrum-goerli': 'goerli.arbiscan.io' }[network]; if (!host) { @@ -57,6 +61,8 @@ export function getEtherscanApiKey(network: string): string { avalanche: process.env.SNOWTRACE_KEY, mumbai: process.env.POLYGONSCAN_KEY, polygon: process.env.POLYGONSCAN_KEY, + arbitrum: process.env.ARBISCAN_KEY, + 'arbitrum-goerli': process.env.ARBISCAN_KEY, }[network]; if (!apiKey) { diff --git a/scenario/BulkerScenario.ts b/scenario/BulkerScenario.ts index f6051b7c0..4e7fc1c16 100644 --- a/scenario/BulkerScenario.ts +++ b/scenario/BulkerScenario.ts @@ -10,7 +10,7 @@ scenario( filter: async (ctx) => await isBulkerSupported(ctx) && !matchesDeployment(ctx, [{deployment: 'weth'}, {network: 'mumbai'}]), supplyCaps: { $asset0: 3000, - $asset2: 1, + $asset1: 3000, }, tokenBalances: { albert: { $base: '== 0', $asset0: 3000 }, @@ -27,7 +27,7 @@ scenario( const collateralAsset = context.getAssetByAddress(collateralAssetAddress); const collateralScale = scaleBN.toBigInt(); const toSupplyCollateral = 3000n * collateralScale; - const toBorrowBase = 1500n * baseScale; + const toBorrowBase = 1000n * baseScale; const toTransferBase = 500n * baseScale; const toSupplyEth = exp(0.01, 18); const toWithdrawEth = exp(0.005, 18); @@ -43,7 +43,7 @@ scenario( // Albert's actions: // 1. Supplies 3000 units of collateral - // 2. Borrows 1500 base + // 2. Borrows 1000 base // 3. Transfers 500 base to Betty // 4. Supplies 0.01 ETH // 5. Withdraws 0.005 ETH @@ -165,9 +165,9 @@ scenario( $asset0: 100, }, tokenBalances: { - albert: { $base: '== 1000000', $asset0: 100 }, + albert: { $base: '== 1000000', $asset0: 3000 }, $comet: { $base: 5000 }, - }, + } }, async ({ comet, actors, rewards, bulker }, context, world) => { const { albert, betty } = actors; @@ -180,8 +180,8 @@ scenario( const collateralScale = scaleBN.toBigInt(); const [rewardTokenAddress] = await rewards.rewardConfig(comet.address); const toSupplyBase = 1_000_000n * baseScale; - const toSupplyCollateral = 100n * collateralScale; - const toBorrowBase = 1500n * baseScale; + const toSupplyCollateral = 3000n * collateralScale; + const toBorrowBase = 1000n * baseScale; const toTransferBase = 500n * baseScale; const toSupplyEth = exp(0.01, 18); const toWithdrawEth = exp(0.005, 18); @@ -207,8 +207,8 @@ scenario( startingRewardBalance + rewardOwed; // Albert's actions: - // 1. Supplies 100 units of collateral - // 2. Borrows 1500 base + // 1. Supplies 3000 units of collateral + // 2. Borrows 1000 base // 3. Transfers 500 base to Betty // 4. Supplies 0.01 ETH // 5. Withdraws 0.005 ETH diff --git a/scenario/CrossChainGovernanceScenario.ts b/scenario/CrossChainGovernanceScenario.ts index b4c989a95..a9bd77464 100644 --- a/scenario/CrossChainGovernanceScenario.ts +++ b/scenario/CrossChainGovernanceScenario.ts @@ -4,6 +4,7 @@ import { utils } from 'ethers'; import { BaseBridgeReceiver } from '../build/types'; import { calldata } from '../src/deploy'; import { isBridgedDeployment, matchesDeployment, createCrossChainProposal } from './utils'; +import { ArbitrumBridgeReceiver } from '../build/types'; // This is a generic scenario that runs for all L2s and sidechains scenario( @@ -142,6 +143,116 @@ scenario( await createCrossChainProposal(context, l2ProposalData, newBridgeReceiver); + expect(await newLocalTimelock.delay()).to.eq(newTimelockDelay); + expect(await comet.isAbsorbPaused()).to.eq(true); + expect(await comet.isBuyPaused()).to.eq(true); + expect(await comet.isSupplyPaused()).to.eq(true); + expect(await comet.isTransferPaused()).to.eq(true); + expect(await comet.isWithdrawPaused()).to.eq(true); + } +); + +scenario( + 'upgrade Arbitrum governance contracts and ensure they work properly', + { + filter: async ctx => matchesDeployment(ctx, [{network: 'arbitrum'}, {network: 'arbitrum-goerli'}]) + }, + async ({ comet, configurator, proxyAdmin, timelock: oldLocalTimelock, bridgeReceiver: oldBridgeReceiver }, context, world) => { + const dm = world.deploymentManager; + const governanceDeploymentManager = world.auxiliaryDeploymentManager; + if (!governanceDeploymentManager) { + throw new Error('cannot execute governance without governance deployment manager'); + } + + // Deploy new ArbitrumBridgeReceiver + const newBridgeReceiver = await dm.deploy( + 'newBridgeReceiver', + 'bridges/arbitrum/ArbitrumBridgeReceiver.sol', + [] + ); + + // Deploy new local Timelock + const secondsPerDay = 24 * 60 * 60; + const newLocalTimelock = await dm.deploy( + 'newTimelock', + 'vendor/Timelock.sol', + [ + newBridgeReceiver.address, // admin + 2 * secondsPerDay, // delay + 14 * secondsPerDay, // grace period + 2 * secondsPerDay, // minimum delay + 30 * secondsPerDay // maxiumum delay + ] + ); + + // Initialize new ArbitrumBridgeReceiver + const mainnetTimelock = (await governanceDeploymentManager.getContractOrThrow('timelock')).address; + await newBridgeReceiver.initialize( + mainnetTimelock, // govTimelock + newLocalTimelock.address // localTimelock + ); + + // Process for upgrading L2 governance contracts (order matters): + // 1. Update the admin of Comet in Configurator to be the new Timelock + // 2. Update the admin of CometProxyAdmin to be the new Timelock + const transferOwnershipCalldata = utils.defaultAbiCoder.encode( + ['address'], + [newLocalTimelock.address] + ); + const setGovernorCalldata = await calldata( + configurator.populateTransaction.setGovernor(comet.address, newLocalTimelock.address) + ); + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + const upgradeL2GovContractsProposal = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, proxyAdmin.address, proxyAdmin.address], + [0, 0, 0], + [ + 'setGovernor(address,address)', + 'deployAndUpgradeTo(address,address)', + 'transferOwnership(address)' + ], + [setGovernorCalldata, deployAndUpgradeToCalldata, transferOwnershipCalldata] + ] + ); + + expect(await proxyAdmin.owner()).to.eq(oldLocalTimelock.address); + expect(await comet.governor()).to.eq(oldLocalTimelock.address); + + await createCrossChainProposal(context, upgradeL2GovContractsProposal, oldBridgeReceiver); + + expect(await proxyAdmin.owner()).to.eq(newLocalTimelock.address); + expect(await comet.governor()).to.eq(newLocalTimelock.address); + + // Update aliases now that the new Timelock and BridgeReceiver are official + await dm.putAlias('timelock', newLocalTimelock); + await dm.putAlias('bridgeReceiver', newBridgeReceiver); + + // Now, test that the new L2 governance contracts are working properly via another cross-chain proposal + const currentTimelockDelay = await newLocalTimelock.delay(); + const newTimelockDelay = currentTimelockDelay.mul(2); + + const setDelayCalldata = utils.defaultAbiCoder.encode(['uint'], [newTimelockDelay]); + const pauseCalldata = await calldata(comet.populateTransaction.pause(true, true, true, true, true)); + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [newLocalTimelock.address, comet.address], + [0, 0], + ['setDelay(uint256)', 'pause(bool,bool,bool,bool,bool)'], + [setDelayCalldata, pauseCalldata] + ] + ); + + expect(await newLocalTimelock.delay()).to.eq(currentTimelockDelay); + expect(currentTimelockDelay).to.not.eq(newTimelockDelay); + + await createCrossChainProposal(context, l2ProposalData, newBridgeReceiver); + expect(await newLocalTimelock.delay()).to.eq(newTimelockDelay); expect(await comet.isAbsorbPaused()).to.eq(true); expect(await comet.isBuyPaused()).to.eq(true); diff --git a/scenario/RewardsScenario.ts b/scenario/RewardsScenario.ts index 03c215f46..a879a2bbe 100644 --- a/scenario/RewardsScenario.ts +++ b/scenario/RewardsScenario.ts @@ -1,9 +1,10 @@ -import { scenario } from './context/CometContext'; +import { CometContext, CometProperties, scenario } from './context/CometContext'; import { expect } from 'chai'; import { exp } from '../test/helpers'; import { isRewardSupported, matchesDeployment } from './utils'; -import { Contract } from 'ethers'; -import { ERC20__factory } from '../build/types'; +import { Contract, ContractReceipt } from 'ethers'; +import { CometRewards, ERC20__factory } from '../build/types'; +import {World} from '../plugins/scenario'; function calculateRewardsOwed( userBalance: bigint, @@ -210,4 +211,93 @@ scenario( return txn; // return txn to measure gas } -); \ No newline at end of file +); + +const MULTIPLIERS = [ + exp(55, 18), + exp(10, 18), + exp(1, 18), + exp(0.01, 18), + exp(0.00355, 18) +]; + +for (let i = 0; i < MULTIPLIERS.length; i++) { + scenario( + `Comet#rewards > can claim supply rewards on scaling rewards contract with multiplier of ${MULTIPLIERS[i]}`, + { + filter: async (ctx) => await isRewardSupported(ctx), + tokenBalances: { + albert: { $base: ' == 100' }, // in units of asset, not wei + }, + }, + async (properties, context, world) => { + return await testScalingReward(properties, context, world, MULTIPLIERS[i]); + } + ); +} + +async function testScalingReward(properties: CometProperties, context: CometContext, world: World, multiplier: bigint): Promise { + const { comet, actors, rewards } = properties; + const { albert } = actors; + const baseAssetAddress = await comet.baseToken(); + const baseAsset = context.getAssetByAddress(baseAssetAddress); + const baseScale = (await comet.baseScale()).toBigInt(); + + const [rewardTokenAddress, rescaleFactorWithoutMultiplier] = await rewards.rewardConfig(comet.address); + // XXX maybe try with a different reward token as well + const rewardToken = new Contract( + rewardTokenAddress, + ERC20__factory.createInterface(), + world.deploymentManager.hre.ethers.provider + ); + const rewardDecimals = await rewardToken.decimals(); + const rewardScale = exp(1, rewardDecimals); + + // Deploy new rewards contract with a multiplier + const newRewards = await world.deploymentManager.deploy( + 'newRewards', + 'CometRewards.sol', + [albert.address] + ); + await newRewards.connect(albert.signer).setRewardConfigWithMultiplier(comet.address, rewardTokenAddress, multiplier); + await context.sourceTokens(exp(1_000, rewardDecimals), rewardTokenAddress, newRewards.address); + + await baseAsset.approve(albert, comet.address); + await albert.safeSupplyAsset({ asset: baseAssetAddress, amount: 100n * baseScale }); + + expect(await rewardToken.balanceOf(albert.address)).to.be.equal(0n); + + const supplyTimestamp = await world.timestamp(); + const albertBalance = await albert.getCometBaseBalance(); + const totalSupplyBalance = (await comet.totalSupply()).toBigInt(); + + await world.increaseTime(86400); // fast forward a day + const preTxnTimestamp = await world.timestamp(); + + const newRewardsOwedBefore = (await newRewards.callStatic.getRewardOwed(comet.address, albert.address)).owed.toBigInt(); + const txn = await (await newRewards.connect(albert.signer).claim(comet.address, albert.address, true)).wait(); + const newRewardsOwedAfter = (await newRewards.callStatic.getRewardOwed(comet.address, albert.address)).owed.toBigInt(); + + const postTxnTimestamp = await world.timestamp(); + const timeElapsed = postTxnTimestamp - preTxnTimestamp; + + const supplySpeed = (await comet.baseTrackingSupplySpeed()).toBigInt(); + const trackingIndexScale = (await comet.trackingIndexScale()).toBigInt(); + const timestampDelta = preTxnTimestamp - supplyTimestamp; + const totalSupplyPrincipal = (await comet.totalsBasic()).totalSupplyBase.toBigInt(); + const baseMinForRewards = (await comet.baseMinForRewards()).toBigInt(); + let expectedRewardsOwedWithoutMultiplier = 0n; + let expectedRewardsReceivedWithoutMultiplier = 0n; + if (totalSupplyPrincipal >= baseMinForRewards) { + expectedRewardsOwedWithoutMultiplier = calculateRewardsOwed(albertBalance, totalSupplyBalance, supplySpeed, timestampDelta, trackingIndexScale, rewardScale, rescaleFactorWithoutMultiplier.toBigInt()); + expectedRewardsReceivedWithoutMultiplier = calculateRewardsOwed(albertBalance, totalSupplyBalance, supplySpeed, timestampDelta + timeElapsed, trackingIndexScale, rewardScale, rescaleFactorWithoutMultiplier.toBigInt()); + } + + // Occasionally `timestampDelta` is equal to 86401 + expect(timestampDelta).to.be.greaterThanOrEqual(86400); + expect(newRewardsOwedBefore).to.be.equal(expectedRewardsOwedWithoutMultiplier * multiplier / exp(1, 18)); + expect(await rewardToken.balanceOf(albert.address)).to.be.equal(expectedRewardsReceivedWithoutMultiplier * multiplier / exp(1, 18)); + expect(newRewardsOwedAfter).to.be.equal(0n); + + return txn; // return txn to measure gas +} \ No newline at end of file diff --git a/scenario/SupplyScenario.ts b/scenario/SupplyScenario.ts index 530355e25..e42c4b383 100644 --- a/scenario/SupplyScenario.ts +++ b/scenario/SupplyScenario.ts @@ -305,6 +305,7 @@ scenario( }), [ /ERC20: transfer amount exceeds allowance/, + /ERC20: insufficient allowance/, /transfer amount exceeds spender allowance/, /Dai\/insufficient-allowance/ ] diff --git a/scenario/constraints/MigrationConstraint.ts b/scenario/constraints/MigrationConstraint.ts index 1b30864d6..a0c2b16a2 100644 --- a/scenario/constraints/MigrationConstraint.ts +++ b/scenario/constraints/MigrationConstraint.ts @@ -50,6 +50,7 @@ export class MigrationConstraint implements StaticConstr const artifact = await migration.actions.prepare(ctx.world.deploymentManager, govDeploymentManager); debug(`${label} Prepared migration ${migration.name}.\n Artifact\n-------\n\n${JSON.stringify(artifact, null, 2)}\n-------\n`); if (await isEnacted(migration.actions, ctx.world.deploymentManager, govDeploymentManager)) { + migrationData.skipVerify = true; debug(`${label} Migration ${migration.name} has already been enacted`); } else { migrationData.preMigrationBlockNumber = await ctx.world.deploymentManager.hre.ethers.provider.getBlockNumber(); diff --git a/scenario/constraints/NativeTokenConstraint.ts b/scenario/constraints/NativeTokenConstraint.ts index 00fb3dbf1..b3eed1869 100644 --- a/scenario/constraints/NativeTokenConstraint.ts +++ b/scenario/constraints/NativeTokenConstraint.ts @@ -9,11 +9,14 @@ export class NativeTokenConstraint implements StaticCons for (const symbol in ctx.assets) { const contract = await ctx.world.deploymentManager.contract(symbol); if (contract && contract['deposit()']) { - const whales = await ctx.getWhales(); + const [whale]= await ctx.getWhales(); + if (!whale) { + throw new Error(`NativeTokenConstraint: no whale found for ${ctx.world.deploymentManager.network}`); + } const amount = exp(200_000, await contract.decimals()); // can make this more sophisticated as needed... await contract.deposit({ value: amount }); - await contract.transfer(whales[0], amount); + await contract.transfer(whale, amount); } } return ctx; diff --git a/scenario/constraints/ProposalConstraint.ts b/scenario/constraints/ProposalConstraint.ts index 57d8da427..c35dbaa03 100644 --- a/scenario/constraints/ProposalConstraint.ts +++ b/scenario/constraints/ProposalConstraint.ts @@ -95,7 +95,7 @@ export class ProposalConstraint implements StaticConstra // Verify all unverified migrations (e.g. ones that are not tied to proposals) if (ctx.migrations) { for (const migrationData of ctx.migrations) { - if (migrationData.verified === true) continue; + if (migrationData.verified === true || migrationData.skipVerify === true) continue; await migrationData.migration.actions.verify( ctx.world.deploymentManager, govDeploymentManager, diff --git a/scenario/context/CometContext.ts b/scenario/context/CometContext.ts index e540a4aaf..fcca35428 100644 --- a/scenario/context/CometContext.ts +++ b/scenario/context/CometContext.ts @@ -43,6 +43,7 @@ export type MigrationData = { migration: Migration; lastProposal?: number; preMigrationBlockNumber?: number; + skipVerify?: boolean; verified?: boolean; } @@ -72,7 +73,7 @@ export class CometContext { } async getCompWhales(): Promise { - const useMainnetComp = ['mainnet', 'polygon'].includes(this.world.base.network); + const useMainnetComp = ['mainnet', 'polygon', 'arbitrum'].includes(this.world.base.network); return COMP_WHALES[useMainnetComp ? 'mainnet' : 'testnet']; } diff --git a/scenario/utils/arbitrumUtils.ts b/scenario/utils/arbitrumUtils.ts new file mode 100644 index 000000000..47d56498a --- /dev/null +++ b/scenario/utils/arbitrumUtils.ts @@ -0,0 +1,93 @@ +import { BigNumber, Contract, utils } from 'ethers'; +import { DeploymentManager } from '../../plugins/deployment_manager/DeploymentManager'; + +// https://github.com/OffchainLabs/arbitrum/blob/master/packages/arb-bridge-eth/contracts/libraries/AddressAliasHelper.sol +export function applyL1ToL2Alias(l1Address: string) { + const offset = BigNumber.from('0x1111000000000000000000000000000000001111'); + return BigNumber.from(l1Address).add(offset).toHexString(); +} + +const gatewayInterface = new utils.Interface( + [ + 'function counterpartGateway() view external returns (address)', + 'function finalizeInboundTransfer(address _token, address _from, address _to, uint256 _amount, bytes calldata _data) external payable' + ] +); + +// See https://developer.arbitrum.io/devs-how-tos/how-to-estimate-gas +export async function estimateL2Transaction( + { from, to, data }: { to: string, from: string, data: string }, + l2DeploymentManager: DeploymentManager +) { + // guess what the l1 gas price will be when the proposal is executed + const l1GasPrice = (utils.parseUnits('200', 'gwei')).toNumber(); + // overestimating standard l2 gas by 5x (usually is 0.1 gwei) + const l2GasPrice = (utils.parseUnits('0.5', 'gwei')).toNumber(); + + const l2GasEstimateHex = await l2DeploymentManager.hre.network.provider.send( + 'eth_estimateGas', + [{ from, to, data }] + ); + const l2GasEstimate = BigNumber.from(l2GasEstimateHex); + + // Add overhead to cover retryable ticket creation etc + const gasBuffer = 200_000; + const l2GasLimit = BigNumber.from(gasBuffer).add(l2GasEstimate.mul(3).div(2)); + + const bytesLength = utils.hexDataLength(data); + // https://etherscan.io/address/0x5aed5f8a1e3607476f1f81c3d8fe126deb0afe94#code + // calculateRetryableSubmissionFee + const submissionCost = (1400 + 6 * bytesLength) * l1GasPrice; + const submissionCostWithMargin = utils.parseUnits('10', 'gwei').add(submissionCost); + + const deposit = submissionCostWithMargin.add(l2GasLimit.mul(l2GasPrice)); + + return { + // gasLimit/maxGas + gasLimit: l2GasLimit, + // maxFeePerGas/gasPriceBid + maxFeePerGas: l2GasPrice, + // maxSubmissionCost/maxSubmissionFee + maxSubmissionCost: submissionCostWithMargin, + // deposit + deposit + }; +} + +export async function estimateTokenBridge( + { to, from, token, amount }: {to: string, from: string, token: string, amount: bigint}, + l1DeploymentManager: DeploymentManager, + l2DeploymentManager: DeploymentManager +) { + const { arbitrumL1GatewayRouter } = await l1DeploymentManager.getContracts(); + + const l1GatewayAddress = await arbitrumL1GatewayRouter.getGateway(token); + const l1Gateway = new Contract( + l1GatewayAddress, + gatewayInterface, + l1DeploymentManager.hre.ethers.provider + ); + const l2GatewayAddress = await l1Gateway.counterpartGateway(); + + const data = gatewayInterface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + token, // address _token, + from, // address _from, + to, // address _to, + amount, // uint256 _amount, + utils.defaultAbiCoder.encode( + ['bytes', 'bytes'], ['0x', '0x'] + ) // bytes calldata _data + ] + ); + + return await estimateL2Transaction( + { + from: applyL1ToL2Alias(l1GatewayAddress), + to: l2GatewayAddress, + data + }, + l2DeploymentManager + ); +} \ No newline at end of file diff --git a/scenario/utils/bridgeProposal.ts b/scenario/utils/bridgeProposal.ts index 1ab67f3b9..14d7cf923 100644 --- a/scenario/utils/bridgeProposal.ts +++ b/scenario/utils/bridgeProposal.ts @@ -8,7 +8,7 @@ export async function getOpenBridgedProposals( ): Promise { const receiver = await deploymentManager.contract('bridgeReceiver'); if (receiver === undefined) return []; - const timelockBuf = 30000; // XXX this should be timelock.delay + timelock.GRACE_PERIOD + const timelockBuf = 500_000; // XXX using a high value because Arbitrum has fast block times const searchBlocks = timelockBuf; const block = await deploymentManager.hre.ethers.provider.getBlockNumber(); const filter = receiver.filters.ProposalCreated(); diff --git a/scenario/utils/index.ts b/scenario/utils/index.ts index 22436af75..b9dac6f60 100644 --- a/scenario/utils/index.ts +++ b/scenario/utils/index.ts @@ -58,11 +58,12 @@ export function expectRevertCustom(tx: Promise Bridge + const bridge = await governanceDeploymentManager.getContractOrThrow('arbitrumBridge'); + + // L2 contracts + const bridgeReceiver = await bridgeDeploymentManager.getContractOrThrow('bridgeReceiver'); + + const inboxMessageDeliveredEvents: Log[] = await governanceDeploymentManager.hre.ethers.provider.getLogs({ + fromBlock: startingBlockNumber, + toBlock: 'latest', + address: inbox.address, + topics: [utils.id('InboxMessageDelivered(uint256,bytes)')] + }); + + const dataAndTargets = inboxMessageDeliveredEvents.map(({ data, topics }) => { + const header = '0x'; + const headerLength = header.length; + const wordLength = 2 * 32; + const innnerData = header + data.slice(headerLength + (11 * wordLength)); + const toValue = data.slice(headerLength + (2 * wordLength), headerLength + (3 * wordLength)); + const toAddress = BigNumber.from(`0x${toValue}`).toHexString(); + const messageNum = topics[1]; + return { + data: innnerData, + toAddress, + messageNum + }; + }); + + const messageDeliveredEvents: Log[] = await governanceDeploymentManager.hre.ethers.provider.getLogs({ + fromBlock: startingBlockNumber, + toBlock: 'latest', + address: bridge.address, + topics: [utils.id('MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)')] + }); + + const senders = messageDeliveredEvents.map(({ data, topics }) => { + const decodedData = utils.defaultAbiCoder.decode( + [ + 'address inbox', + 'uint8 kind', + 'address sender', + 'bytes32 messageDataHash', + 'uint256 baseFeeL1', + 'uint64 timestamp' + ], + data + ); + const { sender } = decodedData; + const messageNum = topics[1]; + return { + sender, + messageNum + }; + }); + + const bridgedMessages = dataAndTargets.map((dataAndTarget, i) => { + if (dataAndTarget.messageNum !== senders[i].messageNum) { + throw new Error(`Mismatched message numbers in Arbitrum bridged message to ${dataAndTarget.toAddress}`); + } + return { + ...dataAndTarget, + ...senders[i] + }; + }); + + for (let bridgedMessage of bridgedMessages) { + const { sender, data, toAddress } = bridgedMessage; + const arbitrumSigner = await impersonateAddress( + bridgeDeploymentManager, + sender + ); + const transactionRequest = await arbitrumSigner.populateTransaction({ + to: toAddress, + from: sender, + data, + gasPrice: 0 + }); + + await setNextBaseFeeToZero(bridgeDeploymentManager); + + const tx = await ( + await arbitrumSigner.sendTransaction(transactionRequest) + ).wait(); + + const proposalCreatedLog = tx.logs.find( + event => event.address === bridgeReceiver.address + ); + if (proposalCreatedLog) { + const { + args: { id, eta } + } = bridgeReceiver.interface.parseLog(proposalCreatedLog); + + // fast forward l2 time + await setNextBlockTimestamp(bridgeDeploymentManager, eta.toNumber() + 1); + + // execute queued proposal + await setNextBaseFeeToZero(bridgeDeploymentManager); + await bridgeReceiver.executeProposal(id, { gasPrice: 0 }); + } + } +} diff --git a/scenario/utils/relayMessage.ts b/scenario/utils/relayMessage.ts index f55661de5..aeb1e5ccf 100644 --- a/scenario/utils/relayMessage.ts +++ b/scenario/utils/relayMessage.ts @@ -1,5 +1,6 @@ import { DeploymentManager } from '../../plugins/deployment_manager'; import relayPolygonMessage from './relayPolygonMessage'; +import relayArbitrumMessage from './relayArbitrumMessage'; export default async function relayMessage( governanceDeploymentManager: DeploymentManager, @@ -12,6 +13,10 @@ export default async function relayMessage( case 'polygon': await relayPolygonMessage(governanceDeploymentManager, bridgeDeploymentManager, startingBlockNumber); break; + case 'arbitrum': + case 'arbitrum-goerli': + await relayArbitrumMessage(governanceDeploymentManager, bridgeDeploymentManager, startingBlockNumber); + break; default: throw new Error(`No message relay implementation from ${bridgeNetwork} -> ${governanceDeploymentManager.network}`); } diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 3c4d0a563..75cfc0ca0 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -94,6 +94,16 @@ export const WHALES = { '0x2093b4281990a568c9d588b8bce3bfd7a1557ebd', // WETH whale '0xd814b26554204245a30f8a42c289af582421bf04', // WBTC whale '0x167384319b41f7094e62f7506409eb38079abff8' // WMATIC whale + ], + 'arbitrum': [ + '0xf89d7b9c864f589bbf53a82105107622b35eaa40', // USDC whale + '0x7b7b957c284c2c227c980d6e2f804311947b84d0', // WBTC whale + '0x88730d254A2f7e6AC8388c3198aFd694bA9f7fae' // COMP whale + ], + 'arbitrum-goerli': [ + '0x4984cbfa5b199e5920995883d345bbe14b005db7', // USDC whale + '0xbbfe34e868343e6f4f5e8b5308de980d7bd88c46', // LINK whale + '0x8DA65F8E3Aa22A498211fc4204C498ae9050DAE4', // COMP whale ] }; diff --git a/test/bridges/base-bridge-receiver-test.ts b/test/bridges/base-bridge-receiver-test.ts index 45ebda545..deb4a4371 100644 --- a/test/bridges/base-bridge-receiver-test.ts +++ b/test/bridges/base-bridge-receiver-test.ts @@ -13,7 +13,7 @@ enum ProposalState { Executed = 2 } -async function makeTimelock({ admin }: { admin: string }) { +export async function makeTimelock({ admin }: { admin: string }) { const TimelockFactory = (await ethers.getContractFactory('Timelock')) as Timelock__factory; const timelock = await TimelockFactory.deploy( admin, // admin @@ -51,7 +51,7 @@ async function makeBridgeReceiver({ initialize } = { initialize: true }) { }; } -function encodeBridgeReceiverCalldata({ targets, values, signatures, calldatas }) { +export function encodeBridgeReceiverCalldata({ targets, values, signatures, calldatas }) { return utils.defaultAbiCoder.encode(BRIDGE_RECEIVER_CALLDATA_ABI, [targets, values, signatures, calldatas]); } diff --git a/test/bridges/sweepable-bridge-receiver-test.ts b/test/bridges/sweepable-bridge-receiver-test.ts new file mode 100644 index 000000000..9c557487c --- /dev/null +++ b/test/bridges/sweepable-bridge-receiver-test.ts @@ -0,0 +1,181 @@ +import { ethers, exp, expect, wait } from './../helpers'; +import { utils } from 'ethers'; +import { + SweepableBridgeReceiverHarness__factory, + FaucetToken__factory, + NonStandardFaucetToken__factory, +} from '../../build/types'; +import { encodeBridgeReceiverCalldata, makeTimelock } from './base-bridge-receiver-test'; + +async function makeSweepableBridgeReceiver({ initialize } = { initialize: true }) { + const [_defaultSigner, govTimelockAdmin, ...signers] = await ethers.getSigners(); + + const SweepableBridgeReceiverFactory = (await ethers.getContractFactory('SweepableBridgeReceiverHarness')) as SweepableBridgeReceiverHarness__factory; + const sweepableBridgeReceiver = await SweepableBridgeReceiverFactory.deploy(); + await sweepableBridgeReceiver.deployed(); + + const govTimelock = await makeTimelock({ admin: govTimelockAdmin.address }); + const localTimelock = await makeTimelock({ admin: sweepableBridgeReceiver.address }); + + if (initialize) { + await sweepableBridgeReceiver.initialize( + govTimelock.address, // govTimelock + localTimelock.address // localTimelock + ); + } + + return { + sweepableBridgeReceiver, + govTimelock, + localTimelock, + signers + }; +} + +async function makeFaucetToken(initialAmount: number, name: string, decimals: number, symbol: string) { + const FaucetFactory = (await ethers.getContractFactory('FaucetToken')) as FaucetToken__factory; + const token = await FaucetFactory.deploy(initialAmount, name, decimals, symbol); + await token.deployed(); + + return token; +} + +async function proposeAndExecute(sweepableBridgeReceiver, govTimelock, { targets, values, signatures, calldatas }) { + // enqueue proposal to sweep tokens + const calldata = encodeBridgeReceiverCalldata({ + targets, + values, + signatures, + calldatas + }); + await sweepableBridgeReceiver.processMessageExternal(govTimelock.address, calldata); + + // execute proposal to sweep tokens + const { eta } = await sweepableBridgeReceiver.proposals(1); + await ethers.provider.send('evm_setNextBlockTimestamp', [eta.toNumber()]); + await wait(sweepableBridgeReceiver.executeProposal(1)); +} + +describe('SweepableBridgeReceiver', async() => { + it('sweeps standard ERC20 token', async () => { + const { sweepableBridgeReceiver, localTimelock, govTimelock, signers } = await makeSweepableBridgeReceiver(); + const [alice] = signers; + + const USDC = await makeFaucetToken(1e6, 'USDC', 6, 'USDC'); + + // Alice "accidentally" sends 10 USDC to the SweepableBridgeReceiver + const transferAmount = exp(10, 6); + await USDC.allocateTo(alice.address, transferAmount); + await USDC.connect(alice).transfer(sweepableBridgeReceiver.address, transferAmount); + + const oldBridgeReceiverBalance = await USDC.balanceOf(sweepableBridgeReceiver.address); + const oldTimelockBalance = await USDC.balanceOf(localTimelock.address); + + await proposeAndExecute( + sweepableBridgeReceiver, + govTimelock, + { + targets: [sweepableBridgeReceiver.address], + values: [0], + signatures: ['sweepToken(address,address)'], + calldatas: [ + utils.defaultAbiCoder.encode(['address', 'address'], [localTimelock.address, USDC.address]), + ] + } + ); + + const newBridgeReceiverBalance = await USDC.balanceOf(sweepableBridgeReceiver.address); + const newTimelockBalance = await USDC.balanceOf(localTimelock.address); + + expect(newBridgeReceiverBalance.sub(oldBridgeReceiverBalance)).to.be.equal(-transferAmount); + expect(newTimelockBalance.sub(oldTimelockBalance)).to.be.equal(transferAmount); + }); + + it('sweeps non-standard ERC20 token', async () => { + const { sweepableBridgeReceiver, localTimelock, govTimelock, signers } = await makeSweepableBridgeReceiver(); + const [alice] = signers; + + // Deploy non-standard token + const NonStandardFaucetFactory = (await ethers.getContractFactory('NonStandardFaucetToken')) as NonStandardFaucetToken__factory; + const nonStandardToken = await NonStandardFaucetFactory.deploy(1000e6, 'Tether', 6, 'USDT'); + await nonStandardToken.deployed(); + + // Alice "accidentally" sends 10 non-standard tokens to the Bulker + const transferAmount = exp(10, 6); + await nonStandardToken.allocateTo(alice.address, transferAmount); + await nonStandardToken.connect(alice).transfer(sweepableBridgeReceiver.address, transferAmount); + + const oldBridgeReceiverBalance = await nonStandardToken.balanceOf(sweepableBridgeReceiver.address); + const oldTimelockBalance = await nonStandardToken.balanceOf(localTimelock.address); + + await proposeAndExecute( + sweepableBridgeReceiver, + govTimelock, + { + targets: [sweepableBridgeReceiver.address], + values: [0], + signatures: ['sweepToken(address,address)'], + calldatas: [ + utils.defaultAbiCoder.encode(['address', 'address'], [localTimelock.address, nonStandardToken.address]), + ] + } + ); + + const newBridgeReceiverBalance = await nonStandardToken.balanceOf(sweepableBridgeReceiver.address); + const newTimelockBalance = await nonStandardToken.balanceOf(localTimelock.address); + + expect(newBridgeReceiverBalance.sub(oldBridgeReceiverBalance)).to.be.equal(-transferAmount); + expect(newTimelockBalance.sub(oldTimelockBalance)).to.be.equal(transferAmount); + }); + + it('sweeps native token', async () => { + const { sweepableBridgeReceiver, localTimelock, govTimelock, signers } = await makeSweepableBridgeReceiver(); + const [alice] = signers; + + // Alice "accidentally" sends 1 ETH to the sweepableBridgeReceiver + const transferAmount = exp(1, 18); + await alice.sendTransaction({ to: sweepableBridgeReceiver.address, value: transferAmount }); + + const oldBridgeReceiverBalance = await ethers.provider.getBalance(sweepableBridgeReceiver.address); + const oldTimelockBalance = await ethers.provider.getBalance(localTimelock.address); + + await proposeAndExecute( + sweepableBridgeReceiver, + govTimelock, + { + targets: [sweepableBridgeReceiver.address], + values: [0], + signatures: ['sweepNativeToken(address)'], + calldatas: [ + utils.defaultAbiCoder.encode(['address'], [localTimelock.address]), + ] + } + ); + + const newBridgeReceiverBalance = await ethers.provider.getBalance(sweepableBridgeReceiver.address); + const newTimelockBalance = await ethers.provider.getBalance(localTimelock.address); + + expect(newBridgeReceiverBalance.sub(oldBridgeReceiverBalance)).to.be.equal(-transferAmount); + expect(newTimelockBalance.sub(oldTimelockBalance)).to.be.equal(transferAmount); + }); + + it('reverts if sweepToken is called by address other than local timelock', async () => { + const { sweepableBridgeReceiver, signers } = await makeSweepableBridgeReceiver(); + const [alice] = signers; + + const USDC = await makeFaucetToken(1e6, 'USDC', 6, 'USDC'); + + // Alice sweeps tokens + await expect(sweepableBridgeReceiver.connect(alice).sweepToken(alice.address, USDC.address)) + .to.be.revertedWith("custom error 'Unauthorized()'"); + }); + + it('reverts if sweepNativeToken is called by non-admin', async () => { + const { sweepableBridgeReceiver, signers } = await makeSweepableBridgeReceiver(); + const [alice] = signers; + + // Alice sweeps ETH + await expect(sweepableBridgeReceiver.connect(alice).sweepNativeToken(alice.address)) + .to.be.revertedWith("custom error 'Unauthorized()'"); + }); +}); \ No newline at end of file diff --git a/test/helpers.ts b/test/helpers.ts index 24db5ed64..31ebf1d81 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -114,7 +114,7 @@ export type ConfiguratorAndProtocol = { export type RewardsOpts = { governor?: SignerWithAddress; - configs?: [Comet, FaucetToken][]; + configs?: [Comet, FaucetToken, Numeric?][]; }; export type Rewards = { @@ -476,8 +476,9 @@ export async function makeRewards(opts: RewardsOpts = {}): Promise { const rewards = await RewardsFactory.deploy(governor.address); await rewards.deployed(); - for (const [comet, token] of configs) { - await wait(rewards.setRewardConfig(comet.address, token.address)); + for (const [comet, token, multiplier] of configs) { + if (multiplier === undefined) await wait(rewards.setRewardConfig(comet.address, token.address)); + else await wait(rewards.setRewardConfigWithMultiplier(comet.address, token.address, multiplier)); } return { diff --git a/test/rewards-test.ts b/test/rewards-test.ts index 9b75b30ba..ec8b7b483 100644 --- a/test/rewards-test.ts +++ b/test/rewards-test.ts @@ -1,5 +1,5 @@ import { ethers } from 'hardhat'; -import { defaultAssets, expect, exp, fastForward, makeProtocol, makeRewards, objectify, wait, event } from './helpers'; +import { defaultAssets, expect, exp, factorScale, fastForward, makeProtocol, makeRewards, objectify, wait, event, getBlock } from './helpers'; describe('CometRewards', () => { describe('claim + supply', () => { @@ -552,7 +552,7 @@ describe('CometRewards', () => { }); }); - describe('setRewardsConfig', () => { + describe('setRewardConfig', () => { it('allows governor to set rewards token with upscale', async () => { const { comet, @@ -564,7 +564,8 @@ describe('CometRewards', () => { expect(objectify(await rewards.rewardConfig(comet.address))).to.be.deep.equal({ token: COMP.address, rescaleFactor: exp(1, 12), - shouldUpscale: true + shouldUpscale: true, + multiplier: exp(1, 18) }); }); @@ -583,7 +584,8 @@ describe('CometRewards', () => { expect(objectify(await rewards.rewardConfig(comet.address))).to.be.deep.equal({ token: COMP.address, rescaleFactor: 10n, - shouldUpscale: false + shouldUpscale: false, + multiplier: exp(1, 18) }); }); @@ -602,7 +604,8 @@ describe('CometRewards', () => { expect(objectify(await rewards.rewardConfig(comet.address))).to.be.deep.equal({ token: COMP.address, rescaleFactor: 1n, - shouldUpscale: true + shouldUpscale: true, + multiplier: exp(1, 18) }); }); @@ -623,8 +626,6 @@ describe('CometRewards', () => { //).to.be.revertedWith(`custom error 'NotPermitted("${alice.address}")'`); ).to.be.revertedWith(`custom error 'NotPermitted(address)'`); }); - - // XXX multiple configs }); describe('withdrawToken', () => { @@ -665,6 +666,116 @@ describe('CometRewards', () => { }); }); + describe('setRewardsClaimed', () => { + it('allows governor to set rewards claimed', async () => { + const { + comet, + governor, + tokens: { COMP }, + users: [alice, bob], + } = await makeProtocol(); + const { rewards } = await makeRewards({ governor, configs: [[comet, COMP]] }); + + const txn = await wait(rewards.setRewardsClaimed(comet.address, [alice.address, bob.address], [exp(1, 18), exp(2, 18)])); + + expect(await rewards.rewardsClaimed(comet.address, alice.address)).to.be.equal(exp(1, 18)); + expect(await rewards.rewardsClaimed(comet.address, bob.address)).to.be.equal(exp(2, 18)); + // Check that reward owed still works as expected + const aliceRewardOwed = await rewards.callStatic.getRewardOwed(comet.address, alice.address); + const bobRewardOwed = await rewards.callStatic.getRewardOwed(comet.address, bob.address); + expect(aliceRewardOwed.owed).to.be.equal(0); + expect(bobRewardOwed.owed).to.be.equal(0); + + expect(event(txn, 0)).to.be.deep.equal({ + RewardsClaimedSet: { + user: alice.address, + comet: comet.address, + amount: exp(1, 18) + } + }); + expect(event(txn, 1)).to.be.deep.equal({ + RewardsClaimedSet: { + user: bob.address, + comet: comet.address, + amount: exp(2, 18) + } + }); + }); + + it('can be used to zero out retroactive rewards for users', async () => { + const { + comet, + governor, + tokens: { COMP, USDC }, + users: [alice], + } = await makeProtocol({ + baseMinForRewards: 10e6 + }); + const { rewards } = await makeRewards({ governor, configs: [[comet, COMP]] }); + + // Get Alice into a state where she is owed 86400e18 rewards + await COMP.allocateTo(rewards.address, exp(86400, 18)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + await comet.connect(alice).supply(USDC.address, 10e6); + await fastForward(86400); + await ethers.provider.send('evm_mine', []); + const aliceRewardOwedBefore = await rewards.callStatic.getRewardOwed(comet.address, alice.address); + expect(aliceRewardOwedBefore.owed).to.be.equal(exp(86400, 18)); + expect(await rewards.rewardsClaimed(comet.address, alice.address)).to.be.equal(0); + + // Set rewards claimed for Alice to zero out the rewards owed + const timestampPreTxn = (await getBlock()).timestamp; + const _tx = await wait(rewards.setRewardsClaimed(comet.address, [alice.address], [exp(86400, 18)])); + const elapsed = (await getBlock()).timestamp - timestampPreTxn; + + // Check that rewards owed has been zeroed out + const aliceRewardOwedAfter = await rewards.callStatic.getRewardOwed(comet.address, alice.address); + const expectedRewardOwed = exp(elapsed, 18); + expect(await rewards.rewardsClaimed(comet.address, alice.address)).to.be.equal(exp(86400, 18)); + expect(aliceRewardOwedAfter.owed).to.be.equal(expectedRewardOwed); + + // Make sure that claiming doesn't transfer any retroactive rewards to Alice + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + const _tx2 = await wait(rewards.claim(comet.address, alice.address, true)); + const elapsedSinceSetRewardsClaimed = (await getBlock()).timestamp - timestampPreTxn; + const expectedRewardClaimed = exp(elapsedSinceSetRewardsClaimed, 18); + expect(await COMP.balanceOf(alice.address)).to.be.equal(expectedRewardClaimed); + }); + + it('reverts if addresses and claimedAmounts have different lengths', async () => { + const { + comet, + governor, + tokens: { COMP }, + users: [alice], + } = await makeProtocol(); + const { rewards } = await makeRewards({ governor, configs: [[comet, COMP]] }); + + await expect( + rewards + .setRewardsClaimed(comet.address, [alice.address], []) + ).to.be.revertedWith(`custom error 'BadData()'`); + }); + + it('does not allow anyone but governor to set rewards claimed', async () => { + const { + comet, + governor, + tokens: { COMP }, + users: [alice], + } = await makeProtocol(); + const { rewards } = await makeRewards({ governor, configs: [[comet, COMP]] }); + + await expect( + rewards + .connect(alice) + .setRewardsClaimed(comet.address, [alice.address], [exp(100, 18)]) + //).to.be.revertedWith(`custom error 'NotPermitted("${alice.address}")'`); + ).to.be.revertedWith(`custom error 'NotPermitted(address)'`); + }); + }); + describe('transferGovernor', () => { it('allows governor to transfer governor', async () => { const { @@ -704,3 +815,1012 @@ describe('CometRewards', () => { }); }); }); + +const TEST_CASES = [ + { multiplier: 598314321.512341 }, + { multiplier: 23141 }, + { multiplier: 100 }, + { multiplier: 5.79 }, + { multiplier: 1.33333332 }, + { multiplier: 0.98765 }, + { multiplier: 0.55 }, + { multiplier: 0.12345 }, + { multiplier: 0.01 }, + { multiplier: 0.0598 }, + { multiplier: 0.00355 }, + { multiplier: 0.000015 }, + { multiplier: 0.00000888 } +]; + +for (const { multiplier } of TEST_CASES) { + describe(`CometRewards with multiplier ${multiplier}`, () => { + const MULTIPLIER = multiplier; + const MULTIPLIER_FACTOR = exp(MULTIPLIER, 18); + + describe('claim + supply', () => { + it('can construct and claim rewards for owner with upscale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 18 } + } + ), + baseMinForRewards: 10e6 + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 18)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + const txn = await wait(rewards.claim(comet.address, alice.address, true)); + expect(await COMP.balanceOf(alice.address)).to.be.equal( + (exp(86400, 18) * MULTIPLIER_FACTOR) / factorScale + ); + + // Note: First event is an ERC20 Transfer event + expect(event(txn, 1)).to.be.deep.equal({ + RewardClaimed: { + src: alice.address, + recipient: alice.address, + token: COMP.address, + amount: (exp(86400, 18) * MULTIPLIER_FACTOR) / factorScale + } + }); + }); + + it('can construct and claim rewards for owner with downscale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 2 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 2)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + const txn = await wait(rewards.claim(comet.address, alice.address, true)); + expect(await COMP.balanceOf(alice.address)).to.be.equal( + (exp(86400, 2) * MULTIPLIER_FACTOR) / factorScale + ); + + // Note: First event is an ERC20 Transfer event + expect(event(txn, 1)).to.be.deep.equal({ + RewardClaimed: { + src: alice.address, + recipient: alice.address, + token: COMP.address, + amount: (exp(86400, 2) * MULTIPLIER_FACTOR) / factorScale + } + }); + }); + + it('can construct and claim rewards for owner with upscale with small rescale factor', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 7 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 7)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + const txn = await wait(rewards.claim(comet.address, alice.address, true)); + expect(await COMP.balanceOf(alice.address)).to.be.equal( + (exp(86400, 7) * MULTIPLIER_FACTOR) / factorScale + ); + + // Note: First event is an ERC20 Transfer event + expect(event(txn, 1)).to.be.deep.equal({ + RewardClaimed: { + src: alice.address, + recipient: alice.address, + token: COMP.address, + amount: (exp(86400, 7) * MULTIPLIER_FACTOR) / factorScale + } + }); + }); + + it('can construct and claim rewards for owner with downscale with small rescale factor', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 5 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 5)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + const txn = await wait(rewards.claim(comet.address, alice.address, true)); + expect(await COMP.balanceOf(alice.address)).to.be.equal( + (exp(86400, 5) * MULTIPLIER_FACTOR) / factorScale + ); + + // Note: First event is an ERC20 Transfer event + expect(event(txn, 1)).to.be.deep.equal({ + RewardClaimed: { + src: alice.address, + recipient: alice.address, + token: COMP.address, + amount: (exp(86400, 5) * MULTIPLIER_FACTOR) / factorScale + } + }); + }); + + it('can construct and claim rewards for owner with same scale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 6 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 6)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + const txn = await wait(rewards.claim(comet.address, alice.address, true)); + expect(await COMP.balanceOf(alice.address)).to.be.equal( + (exp(86400, 6) * MULTIPLIER_FACTOR) / factorScale + ); + + // Note: First event is an ERC20 Transfer event + expect(event(txn, 1)).to.be.deep.equal({ + RewardClaimed: { + src: alice.address, + recipient: alice.address, + token: COMP.address, + amount: (exp(86400, 6) * MULTIPLIER_FACTOR) / factorScale + } + }); + }); + + it('does not overpay when claiming more than once', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + baseMinForRewards: 10e6 + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 18)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + const _tx0 = await wait(rewards.claim(comet.address, alice.address, true)); + const _tx1 = await wait(rewards.claim(comet.address, alice.address, false)); + expect(await COMP.balanceOf(alice.address)).to.be.equal( + (exp(86400, 18) * MULTIPLIER_FACTOR) / factorScale + ); + }); + + it('fails if comet instance is already configured', async () => { + const { + comet, + governor, + tokens: { COMP } + } = await makeProtocol({ + baseMinForRewards: 10e6 + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + await expect( + rewards.setRewardConfig(comet.address, COMP.address) + //).to.be.revertedWith(`custom error 'AlreadyConfigured("${comet.address}")`); + ).to.be.revertedWith(`custom error 'AlreadyConfigured(address)'`); + }); + + it('fails if comet instance is not configured', async () => { + const { + comet, + governor, + users: [alice] + } = await makeProtocol(); + const { rewards } = await makeRewards({ governor, configs: [] }); + + await expect( + rewards.claim(comet.address, alice.address, true) + //).to.be.revertedWith(`custom error 'NotSupported("${comet.address}")`); + ).to.be.revertedWith(`custom error 'NotSupported(address)'`); + }); + + it('fails if not enough rewards in the pool to transfer', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + baseMinForRewards: 10e6 + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await expect(rewards.claim(comet.address, alice.address, true)).to.be.revertedWith( + 'ERC20: transfer amount exceeds balance' + ); + }); + }); + + describe('claimTo + borrow', () => { + it('can construct and claim rewards to target with upscale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP, WBTC }, + users: [alice, bob] + } = await makeProtocol({ + baseMinForRewards: exp(10, 6), + baseTrackingBorrowSpeed: exp(2, 15) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * 2 * MULTIPLIER, 18)); + await USDC.allocateTo(comet.address, exp(1e6, 6)); + await WBTC.allocateTo(alice.address, exp(1, 8)); + await WBTC.connect(alice).approve(comet.address, exp(1, 8)); + + // allow manager, supply collateral, borrow + await comet.connect(alice).allow(bob.address, true); + await comet.connect(alice).supply(WBTC.address, exp(1, 8)); + await comet.connect(alice).withdraw(USDC.address, exp(10, 6)); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(await USDC.balanceOf(alice.address)).to.be.equal(10e6); + expect(await comet.borrowBalanceOf(alice.address)).to.be.equal(10e6); + const tx = await wait( + rewards.connect(bob).claimTo(comet.address, alice.address, bob.address, true) + ); + expect(await COMP.balanceOf(bob.address)).to.be.equal( + (exp(86400 * 2, 18) * MULTIPLIER_FACTOR) / factorScale + ); + + // Note: First event is an ERC20 Transfer event + expect(event(tx, 1)).to.be.deep.equal({ + RewardClaimed: { + src: alice.address, + recipient: bob.address, + token: COMP.address, + amount: (exp(86400 * 2, 18) * MULTIPLIER_FACTOR) / factorScale + } + }); + }); + + it('can construct and claim rewards to target with downscale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP, WBTC }, + users: [alice, bob] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 5 } + } + ), + baseMinForRewards: exp(10, 5), + baseTrackingBorrowSpeed: exp(2, 15) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * 2 * MULTIPLIER, 5)); + await USDC.allocateTo(comet.address, exp(1e6, 6)); + await WBTC.allocateTo(alice.address, exp(1, 8)); + await WBTC.connect(alice).approve(comet.address, exp(1, 8)); + + // allow manager, supply collateral, borrow + await comet.connect(alice).allow(bob.address, true); + await comet.connect(alice).supply(WBTC.address, exp(1, 8)); + await comet.connect(alice).withdraw(USDC.address, exp(10, 6)); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(await USDC.balanceOf(alice.address)).to.be.equal(10e6); + expect(await comet.borrowBalanceOf(alice.address)).to.be.equal(10e6); + const _tx = await wait( + rewards.connect(bob).claimTo(comet.address, alice.address, bob.address, true) + ); + expect(await COMP.balanceOf(bob.address)).to.be.equal( + (exp(86400 * 2, 5) * MULTIPLIER_FACTOR) / factorScale + ); + }); + + it('can construct and claim rewards to target with same scale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP, WBTC }, + users: [alice, bob] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 6 } + } + ), + baseMinForRewards: exp(10, 6), + baseTrackingBorrowSpeed: exp(2, 15) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * 2 * MULTIPLIER, 6)); + await USDC.allocateTo(comet.address, exp(1e6, 6)); + await WBTC.allocateTo(alice.address, exp(1, 8)); + await WBTC.connect(alice).approve(comet.address, exp(1, 8)); + + // allow manager, supply collateral, borrow + await comet.connect(alice).allow(bob.address, true); + await comet.connect(alice).supply(WBTC.address, exp(1, 8)); + await comet.connect(alice).withdraw(USDC.address, exp(10, 6)); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(await USDC.balanceOf(alice.address)).to.be.equal(10e6); + expect(await comet.borrowBalanceOf(alice.address)).to.be.equal(10e6); + const _tx = await wait( + rewards.connect(bob).claimTo(comet.address, alice.address, bob.address, true) + ); + expect(await COMP.balanceOf(bob.address)).to.be.equal( + (exp(86400 * 2, 6) * MULTIPLIER_FACTOR) / factorScale + ); + }); + + it('does not allow claiming more than once', async () => { + const { + comet, + governor, + tokens: { USDC, COMP, WBTC }, + users: [alice, bob] + } = await makeProtocol({ + baseMinForRewards: exp(10, 6), + baseTrackingBorrowSpeed: exp(2, 15) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * 2 * MULTIPLIER, 18)); + await USDC.allocateTo(comet.address, exp(1e6, 6)); + await WBTC.allocateTo(alice.address, exp(1, 8)); + await WBTC.connect(alice).approve(comet.address, exp(1, 8)); + + // allow manager, supply collateral, borrow + await comet.connect(alice).allow(bob.address, true); + await comet.connect(alice).supply(WBTC.address, exp(1, 8)); + await comet.connect(alice).withdraw(USDC.address, exp(10, 6)); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(await USDC.balanceOf(alice.address)).to.be.equal(10e6); + expect(await comet.borrowBalanceOf(alice.address)).to.be.equal(10e6); + const _tx0 = await wait( + rewards.connect(bob).claimTo(comet.address, alice.address, bob.address, true) + ); + const _tx1 = await wait( + rewards.connect(bob).claimTo(comet.address, alice.address, bob.address, false) + ); + expect(await COMP.balanceOf(bob.address)).to.be.equal( + (exp(86400 * 2, 18) * MULTIPLIER_FACTOR) / factorScale + ); + }); + + it('fails if comet instance is not configured', async () => { + const { + comet, + governor, + users: [alice, bob] + } = await makeProtocol(); + const { rewards } = await makeRewards({ governor, configs: [] }); + + await comet.connect(alice).allow(bob.address, true); + await expect( + rewards.connect(bob).claim(comet.address, alice.address, true) + //).to.be.revertedWith(`custom error 'NotSupported("${comet.address}")`); + ).to.be.revertedWith(`custom error 'NotSupported(address)'`); + }); + + it('fails if not enough rewards in the pool to transfer', async () => { + const { + comet, + governor, + tokens: { USDC, COMP, WBTC }, + users: [alice, bob] + } = await makeProtocol({ + baseMinForRewards: 10e6 + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await USDC.allocateTo(comet.address, exp(1e6, 6)); + await WBTC.allocateTo(alice.address, exp(1, 8)); + await WBTC.connect(alice).approve(comet.address, exp(1, 8)); + + // allow manager, supply collateral, borrow + await comet.connect(alice).allow(bob.address, true); + await comet.connect(alice).supply(WBTC.address, exp(1, 8)); + await comet.connect(alice).withdraw(USDC.address, exp(10, 6)); + + await expect( + rewards.connect(bob).claimTo(comet.address, alice.address, bob.address, true) + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); + + it('fails if caller is not permitted to claim rewards for owner', async () => { + const { + comet, + governor, + tokens: { COMP }, + users: [alice] + } = await makeProtocol({ + baseMinForRewards: exp(10, 6), + baseTrackingBorrowSpeed: exp(2, 15) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + await expect( + rewards.claimTo(comet.address, alice.address, governor.address, true) + //).to.be.revertedWith(`custom error 'NotPermitted("${governor.address}")'`); + ).to.be.revertedWith(`custom error 'NotPermitted(address)'`); + }); + }); + + describe('getRewardOwed', () => { + it('can construct and calculate rewards for owner with upscale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 18 } + } + ), + baseMinForRewards: 10e6 + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 18)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + await ethers.provider.send('evm_mine', []); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + + const { token, owed } = await rewards.callStatic.getRewardOwed( + comet.address, + alice.address + ); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(token).to.be.equal(COMP.address); + expect(owed).to.be.equal((exp(86400, 18) * MULTIPLIER_FACTOR) / factorScale); + }); + + it('can construct and calculate rewards for owner with downscale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 2 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 2)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + await ethers.provider.send('evm_mine', []); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + + const { token, owed } = await rewards.callStatic.getRewardOwed( + comet.address, + alice.address + ); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(token).to.be.equal(COMP.address); + expect(owed).to.be.equal((exp(86400, 2) * MULTIPLIER_FACTOR) / factorScale); + }); + + it('can construct and calculate rewards for owner with upscale with small rescale factor', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 7 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 7)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + await ethers.provider.send('evm_mine', []); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + + const { token, owed } = await rewards.callStatic.getRewardOwed( + comet.address, + alice.address + ); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(token).to.be.equal(COMP.address); + expect(owed).to.be.equal((exp(86400, 7) * MULTIPLIER_FACTOR) / factorScale); + }); + + it('can construct and calculate rewards for owner with downscale with small rescale factor', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 5 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 5)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + await ethers.provider.send('evm_mine', []); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + + const { token, owed } = await rewards.callStatic.getRewardOwed( + comet.address, + alice.address + ); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(token).to.be.equal(COMP.address); + expect(owed).to.be.equal((exp(86400, 5) * MULTIPLIER_FACTOR) / factorScale); + }); + + it('can construct and calculate rewards for owner with same scale', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 6 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 6)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + await ethers.provider.send('evm_mine', []); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + + const { token, owed } = await rewards.callStatic.getRewardOwed( + comet.address, + alice.address + ); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + expect(token).to.be.equal(COMP.address); + expect(owed).to.be.equal((exp(86400, 6) * MULTIPLIER_FACTOR) / factorScale); + }); + + it('returns 0 owed if user already claimed', async () => { + const { + comet, + governor, + tokens: { USDC, COMP }, + users: [alice] + } = await makeProtocol({ + baseMinForRewards: 10e6 + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + // allocate and approve transfers + await COMP.allocateTo(rewards.address, exp(86400 * MULTIPLIER, 18)); + await USDC.allocateTo(alice.address, 10e6); + await USDC.connect(alice).approve(comet.address, 10e6); + + // supply once + await comet.connect(alice).supply(USDC.address, 10e6); + + await fastForward(86400); + + expect(await COMP.balanceOf(alice.address)).to.be.equal(0); + + const _tx0 = await wait(rewards.claim(comet.address, alice.address, true)); + const { token, owed } = await rewards.callStatic.getRewardOwed( + comet.address, + alice.address + ); + + expect(await COMP.balanceOf(alice.address)).to.be.equal( + (exp(86400, 18) * MULTIPLIER_FACTOR) / factorScale + ); + expect(token).to.be.equal(COMP.address); + expect(owed).to.be.equal(0); + }); + + it('fails if comet instance is not configured', async () => { + const { + comet, + governor, + users: [alice] + } = await makeProtocol(); + const { rewards } = await makeRewards({ governor, configs: [] }); + + await expect( + rewards.getRewardOwed(comet.address, alice.address) + //).to.be.revertedWith(`custom error 'NotSupported("${comet.address}")`); + ).to.be.revertedWith(`custom error 'NotSupported(address)'`); + }); + }); + + describe('setRewardConfig', () => { + it('allows governor to set rewards token with upscale', async () => { + const { + comet, + governor, + tokens: { COMP } + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 18 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + expect(objectify(await rewards.rewardConfig(comet.address))).to.be.deep.equal({ + token: COMP.address, + rescaleFactor: exp(1, 12), + shouldUpscale: true, + multiplier: MULTIPLIER_FACTOR + }); + }); + + it('allows governor to set rewards token with downscale', async () => { + const { + comet, + governor, + tokens: { COMP } + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 2 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + expect(objectify(await rewards.rewardConfig(comet.address))).to.be.deep.equal({ + token: COMP.address, + rescaleFactor: exp(1, 4), + shouldUpscale: false, + multiplier: MULTIPLIER_FACTOR + }); + }); + + it('allows governor to set rewards token with upscale with small rescale factor', async () => { + const { + comet, + governor, + tokens: { COMP } + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 7 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + expect(objectify(await rewards.rewardConfig(comet.address))).to.be.deep.equal({ + token: COMP.address, + rescaleFactor: 10n, + shouldUpscale: true, + multiplier: MULTIPLIER_FACTOR + }); + }); + + it('allows governor to set rewards token with downscale with small rescale factor', async () => { + const { + comet, + governor, + tokens: { COMP } + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 5 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + expect(objectify(await rewards.rewardConfig(comet.address))).to.be.deep.equal({ + token: COMP.address, + rescaleFactor: 10n, + shouldUpscale: false, + multiplier: MULTIPLIER_FACTOR + }); + }); + + it('allows governor to set rewards token with same scale', async () => { + const { + comet, + governor, + tokens: { COMP } + } = await makeProtocol({ + assets: defaultAssets( + {}, + { + COMP: { decimals: 6 } + } + ) + }); + const { rewards } = await makeRewards({ + governor, + configs: [[comet, COMP, MULTIPLIER_FACTOR]] + }); + + expect(objectify(await rewards.rewardConfig(comet.address))).to.be.deep.equal({ + token: COMP.address, + rescaleFactor: 1n, + shouldUpscale: true, + multiplier: MULTIPLIER_FACTOR + }); + }); + + it('does not allow anyone but governor to set config', async () => { + const { + comet, + governor, + tokens: { COMP }, + users: [alice] + } = await makeProtocol(); + const { rewards } = await makeRewards({ governor, configs: [] }); + + expect(await rewards.governor()).to.be.equal(governor.address); + await expect( + rewards + .connect(alice) + .setRewardConfigWithMultiplier(comet.address, COMP.address, MULTIPLIER_FACTOR) + //).to.be.revertedWith(`custom error 'NotPermitted("${alice.address}")'`); + ).to.be.revertedWith(`custom error 'NotPermitted(address)'`); + }); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index 543415bf1..a3ef4190f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,16 @@ # yarn lockfile v1 +"@arbitrum/sdk@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.1.2.tgz#c1ded298778c141b6d8d0342a507a4f94af02757" + integrity sha512-QcS5t6GDCLyY+u0WaYPIjBd2U9hmDbzGc8gLMyiUxpP7w4bOWs6ZGBGUw2N6oLOMLI5IEq9ZbRZEC/Ejsy/URg== + dependencies: + "@ethersproject/address" "^5.0.8" + "@ethersproject/bignumber" "^5.1.1" + "@ethersproject/bytes" "^5.0.8" + ethers "^5.1.0" + "@babel/code-frame@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -23,6 +33,42 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@chainsafe/as-sha256@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz#3639df0e1435cab03f4d9870cc3ac079e57a6fc9" + integrity sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg== + +"@chainsafe/persistent-merkle-tree@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz#4c9ee80cc57cd3be7208d98c40014ad38f36f7ff" + integrity sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + +"@chainsafe/persistent-merkle-tree@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz#2b4a62c9489a5739dedd197250d8d2f5427e9f63" + integrity sha512-l0V1b5clxA3iwQLXP40zYjyZYospQLZXzBVIhhr9kDg/1qHZfzzHw0jj4VPBijfYCArZDlPkRi1wZaV2POKeuw== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + +"@chainsafe/ssz@^0.10.0": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.10.2.tgz#c782929e1bb25fec66ba72e75934b31fd087579e" + integrity sha512-/NL3Lh8K+0q7A3LsiFq09YXS9fPE+ead2rr7vM2QK8PLzrNsw3uqrif9bpRX5UxgeRjM+vYi+boCM3+GM4ovXg== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + "@chainsafe/persistent-merkle-tree" "^0.5.0" + +"@chainsafe/ssz@^0.9.2": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.4.tgz#696a8db46d6975b600f8309ad3a12f7c0e310497" + integrity sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + "@chainsafe/persistent-merkle-tree" "^0.4.2" + case "^1.6.3" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -100,7 +146,7 @@ "@ethersproject/logger" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.5.0", "@ethersproject/address@^5.7.0": +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.0.8", "@ethersproject/address@^5.5.0", "@ethersproject/address@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== @@ -126,7 +172,7 @@ "@ethersproject/bytes" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": +"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.1.1", "@ethersproject/bignumber@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== @@ -135,7 +181,7 @@ "@ethersproject/logger" "^5.7.0" bn.js "^5.2.1" -"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": +"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.0.8", "@ethersproject/bytes@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== @@ -287,7 +333,7 @@ bech32 "1.1.4" ws "7.4.6" -"@ethersproject/providers@5.7.2": +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.1", "@ethersproject/providers@^5.7.2": version "5.7.2" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== @@ -537,6 +583,19 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomicfoundation/ethereumjs-block@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.1.tgz#6f89664f55febbd723195b6d0974773d29ee133d" + integrity sha512-u1Yioemi6Ckj3xspygu/SfFvm8vZEO8/Yx5a1QLzi6nVU0jz3Pg2OmHKJ5w+D9Ogk1vhwRiqEBAqcb0GVhCyHw== + dependencies: + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-trie" "6.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + ethereum-cryptography "0.1.3" + ethers "^5.7.1" + "@nomicfoundation/ethereumjs-block@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz#fdd5c045e7baa5169abeed0e1202bf94e4481c49" @@ -549,6 +608,25 @@ "@nomicfoundation/ethereumjs-util" "^8.0.0" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-blockchain@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.1.tgz#80e0bd3535bfeb9baa29836b6f25123dab06a726" + integrity sha512-NhzndlGg829XXbqJEYrF1VeZhAwSPgsK/OB7TVrdzft3y918hW5KNd7gIZ85sn6peDZOdjBsAXIpXZ38oBYE5A== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.1" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-ethash" "3.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-trie" "6.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + abstract-level "^1.0.3" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + level "^8.0.0" + lru-cache "^5.1.1" + memory-level "^1.0.0" + "@nomicfoundation/ethereumjs-blockchain@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-6.0.0.tgz#1a8c243a46d4d3691631f139bfb3a4a157187b0c" @@ -567,6 +645,14 @@ lru-cache "^5.1.1" memory-level "^1.0.0" +"@nomicfoundation/ethereumjs-common@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.1.tgz#4702d82df35b07b5407583b54a45bf728e46a2f0" + integrity sha512-OBErlkfp54GpeiE06brBW/TTbtbuBJV5YI5Nz/aB2evTDo+KawyEzPjBlSr84z/8MFfj8wS2wxzQX1o32cev5g== + dependencies: + "@nomicfoundation/ethereumjs-util" "9.0.1" + crc-32 "^1.2.0" + "@nomicfoundation/ethereumjs-common@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0.tgz#f6bcc7753994555e49ab3aa517fc8bcf89c280b9" @@ -575,6 +661,18 @@ "@nomicfoundation/ethereumjs-util" "^8.0.0" crc-32 "^1.2.0" +"@nomicfoundation/ethereumjs-ethash@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.1.tgz#65ca494d53e71e8415c9a49ef48bc921c538fc41" + integrity sha512-KDjGIB5igzWOp8Ik5I6QiRH5DH+XgILlplsHR7TEuWANZA759G6krQ6o8bvj+tRUz08YygMQu/sGd9mJ1DYT8w== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + abstract-level "^1.0.3" + bigint-crypto-utils "^3.0.23" + ethereum-cryptography "0.1.3" + "@nomicfoundation/ethereumjs-ethash@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-2.0.0.tgz#11539c32fe0990e1122ff987d1b84cfa34774e81" @@ -587,6 +685,20 @@ bigint-crypto-utils "^3.0.23" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-evm@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.1.tgz#f35681e203363f69ce2b3d3bf9f44d4e883ca1f1" + integrity sha512-oL8vJcnk0Bx/onl+TgQOQ1t/534GKFaEG17fZmwtPFeH8S5soiBYPCLUrvANOl4sCp9elYxIMzIiTtMtNNN8EQ== + dependencies: + "@ethersproject/providers" "^5.7.1" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + mcl-wasm "^0.7.1" + rustbn.js "~0.2.0" + "@nomicfoundation/ethereumjs-evm@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-1.0.0.tgz#99cd173c03b59107c156a69c5e215409098a370b" @@ -601,11 +713,28 @@ mcl-wasm "^0.7.1" rustbn.js "~0.2.0" +"@nomicfoundation/ethereumjs-rlp@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.1.tgz#0b30c1cf77d125d390408e391c4bb5291ef43c28" + integrity sha512-xtxrMGa8kP4zF5ApBQBtjlSbN5E2HI8m8FYgVSYAnO6ssUoY5pVPGy2H8+xdf/bmMa22Ce8nWMH3aEW8CcqMeQ== + "@nomicfoundation/ethereumjs-rlp@^4.0.0", "@nomicfoundation/ethereumjs-rlp@^4.0.0-beta.2": version "4.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0.tgz#d9a9c5f0f10310c8849b6525101de455a53e771d" integrity sha512-GaSOGk5QbUk4eBP5qFbpXoZoZUj/NrW7MRa0tKY4Ew4c2HAS0GXArEMAamtFrkazp0BO4K5p2ZCG3b2FmbShmw== +"@nomicfoundation/ethereumjs-statemanager@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.1.tgz#8824a97938db4471911e2d2f140f79195def5935" + integrity sha512-B5ApMOnlruVOR7gisBaYwFX+L/AP7i/2oAahatssjPIBVDF6wTX1K7Qpa39E/nzsH8iYuL3krkYeUFIdO3EMUQ== + dependencies: + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + ethers "^5.7.1" + js-sdsl "^4.1.4" + "@nomicfoundation/ethereumjs-statemanager@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-1.0.0.tgz#14a9d4e1c828230368f7ab520c144c34d8721e4b" @@ -619,6 +748,17 @@ ethereum-cryptography "0.1.3" functional-red-black-tree "^1.0.1" +"@nomicfoundation/ethereumjs-trie@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.1.tgz#662c55f6b50659fd4b22ea9f806a7401cafb7717" + integrity sha512-A64It/IMpDVODzCgxDgAAla8jNjNtsoQZIzZUfIV5AY6Coi4nvn7+VReBn5itlxMiL2yaTlQr9TRWp3CSI6VoA== + dependencies: + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + "@types/readable-stream" "^2.3.13" + ethereum-cryptography "0.1.3" + readable-stream "^3.6.0" + "@nomicfoundation/ethereumjs-trie@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-5.0.0.tgz#dcfbe3be53a94bc061c9767a396c16702bc2f5b7" @@ -629,6 +769,18 @@ ethereum-cryptography "0.1.3" readable-stream "^3.6.0" +"@nomicfoundation/ethereumjs-tx@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.1.tgz#7629dc2036b4a33c34e9f0a592b43227ef4f0c7d" + integrity sha512-0HwxUF2u2hrsIM1fsasjXvlbDOq1ZHFV2dd1yGq8CA+MEYhaxZr8OTScpVkkxqMwBcc5y83FyPl0J9MZn3kY0w== + dependencies: + "@chainsafe/ssz" "^0.9.2" + "@ethersproject/providers" "^5.7.2" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + ethereum-cryptography "0.1.3" + "@nomicfoundation/ethereumjs-tx@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0.tgz#59dc7452b0862b30342966f7052ab9a1f7802f52" @@ -639,6 +791,15 @@ "@nomicfoundation/ethereumjs-util" "^8.0.0" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-util@9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.1.tgz#530cda8bae33f8b5020a8f199ed1d0a2ce48ec89" + integrity sha512-TwbhOWQ8QoSCFhV/DDfSmyfFIHjPjFBj957219+V3jTZYZ2rf9PmDtNOeZWAE3p3vlp8xb02XGpd0v6nTUPbsA== + dependencies: + "@chainsafe/ssz" "^0.10.0" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + ethereum-cryptography "0.1.3" + "@nomicfoundation/ethereumjs-util@^8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0.tgz#deb2b15d2c308a731e82977aefc4e61ca0ece6c5" @@ -647,6 +808,25 @@ "@nomicfoundation/ethereumjs-rlp" "^4.0.0-beta.2" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-vm@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.1.tgz#7d035e0993bcad10716c8b36e61dfb87fa3ca05f" + integrity sha512-rArhyn0jPsS/D+ApFsz3yVJMQ29+pVzNZ0VJgkzAZ+7FqXSRtThl1C1prhmlVr3YNUlfpZ69Ak+RUT4g7VoOuQ== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.1" + "@nomicfoundation/ethereumjs-blockchain" "7.0.1" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-evm" "2.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-statemanager" "2.0.1" + "@nomicfoundation/ethereumjs-trie" "6.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + mcl-wasm "^0.7.1" + rustbn.js "~0.2.0" + "@nomicfoundation/ethereumjs-vm@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0.tgz#2bb50d332bf41790b01a3767ffec3987585d1de6" @@ -1039,6 +1219,14 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== +"@types/readable-stream@^2.3.13": + version "2.3.15" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.15.tgz#3d79c9ceb1b6a57d5f6e6976f489b9b5384321ae" + integrity sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ== + dependencies: + "@types/node" "*" + safe-buffer "~5.1.1" + "@types/secp256k1@^4.0.1": version "4.0.3" resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.3.tgz#1b8e55d8e00f08ee7220b4d59a6abe89c37a901c" @@ -1644,6 +1832,11 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +case@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9" + integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== + caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -2497,7 +2690,7 @@ ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.5: ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethers@5.7.2, ethers@^5.7.2: +ethers@5.7.2, ethers@^5.1.0, ethers@^5.7.1, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -3122,22 +3315,23 @@ hardhat@^2.0.4: uuid "^8.3.2" ws "^7.4.6" -"hardhat@https://github.com/jflatow/hardhat/releases/download/viaIR/hardhat-v2.12.0.tgz": - version "2.12.0" - resolved "https://github.com/jflatow/hardhat/releases/download/viaIR/hardhat-v2.12.0.tgz#d4c7fedfb6361bbf9080c75ea114755bb303c0d9" +hardhat@^2.12.2: + version "2.14.0" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.14.0.tgz#b60c74861494aeb1b50803cf04cc47865a42b87a" + integrity sha512-73jsInY4zZahMSVFurSK+5TNCJTXMv+vemvGia0Ac34Mm19fYp6vEPVGF3sucbumszsYxiTT2TbS8Ii2dsDSoQ== dependencies: "@ethersproject/abi" "^5.1.2" "@metamask/eth-sig-util" "^4.0.0" - "@nomicfoundation/ethereumjs-block" "^4.0.0" - "@nomicfoundation/ethereumjs-blockchain" "^6.0.0" - "@nomicfoundation/ethereumjs-common" "^3.0.0" - "@nomicfoundation/ethereumjs-evm" "^1.0.0" - "@nomicfoundation/ethereumjs-rlp" "^4.0.0" - "@nomicfoundation/ethereumjs-statemanager" "^1.0.0" - "@nomicfoundation/ethereumjs-trie" "^5.0.0" - "@nomicfoundation/ethereumjs-tx" "^4.0.0" - "@nomicfoundation/ethereumjs-util" "^8.0.0" - "@nomicfoundation/ethereumjs-vm" "^6.0.0" + "@nomicfoundation/ethereumjs-block" "5.0.1" + "@nomicfoundation/ethereumjs-blockchain" "7.0.1" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-evm" "2.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-statemanager" "2.0.1" + "@nomicfoundation/ethereumjs-trie" "6.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + "@nomicfoundation/ethereumjs-vm" "7.0.1" "@nomicfoundation/solidity-analyzer" "^0.1.0" "@sentry/node" "^5.18.1" "@types/bn.js" "^5.1.0" @@ -3173,7 +3367,7 @@ hardhat@^2.0.4: source-map-support "^0.5.13" stacktrace-parser "^0.1.10" tsort "0.0.1" - undici "^5.4.0" + undici "^5.14.0" uuid "^8.3.2" ws "^7.4.6" @@ -5224,7 +5418,7 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.3.tgz#f0feedf019c4510f164099e8d7e72ff2d7304377" integrity sha512-JmMFDME3iufZnBpyKL+uS78LRiC+mK55zWfM5f/pWBJfpOttXAqYfdDGRukYhJuyRinvPVAtUhvy7rlDybNtFg== -undici@^5.14.0, undici@^5.21.0: +undici@^5.14.0, undici@^5.21.2: version "5.22.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.0.tgz#5e205d82a5aecc003fc4388ccd3d2c6e8674a0ad" integrity sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==