From caa66a32717fde2f92afa861f3d3d63e6cf9fb4e Mon Sep 17 00:00:00 2001 From: Dmitriy Babenko <159453675+dmitriy-woof-software@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:46:11 +0200 Subject: [PATCH 1/3] Deploy compound v3 usdt market (#848) Forum thread - https://www.comp.xyz/t/deploy-compound-iii-on-optimism/4975 --------- Co-authored-by: Mikhailo Shabodyash <67977488+EviLord032@users.noreply.github.com> Co-authored-by: GitHub Actions Bot <> --- .github/workflows/enact-migration.yaml | 17 +- .github/workflows/run-scenarios.yaml | 2 +- deployments/optimism/usdt/configuration.json | 57 ++++ deployments/optimism/usdt/deploy.ts | 71 ++++ .../1713012100_configurate_and_ens.ts | 316 ++++++++++++++++++ deployments/optimism/usdt/relations.ts | 30 ++ deployments/optimism/usdt/roots.json | 11 + hardhat.config.ts | 10 +- scenario/BulkerScenario.ts | 6 +- 9 files changed, 516 insertions(+), 4 deletions(-) create mode 100644 deployments/optimism/usdt/configuration.json create mode 100644 deployments/optimism/usdt/deploy.ts create mode 100644 deployments/optimism/usdt/migrations/1713012100_configurate_and_ens.ts create mode 100644 deployments/optimism/usdt/relations.ts create mode 100644 deployments/optimism/usdt/roots.json diff --git a/.github/workflows/enact-migration.yaml b/.github/workflows/enact-migration.yaml index 1b1558688..6842ba074 100644 --- a/.github/workflows/enact-migration.yaml +++ b/.github/workflows/enact-migration.yaml @@ -36,6 +36,10 @@ on: description: Run ID for Artifact eth_pk: description: Ignore if you plan to use WalletConnect, otherwise, you can paste in a Ethereum private key + impersonateAccount: + description: Impersonate Account + required: false + default: '' jobs: enact-migration: name: Enact Migration @@ -114,7 +118,18 @@ jobs: GOV_NETWORK_PROVIDER: ${{ fromJSON('["", "http://localhost:8685"]')[github.event.inputs.eth_pk == '' && env.GOV_NETWORK != ''] }} GOV_NETWORK: ${{ env.GOV_NETWORK }} REMOTE_ACCOUNTS: ${{ fromJSON('["", "true"]')[github.event.inputs.eth_pk == ''] }} - + if: github.event.inputs.impersonateAccount == '' + - name: Run Enact Migration (impersonate) + run: | + yarn hardhat migrate --network ${{ github.event.inputs.network }} --deployment ${{ github.event.inputs.deployment }} --enact --overwrite ${{ fromJSON('["", "--simulate"]')[github.event.inputs.simulate == 'true'] }} ${{ fromJSON('["", "--no-enacted"]')[github.event.inputs.no_enacted == 'true'] }} ${{ github.event.inputs.migration }} --impersonate ${{ github.event.inputs.impersonateAccount }} + env: + DEBUG: true + ETH_PK: "${{ inputs.eth_pk }}" + NETWORK_PROVIDER: ${{ fromJSON('["", "http://localhost:8585"]')[github.event.inputs.eth_pk == ''] }} + GOV_NETWORK_PROVIDER: ${{ fromJSON('["", "http://localhost:8685"]')[github.event.inputs.eth_pk == '' && env.GOV_NETWORK != ''] }} + GOV_NETWORK: ${{ env.GOV_NETWORK }} + REMOTE_ACCOUNTS: ${{ fromJSON('["", "true"]')[github.event.inputs.eth_pk == ''] }} + if: github.event.inputs.impersonateAccount != '' - name: Commit changes if: ${{ github.event.inputs.simulate == 'false' }} run: | diff --git a/.github/workflows/run-scenarios.yaml b/.github/workflows/run-scenarios.yaml index 7c551a883..9c2990382 100644 --- a/.github/workflows/run-scenarios.yaml +++ b/.github/workflows/run-scenarios.yaml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, arbitrum-usdc.e, arbitrum-usdc, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-goerli, base-goerli-weth, linea-goerli, optimism-usdc, scroll-goerli, scroll-usdc] + bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, arbitrum-usdc.e, arbitrum-usdc, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-goerli, base-goerli-weth, linea-goerli, optimism-usdc, optimism-usdt, scroll-goerli, scroll-usdc] name: Run scenarios env: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }} diff --git a/deployments/optimism/usdt/configuration.json b/deployments/optimism/usdt/configuration.json new file mode 100644 index 000000000..a6e5e3665 --- /dev/null +++ b/deployments/optimism/usdt/configuration.json @@ -0,0 +1,57 @@ +{ + "name": "Compound USDT", + "symbol": "cUSDTv3", + "baseToken": "USDT", + "baseTokenAddress": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", + "baseTokenPriceFeed": "0xECef79E109e997bCA29c1c0897ec9d7b03647F5E", + "pauseGuardian": "0x3fFd6c073a4ba24a113B18C8F373569640916A45", + "borrowMin": "1e5", + "storeFrontPriceFactor": 0.6, + "targetReserves": "20000000e6", + "rates": { + "supplyKink": 0.9, + "supplySlopeLow": 0.059, + "supplySlopeHigh": 2.9, + "supplyBase": 0, + "borrowKink": 0.9, + "borrowSlopeLow": 0.061, + "borrowSlopeHigh": 3.2, + "borrowBase": 0.015 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "0e15", + "baseBorrowSpeed": "0e15", + "baseMinForRewards": "1000e6" + }, + "assets": { + "OP": { + "address": "0x4200000000000000000000000000000000000042", + "priceFeed": "0x0D276FC14719f9292D5C1eA2198673d1f4269246", + "decimals": "18", + "borrowCF": 0.65, + "liquidateCF": 0.7, + "liquidationFactor": 0.8, + "supplyCap": "0e18" + }, + "WETH": { + "address": "0x4200000000000000000000000000000000000006", + "priceFeed": "0x13e3Ee699D1909E989722E753853AE30b17e08c5", + "decimals": "18", + "borrowCF": 0.83, + "liquidateCF": 0.9, + "liquidationFactor": 0.95, + "supplyCap": "0e18" + }, + "WBTC": { + "address": "0x68f180fcCe6836688e9084f035309E29Bf0A2095", + "priceFeed": "0x718A5788b89454aAE3A028AE9c111A29Be6c2a6F", + "decimals": "8", + "borrowCF": 0.8, + "liquidateCF": 0.85, + "liquidationFactor": 0.95, + "supplyCap": "0e8" + } + } + } + \ No newline at end of file diff --git a/deployments/optimism/usdt/deploy.ts b/deployments/optimism/usdt/deploy.ts new file mode 100644 index 000000000..ce633038b --- /dev/null +++ b/deployments/optimism/usdt/deploy.ts @@ -0,0 +1,71 @@ +import { + Deployed, + DeploymentManager, +} from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet } from '../../../src/deploy'; + +const HOUR = 60 * 60; +const DAY = 24 * HOUR; + +export default async function deploy( + deploymentManager: DeploymentManager, + deploySpec: DeploySpec +): Promise { + const deployed = await deployContracts(deploymentManager, deploySpec); + return deployed; +} + +async function deployContracts( + deploymentManager: DeploymentManager, + deploySpec: DeploySpec +): Promise { + const trace = deploymentManager.tracer(); + + const USDT = await deploymentManager.existing( + 'USDT', + '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58', + 'optimism' + ); + const WETH = await deploymentManager.existing( + 'WETH', + '0x4200000000000000000000000000000000000006', + 'optimism' + ); + const WBTC = await deploymentManager.existing( + 'WBTC', + '0x68f180fcCe6836688e9084f035309E29Bf0A2095', + 'optimism' + ); + const OP = await deploymentManager.existing( + 'OP', + '0x4200000000000000000000000000000000000042', + 'optimism' + ); + + const COMP = await deploymentManager.existing( + 'COMP', + '0x7e7d4467112689329f7E06571eD0E8CbAd4910eE', + 'optimism' + ); + + // Import shared contracts from cUSDCv3 + const cometAdmin = await deploymentManager.fromDep('cometAdmin', 'optimism', 'usdc'); + const cometFactory = await deploymentManager.fromDep('cometFactory', 'optimism', 'usdc'); + const $configuratorImpl = await deploymentManager.fromDep('configurator:implementation', 'optimism', 'usdc'); + const configurator = await deploymentManager.fromDep('configurator', 'optimism', 'usdc'); + const rewards = await deploymentManager.fromDep('rewards', 'optimism', 'usdc'); + const bulker = await deploymentManager.fromDep('bulker', 'optimism', 'usdc'); + const localTimelock = await deploymentManager.fromDep('timelock', 'optimism', 'usdc'); + const bridgeReceiver = await deploymentManager.fromDep('bridgeReceiver', 'optimism', 'usdc'); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + + return { + ...deployed, + bridgeReceiver, + bulker, + rewards, + COMP + }; +} diff --git a/deployments/optimism/usdt/migrations/1713012100_configurate_and_ens.ts b/deployments/optimism/usdt/migrations/1713012100_configurate_and_ens.ts new file mode 100644 index 000000000..d457597e7 --- /dev/null +++ b/deployments/optimism/usdt/migrations/1713012100_configurate_and_ens.ts @@ -0,0 +1,316 @@ +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { Contract, ethers } from 'ethers'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { diffState, getCometConfig } from '../../../../plugins/deployment_manager/DiffState'; +import { + calldata, + exp, + getConfigurationStruct, + proposal, +} from '../../../../src/deploy'; +import { expect } from 'chai'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +const ENSRegistryAddress = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; +const opCOMPAddress = '0x7e7d4467112689329f7E06571eD0E8CbAd4910eE'; +const USDTAmountToBridge = exp(10_000, 6); +const cUSDTAddress = '0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9'; +const mainnetUSDTAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'; + +export default migration('1713012100_configurate_and_ens', { + prepare: async (deploymentManager: DeploymentManager) => { + return {}; + }, + + enact: async ( + deploymentManager: DeploymentManager, + govDeploymentManager: DeploymentManager + ) => { + const trace = deploymentManager.tracer(); + const { utils } = ethers; + + const cometFactory = await deploymentManager.fromDep( + 'cometFactory', + 'optimism', + 'usdc' + ); + const { + bridgeReceiver, + timelock: localTimelock, + comet, + cometAdmin, + configurator, + rewards, + USDT + } = await deploymentManager.getContracts(); + + const { + opL1CrossDomainMessenger, + opL1StandardBridge, + governor, + timelock + } = await govDeploymentManager.getContracts(); + + // ENS Setup + // See also: https://docs.ens.domains/contract-api-reference/name-processing + const ENSResolver = await govDeploymentManager.existing( + 'ENSResolver', + ENSResolverAddress + ); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const opChainId = ( + await deploymentManager.hre.ethers.provider.getNetwork() + ).chainId.toString(); + const newMarketObject = { baseSymbol: 'USDT', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse( + await ENSResolver.text(subdomainHash, ENSTextRecordKey) + ); + if (officialMarketsJSON[opChainId]) { + officialMarketsJSON[opChainId].push(newMarketObject); + } else { + officialMarketsJSON[opChainId] = [newMarketObject]; + } + + const configuration = await getConfigurationStruct(deploymentManager); + const setFactoryCalldata = await calldata( + configurator.populateTransaction.setFactory( + comet.address, + cometFactory.address + ) + ); + 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, opCOMPAddress] + ); + + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, + configurator.address, + cometAdmin.address, + rewards.address, + ], + [0, 0, 0, 0], + [ + 'setFactory(address,address)', + '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)', + ], + [ + setFactoryCalldata, + setConfigurationCalldata, + deployAndUpgradeToCalldata, + setRewardConfigCalldata, + ], + ] + ); + + const USDTMainnet = new Contract( + mainnetUSDTAddress, + [ + 'function balanceOf(address account) external view returns (uint256)', + 'function approve(address,uint256) external' + ], + govDeploymentManager.hre.ethers.provider + ); + + const notEnoughUSDT = (await USDTMainnet.balanceOf(timelock.address)).lt(USDTAmountToBridge); + const amountToSupply = notEnoughUSDT ? ethers.BigNumber.from(USDTAmountToBridge).sub(await USDTMainnet.balanceOf(timelock.address)) : 0; + const _reduceReservesCalldata = utils.defaultAbiCoder.encode( + ['uint256'], + [amountToSupply] + ); + + // COMP speeds for rewards are setted, but the bridge of tokens does not happen, because it was bridged in the USDC market proposal + + const actions = [ + // 1. Set Comet configuration + deployAndUpgradeTo new Comet, set Reward Config on Optimism + { + contract: opL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalData, 3_000_000], + }, + // 2. Get USDT reserves from cUSDT contract + { + target: cUSDTAddress, + signature: '_reduceReserves(uint256)', + calldata: _reduceReservesCalldata + }, + // 3. Approve USDT to L1StandardBridge + { + contract: USDTMainnet, + signature: 'approve(address,uint256)', + args: [opL1StandardBridge.address, USDTAmountToBridge], + }, + // 4. Bridge USDT from Ethereum to OP Comet using L1StandardBridge + { + contract: opL1StandardBridge, + // function depositERC20To(address _l1Token, address _l2Token, address _to, uint256 _amount, uint32 _l2Gas,bytes calldata _data) + signature: + 'depositERC20To(address,address,address,uint256,uint32,bytes)', + args: [ + USDTMainnet.address, + USDT.address, + comet.address, + USDTAmountToBridge, + 200_000, + '0x', + ], + }, + // 5. 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(officialMarketsJSON)] + ), + }, + ]; + + // the description has speeds. speeds will be set up on on-chain proposal + const description = "# Initialize cUSDTv3 on Optimism\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes the deployment of Compound III to the Optimism network. This proposal takes the governance steps recommended and necessary to initialize a Compound III USDT market on Optimism; upon execution, cUSDTv3 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). The new parameters include setting the risk parameters based on the [recommendations from Gauntlet](https://www.comp.xyz/t/deploy-compound-iii-on-optimism/4975/6).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/848) and [forum discussion](https://www.comp.xyz/t/deploy-compound-iii-on-optimism/4975).\n\n\n## Proposal Actions\n\nThe first proposal action sets the Comet configuration and deploys a new Comet implementation on Optimism. This sends the encoded `setFactory`, `setConfiguration` and `deployAndUpgradeTo` calls across the bridge to the governance receiver on Optimism. It also calls `setRewardConfig` on the Optimism rewards contract, to establish Optimism’s bridged version of COMP as the reward token for the deployment and set the initial supply speed to be 5 COMP/day and borrow speed to be 5 COMP/day.\n\nThe second action reduces Compound [cUSDT](https://etherscan.io/address/0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9) reserves to Timelock, in order to seed the market reserves through the Optimism L1StandardBridge.\n\nThe third action approves Optimism’s [L1StandardBridge](https://etherscan.io/address/0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1) to take Timelock's USDT, in order to seed the reserves through the bridge.\n\nThe fourth action deposits 10K USDT from mainnet to the Optimism L1StandardBridge contract to bridge to Comet.\n\nThe fifth 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 Optimism cUSDTv3 market"; + const txn = await govDeploymentManager.retry(async () =>{ + return trace(await governor.propose(...(await proposal(actions, description)))); + } + ); + + const event = txn.events.find((event) => event.event === 'ProposalCreated'); + const [proposalId] = event.args; + + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return false; + }, + + async verify(deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, preMigrationBlockNumber: number) { + const ethers = deploymentManager.hre.ethers; + + const { + comet, + rewards, + WBTC, + WETH, + OP, + COMP + } = await deploymentManager.getContracts(); + + const { + timelock + } = await govDeploymentManager.getContracts(); + + const stateChanges = await diffState(comet, getCometConfig, preMigrationBlockNumber); + + // uncomment on on-chain proposal PR + // expect(stateChanges).to.deep.equal({ + // WBTC: { + // supplyCap: exp(400, 8) + // }, + // WETH: { + // supplyCap: exp(11_000, 18) + // }, + // OP: { + // supplyCap: exp(10_000_000, 18) + // }, + // baseTrackingSupplySpeed: exp(5 / 86400, 15, 18), + // baseTrackingBorrowSpeed: exp(5 / 86400, 15, 18), + // }); + + const config = await rewards.rewardConfig(comet.address); + expect(config.token).to.be.equal(COMP.address); + expect(config.rescaleFactor).to.be.equal(exp(1, 12)); + expect(config.shouldUpscale).to.be.equal(true); + + // 2. & 3 & 4. + expect(await comet.getReserves()).to.be.equal(USDTAmountToBridge); + + // 5. + 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', + }, + ], + 8453: [ + { + baseSymbol: 'USDbC', + cometAddress: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + }, + { + baseSymbol: 'WETH', + cometAddress: '0x46e6b214b524310239732D51387075E0e70970bf', + }, + { + baseSymbol: 'USDC', + cometAddress: '0xb125E6687d4313864e53df431d5425969c15Eb2F', + }, + ], + 42161: [ + { + baseSymbol: 'USDC.e', + cometAddress: '0xA5EDBDD9646f8dFF606d7448e414884C7d905dCA', + }, + { + baseSymbol: 'USDC', + cometAddress: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + }, + ], + 534352: [ + { + baseSymbol: 'USDC', + cometAddress: '0xB2f97c1Bd3bf02f5e74d13f02E3e26F93D77CE44', + }, + ], + 10: [ + { + baseSymbol: 'USDC', + cometAddress: '0x2e44e174f7D53F0212823acC11C01A11d58c5bCB', + }, + { + baseSymbol: 'USDT', + cometAddress: comet.address, + }, + ], + }); + } +}); \ No newline at end of file diff --git a/deployments/optimism/usdt/relations.ts b/deployments/optimism/usdt/relations.ts new file mode 100644 index 000000000..281057307 --- /dev/null +++ b/deployments/optimism/usdt/relations.ts @@ -0,0 +1,30 @@ +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + governor: { + artifact: + 'contracts/bridges/optimism/OptimismBridgeReceiver.sol:OptimismBridgeReceiver', + }, + Proxy: { + artifact: 'contracts/ERC20.sol:ERC20', + }, + + l2CrossDomainMessenger: { + delegates: { + field: { + slot: + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + }, + }, + }, + + l2StandardBridge: { + delegates: { + field: { + slot: + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + }, + }, + }, +}; \ No newline at end of file diff --git a/deployments/optimism/usdt/roots.json b/deployments/optimism/usdt/roots.json new file mode 100644 index 000000000..1b7b5cb67 --- /dev/null +++ b/deployments/optimism/usdt/roots.json @@ -0,0 +1,11 @@ +{ + "l2CrossDomainMessenger": "0x4200000000000000000000000000000000000007", + "l2StandardBridge": "0x4200000000000000000000000000000000000010", + "CCTPMessageTransmitter": "0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8", + "comet": "0x995E394b8B2437aC8Ce61Ee0bC610D617962B214", + "configurator": "0x84E93EC6170ED630f5ebD89A1AAE72d4F63f2713", + "rewards": "0x443EA0340cb75a160F31A440722dec7b5bc3C2E9", + "bridgeReceiver": "0xC3a73A70d1577CD5B02da0bA91C0Afc8fA434DAF", + "bulker": "0xcb3643CC8294B23171272845473dEc49739d4Ba3", + "COMP": "0x7e7d4467112689329f7E06571eD0E8CbAd4910eE" +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 284c8cb5c..a78992283 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -37,6 +37,7 @@ import baseGoerliRelationConfigMap from './deployments/base-goerli/usdc/relation import baseGoerliWethRelationConfigMap from './deployments/base-goerli/weth/relations'; import lineaGoerliRelationConfigMap from './deployments/linea-goerli/usdc/relations'; import optimismRelationConfigMap from './deployments/optimism/usdc/relations'; +import optimismUsdtRelationConfigMap from './deployments/optimism/usdt/relations'; import scrollGoerliRelationConfigMap from './deployments/scroll-goerli/usdc/relations'; import scrollRelationConfigMap from './deployments/scroll/usdc/relations'; @@ -372,7 +373,8 @@ const config: HardhatUserConfig = { usdc: lineaGoerliRelationConfigMap }, optimism: { - usdc: optimismRelationConfigMap + usdc: optimismRelationConfigMap, + usdt: optimismUsdtRelationConfigMap }, 'scroll-goerli': { usdc: scrollGoerliRelationConfigMap @@ -504,6 +506,12 @@ const config: HardhatUserConfig = { deployment: 'usdc', auxiliaryBase: 'mainnet' }, + { + name: 'optimism-usdt', + network: 'optimism', + deployment: 'usdt', + auxiliaryBase: 'mainnet', + }, { name: 'scroll-goerli', network: 'scroll-goerli', diff --git a/scenario/BulkerScenario.ts b/scenario/BulkerScenario.ts index 91e2608f8..a73d84d2c 100644 --- a/scenario/BulkerScenario.ts +++ b/scenario/BulkerScenario.ts @@ -165,6 +165,7 @@ scenario( filter: async (ctx) => await isBulkerSupported(ctx) && await isRewardSupported(ctx) && !matchesDeployment(ctx, [{deployment: 'weth'}, { network: 'linea-goerli' }]), supplyCaps: { $asset0: 3000, + $asset1: 3000, }, tokenBalances: { albert: { $base: '== 1000000', $asset0: 3000 }, @@ -177,7 +178,10 @@ scenario( const baseAssetAddress = await comet.baseToken(); const baseAsset = context.getAssetByAddress(baseAssetAddress); const baseScale = (await comet.baseScale()).toBigInt(); - const { asset: collateralAssetAddress, scale: scaleBN } = await comet.getAssetInfo(0); + // if asset 0 is native token we took asset 1 + const { asset: asset0, scale: scale0 } = await comet.getAssetInfo(0); + const { asset: asset1, scale: scale1 } = await comet.getAssetInfo(1); + const { asset: collateralAssetAddress, scale: scaleBN } = asset0 === wrappedNativeToken ? { asset: asset1, scale: scale1 } : { asset: asset0, scale: scale0 }; const collateralAsset = context.getAssetByAddress(collateralAssetAddress); const collateralScale = scaleBN.toBigInt(); const [rewardTokenAddress] = await rewards.rewardConfig(comet.address); From 854ec58acc56e000f7e201cc16c2aa363dde904c Mon Sep 17 00:00:00 2001 From: Dmitriy Babenko <159453675+dmitriy-woof-software@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:53:32 +0200 Subject: [PATCH 2/3] On-chain Compound v3 WETH Arbitrum market (#860) Co-authored-by: Mikhailo Shabodyash <67977488+EviLord032@users.noreply.github.com> Co-authored-by: GitHub Actions Bot <> Co-authored-by: Mikhailo Shabodyash --- .github/workflows/run-scenarios.yaml | 2 +- deployments/arbitrum/weth/configuration.json | 54 ++++ deployments/arbitrum/weth/deploy.ts | 71 +++++ .../1716912328_configure_and_ens.ts | 296 ++++++++++++++++++ deployments/arbitrum/weth/relations.ts | 53 ++++ deployments/arbitrum/weth/roots.json | 8 + hardhat.config.ts | 12 +- scenario/utils/relayArbitrumMessage.ts | 20 ++ .../liquidateUnderwaterBorrowers.ts | 35 ++- src/deploy/index.ts | 14 +- 10 files changed, 559 insertions(+), 6 deletions(-) create mode 100644 deployments/arbitrum/weth/configuration.json create mode 100644 deployments/arbitrum/weth/deploy.ts create mode 100644 deployments/arbitrum/weth/migrations/1716912328_configure_and_ens.ts create mode 100644 deployments/arbitrum/weth/relations.ts create mode 100644 deployments/arbitrum/weth/roots.json diff --git a/.github/workflows/run-scenarios.yaml b/.github/workflows/run-scenarios.yaml index 9c2990382..0d9805358 100644 --- a/.github/workflows/run-scenarios.yaml +++ b/.github/workflows/run-scenarios.yaml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, arbitrum-usdc.e, arbitrum-usdc, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-goerli, base-goerli-weth, linea-goerli, optimism-usdc, optimism-usdt, scroll-goerli, scroll-usdc] + bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, arbitrum-usdc.e, arbitrum-usdc, arbitrum-weth, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-goerli, base-goerli-weth, linea-goerli, optimism-usdc, optimism-usdt, scroll-goerli, scroll-usdc] name: Run scenarios env: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }} diff --git a/deployments/arbitrum/weth/configuration.json b/deployments/arbitrum/weth/configuration.json new file mode 100644 index 000000000..aacebfc58 --- /dev/null +++ b/deployments/arbitrum/weth/configuration.json @@ -0,0 +1,54 @@ +{ + "name": "Compound WETH", + "symbol": "cWETHv3", + "baseToken": "WETH", + "baseTokenAddress": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "borrowMin": "0.1e18", + "governor": "0x3fB4d38ea7EC20D91917c09591490Eeda38Cf88A", + "pauseGuardian": "0x78E6317DD6D43DdbDa00Dce32C2CbaFc99361a9d", + "rewardTokenAddress": "0x354A6dA3fcde098F8389cad84b0182725c6C91dE", + "storeFrontPriceFactor": 0.7, + "targetReserves": "5000e18", + "rates": { + "supplyKink": 0.85, + "supplySlopeLow": 0.0185, + "supplySlopeHigh": 1, + "supplyBase": 0, + "borrowKink": 0.85, + "borrowSlopeLow": 0.014, + "borrowSlopeHigh": 1.15, + "borrowBase": 0.01 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "69444444444e0", + "baseBorrowSpeed": "46296296296e0", + "baseMinForRewards": "1000e18" + }, + "assets": { + "weETH": { + "address": "0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe", + "decimals": "18", + "borrowCF": 0.82, + "liquidateCF": 0.87, + "liquidationFactor": 0.92, + "supplyCap": "550e18" + }, + "rETH": { + "address": "0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8", + "decimals": "18", + "borrowCF": 0.90, + "liquidateCF": 0.93, + "liquidationFactor": 0.97, + "supplyCap": "800e18" + }, + "wstETH": { + "address": "0x5979D7b546E38E414F7E9822514be443A4800529", + "decimals": "18", + "borrowCF": 0.88, + "liquidateCF": 0.93, + "liquidationFactor": 0.97, + "supplyCap": "2000e18" + } + } +} \ No newline at end of file diff --git a/deployments/arbitrum/weth/deploy.ts b/deployments/arbitrum/weth/deploy.ts new file mode 100644 index 000000000..661241d30 --- /dev/null +++ b/deployments/arbitrum/weth/deploy.ts @@ -0,0 +1,71 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet, exp } from '../../../src/deploy'; + +export default async function deploy(deploymentManager: DeploymentManager, deploySpec: DeploySpec): Promise { + const WETH = await deploymentManager.existing('WETH', '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', 'arbitrum'); + const rETH = await deploymentManager.existing('rETH', '0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8', 'arbitrum'); + const wstETH = await deploymentManager.existing('wstETH', '0x5979D7b546E38E414F7E9822514be443A4800529', 'arbitrum'); + const COMP = await deploymentManager.existing('COMP', '0x354A6dA3fcde098F8389cad84b0182725c6C91dE', 'arbitrum'); + + // Deploy WstETHPriceFeed + const wstETHPriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'pricefeeds/ScalingPriceFeed.sol', + [ + '0xb523AE262D20A936BC152e6023996e46FDC2A95D', // wstETH / ETH price feed + 8 // decimals + ] + ); + + // Deploy constant price feed for WETH + const wethConstantPriceFeed = await deploymentManager.deploy( + 'WETH:priceFeed', + 'pricefeeds/ConstantPriceFeed.sol', + [ + 8, // decimals + exp(1, 8) // constantPrice + ] + ); + + // Deploy scaling price feed for rETH + const rETHScalingPriceFeed = await deploymentManager.deploy( + 'rETH:priceFeed', + 'pricefeeds/ScalingPriceFeed.sol', + [ + '0xD6aB2298946840262FcC278fF31516D39fF611eF', // rETH / ETH price feed + 8 // decimals + ] + ); + + // Deploy scaling price feed for weETH + const weETHScalingPriceFeed = await deploymentManager.deploy( + 'weETH:priceFeed', + 'pricefeeds/ScalingPriceFeed.sol', + [ + '0xE141425bc1594b8039De6390db1cDaf4397EA22b', // weETH / ETH price feed + 8 // decimals + ] + ); + + // Import shared contracts from cUSDCv3 + const cometAdmin = await deploymentManager.fromDep('cometAdmin', 'arbitrum', 'usdc.e'); + const cometFactory = await deploymentManager.fromDep('cometFactory', 'arbitrum', 'usdc.e'); + const $configuratorImpl = await deploymentManager.fromDep('configurator:implementation', 'arbitrum', 'usdc.e'); + const configurator = await deploymentManager.fromDep('configurator', 'arbitrum', 'usdc.e'); + const rewards = await deploymentManager.fromDep('rewards', 'arbitrum', 'usdc.e'); + const bulker = await deploymentManager.fromDep('bulker', 'arbitrum', 'usdc.e'); + const localTimelock = await deploymentManager.fromDep('timelock', 'arbitrum', 'usdc.e'); + const bridgeReceiver = await deploymentManager.fromDep('bridgeReceiver', 'arbitrum', 'usdc.e'); + + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + + return { + ...deployed, + bridgeReceiver, + bulker, + rewards, + COMP + }; +} \ No newline at end of file diff --git a/deployments/arbitrum/weth/migrations/1716912328_configure_and_ens.ts b/deployments/arbitrum/weth/migrations/1716912328_configure_and_ens.ts new file mode 100644 index 000000000..57c70b163 --- /dev/null +++ b/deployments/arbitrum/weth/migrations/1716912328_configure_and_ens.ts @@ -0,0 +1,296 @@ +import { Contract, ethers } from 'ethers'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, getConfigurationStruct, proposal } from '../../../../src/deploy'; +import { expect } from 'chai'; +import { applyL1ToL2Alias, estimateL2Transaction, estimateTokenBridge } from '../../../../scenario/utils/arbitrumUtils'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +const ENSRegistryAddress = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; + +const WETHAmountToBridge = ethers.BigNumber.from(exp(10, 18)); +const arbitrumCOMPAddress = '0x354A6dA3fcde098F8389cad84b0182725c6C91dE'; + +export default migration('1713517203_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 cometFactory = await deploymentManager.fromDep('cometFactory', 'arbitrum', 'usdc.e'); + const { + bridgeReceiver, + timelock: l2Timelock, + comet, + cometAdmin, + configurator, + rewards + } = await deploymentManager.getContracts(); + const { + arbitrumInbox, + arbitrumL1GatewayRouter, + timelock, + governor, + WETH, + COMP + } = await govDeploymentManager.getContracts(); + const refundAddress = l2Timelock.address; + const wethGatewayAddress = await arbitrumL1GatewayRouter.getGateway(WETH.address); + + const wethGasParams = await estimateTokenBridge( + { + token: COMP.address, + from: timelock.address, + to: comet.address, + amount: WETHAmountToBridge.toBigInt() + }, + govDeploymentManager, + deploymentManager + ); + + const configuration = await getConfigurationStruct(deploymentManager); + const setFactoryCalldata = await calldata( + configurator.populateTransaction.setFactory(comet.address, cometFactory.address) + ); + 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, configurator.address, cometAdmin.address, rewards.address], + [0, 0, 0, 0], + [ + 'setFactory(address,address)', + '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)' + ], + [ + setFactoryCalldata, + setConfigurationCalldata, + deployAndUpgradeToCalldata, + setRewardConfigCalldata + ] + ] + ); + + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const polygonChainId = (await deploymentManager.hre.ethers.provider.getNetwork()).chainId.toString(); + const newMarketObject = { baseSymbol: 'WETH', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse(await ENSResolver.text(subdomainHash, ENSTextRecordKey)); + + if (officialMarketsJSON[polygonChainId]) { + officialMarketsJSON[polygonChainId].push(newMarketObject); + } else { + officialMarketsJSON[polygonChainId] = [newMarketObject]; + } + + const createRetryableTicketGasParams = await estimateL2Transaction( + { + from: applyL1ToL2Alias(timelock.address), + to: bridgeReceiver.address, + data: l2ProposalData + }, + deploymentManager + ); + + const outboundTransferCalldata = utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes'], + [ + WETH.address, + comet.address, + WETHAmountToBridge.toBigInt(), + wethGasParams.gasLimit, + wethGasParams.maxFeePerGas, + utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [wethGasParams.maxSubmissionCost, '0x'] + ) + ] + ); + + 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. Wrap some ETH as WETH + { + contract: WETH, + signature: 'deposit()', + args: [], + value: WETHAmountToBridge, + }, + // 3. Approve the WETH gateway to take Timelock's WETH for bridging + { + contract: WETH, + signature: 'approve(address,uint256)', + args: [wethGatewayAddress, WETHAmountToBridge] + }, + // 4. Bridge WETH from mainnet to Arbitrum Comet + { + target: arbitrumL1GatewayRouter.address, + signature: 'outboundTransfer(address,address,uint256,uint256,uint256,bytes)', + calldata: outboundTransferCalldata, + value: wethGasParams.deposit + }, + // 5. 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(officialMarketsJSON)] + ) + } + ]; + + const description = "# Initialize cWETHv3 on Arbitrum\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes deployment of Compound III to the Arbitrum network. This proposal takes the governance steps recommended and necessary to initialize a Compound III WETH market on Arbitrum; upon execution, cWETHv3 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). The new parameters include setting the risk parameters based off of the [recommendations from Gauntlet](https://www.comp.xyz/t/add-eth-market-on-arbitrum/5252/2).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/860), [deploy market GitHub action run](https://github.com/woof-software/comet/actions/runs/9419069237/job/25948008428) and [forum discussion](https://www.comp.xyz/t/add-eth-market-on-arbitrum/5252).\n\nThe market migration to create a new WETH Comet market on Arbitrum also includes a new collateral asset weETH which has not been used in any other Compound market previously. In prior analysis of weETH as a collateral asset, Gauntlet identified oracle risks which [could expose the protocol to exaggerated market movements](https://www.comp.xyz/t/add-weeth-market-on-ethereum/5179/3#oracle-risk-7) and a yield risk which could [cause yield shocks and consequentially elevate slippage magnitude and liquidity on DEXs](https://www.comp.xyz/t/add-weeth-market-on-ethereum/5179/3#yield-risk-8).\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 `setFactory`, `setConfiguration`, `deployAndUpgradeTo` calls across the bridge to the governance receiver on Arbitrum. It also calls `setRewardConfig` on the Arbitrum rewards contract, to establish Artitrum’s bridged version of COMP as the reward token for the deployment and set the initial supply speed to be 6 COMP/day and borrow speed to be 4 COMP/day.\n\nThe second action wraps ETH as WETH so it can be then transferred.\n\nThe third action approves (ArbitrumL1GatewayRouter) [TokenMessenger](https://etherscan.io/address/0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef) to take the Timelock's WETH on Mainnet, in order to seed the market reserves through the arbitrumL1GatewayRouter.\n\nThe fourth action bridges WETH from mainnet via ‘outboundTransfer’ function on ArbitrumL1GatewayRouter’s contract to mint native WETH to Comet on Arbitrum.\n\nThe fifth 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 cWETHv3 market.\n"; + 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) { + const ethers = deploymentManager.hre.ethers; + + const { + comet, + rewards, + wstETH, + rETH, + weETH + } = await deploymentManager.getContracts(); + + const { + timelock + } = await govDeploymentManager.getContracts(); + + // 1. + const weETHInfo = await comet.getAssetInfoByAddress(weETH.address); + const rETHInfo = await comet.getAssetInfoByAddress(rETH.address); + const wstETHInfo = await comet.getAssetInfoByAddress(wstETH.address); + expect(weETHInfo.supplyCap).to.be.eq(exp(550, 18)); + expect(wstETHInfo.supplyCap).to.be.eq(exp(2000, 18)); + expect(rETHInfo.supplyCap).to.be.eq(exp(800, 18)); + + expect(await comet.pauseGuardian()).to.be.eq('0x78E6317DD6D43DdbDa00Dce32C2CbaFc99361a9d'); + + // 2. & 3. & 4. + expect(await comet.getReserves()).to.be.equal(WETHAmountToBridge); + + // 5. + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const ENSRegistry = await govDeploymentManager.existing('ENSRegistry', ENSRegistryAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + const officialMarkets = JSON.parse(officialMarketsJSON); + expect(await ENSRegistry.recordExists(subdomainHash)).to.be.equal(true); + expect(await ENSRegistry.owner(subdomainHash)).to.be.equal(timelock.address); + expect(await ENSRegistry.resolver(subdomainHash)).to.be.equal(ENSResolverAddress); + expect(await ENSRegistry.ttl(subdomainHash)).to.be.equal(0); + expect(officialMarkets).to.deep.equal({ + 1: [ + { + baseSymbol: 'USDC', + cometAddress: '0xc3d688B66703497DAA19211EEdff47f25384cdc3', + }, + { + baseSymbol: 'WETH', + cometAddress: '0xA17581A9E3356d9A858b789D68B4d866e593aE94', + }, + ], + 137: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF25212E676D1F7F89Cd72fFEe66158f541246445', + }, + ], + 8453: [ + { + baseSymbol: 'USDbC', + cometAddress: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + }, + { + baseSymbol: 'WETH', + cometAddress: '0x46e6b214b524310239732D51387075E0e70970bf', + }, + { + baseSymbol: 'USDC', + cometAddress: '0xb125E6687d4313864e53df431d5425969c15Eb2F', + }, + ], + 42161: [ + { + baseSymbol: 'USDC.e', + cometAddress: '0xA5EDBDD9646f8dFF606d7448e414884C7d905dCA', + }, + { + baseSymbol: 'USDC', + cometAddress: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + }, + { + baseSymbol: 'WETH', + cometAddress: comet.address, + }, + ], + 534352: [ + { + baseSymbol: 'USDC', + cometAddress: '0xB2f97c1Bd3bf02f5e74d13f02E3e26F93D77CE44', + }, + ], + 10: [ + { + baseSymbol: 'USDC', + cometAddress: '0x2e44e174f7D53F0212823acC11C01A11d58c5bCB', + }, + ], + }); + + // 8. + expect(await comet.baseTrackingSupplySpeed()).to.be.equal(exp(6 / 86400, 15, 18)); + expect(await comet.baseTrackingBorrowSpeed()).to.be.equal(exp(4 / 86400, 15, 18)); + } +}); \ No newline at end of file diff --git a/deployments/arbitrum/weth/relations.ts b/deployments/arbitrum/weth/relations.ts new file mode 100644 index 000000000..b42cc0bf2 --- /dev/null +++ b/deployments/arbitrum/weth/relations.ts @@ -0,0 +1,53 @@ +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + 'governor': { + artifact: 'contracts/bridges/polygon/PolygonBridgeReceiver.sol:PolygonBridgeReceiver', + }, + TransparentUpgradeableProxy: { + artifact: 'contracts/ERC20.sol:ERC20' + }, + OssifiableProxy: { + artifact: 'contracts/ERC20.sol:ERC20' + }, + ClonableBeaconProxy: { + artifact: 'contracts/ERC20.sol:ERC20' + }, + // WETH + '0x82af49447d8a07e3bd95bd0d56f35241523fbab1': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + // rETH + '0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + // wstETH + '0x5979D7b546E38E414F7E9822514be443A4800529': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + // weETH + '0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + } +}; \ No newline at end of file diff --git a/deployments/arbitrum/weth/roots.json b/deployments/arbitrum/weth/roots.json new file mode 100644 index 000000000..b82141555 --- /dev/null +++ b/deployments/arbitrum/weth/roots.json @@ -0,0 +1,8 @@ +{ + "comet": "0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486", + "configurator": "0xb21b06D71c75973babdE35b49fFDAc3F82Ad3775", + "rewards": "0x88730d254A2f7e6AC8388c3198aFd694bA9f7fae", + "bridgeReceiver": "0x42480C37B249e33aABaf4c22B20235656bd38068", + "bulker": "0xbdE8F31D2DdDA895264e27DD990faB3DC87b372d", + "COMP": "0x354A6dA3fcde098F8389cad84b0182725c6C91dE" +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index a78992283..02b603d6e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -28,6 +28,7 @@ import mainnetWethRelationConfigMap from './deployments/mainnet/weth/relations'; import polygonRelationConfigMap from './deployments/polygon/usdc/relations'; import arbitrumBridgedUsdcRelationConfigMap from './deployments/arbitrum/usdc.e/relations'; import arbitrumNativeUsdcRelationConfigMap from './deployments/arbitrum/usdc/relations'; +import arbitrumWETHRelationConfigMap from './deployments/arbitrum/weth/relations'; import arbitrumBridgedUsdcGoerliRelationConfigMap from './deployments/arbitrum-goerli/usdc.e/relations'; import arbitrumGoerliNativeUsdcRelationConfigMap from './deployments/arbitrum-goerli/usdc/relations'; import baseUsdbcRelationConfigMap from './deployments/base/usdbc/relations'; @@ -354,7 +355,8 @@ const config: HardhatUserConfig = { }, arbitrum: { 'usdc.e': arbitrumBridgedUsdcRelationConfigMap, - usdc: arbitrumNativeUsdcRelationConfigMap + usdc: arbitrumNativeUsdcRelationConfigMap, + weth: arbitrumWETHRelationConfigMap }, 'arbitrum-goerli': { 'usdc.e': arbitrumBridgedUsdcGoerliRelationConfigMap, @@ -452,6 +454,12 @@ const config: HardhatUserConfig = { deployment: 'usdc', auxiliaryBase: 'mainnet' }, + { + name: 'arbitrum-weth', + network: 'arbitrum', + deployment: 'weth', + auxiliaryBase: 'mainnet' + }, { name: 'arbitrum-goerli-usdc.e', network: 'arbitrum-goerli', @@ -559,4 +567,4 @@ const config: HardhatUserConfig = { setupDefaultNetworkProviders(config); -export default config; +export default config; \ No newline at end of file diff --git a/scenario/utils/relayArbitrumMessage.ts b/scenario/utils/relayArbitrumMessage.ts index 4929b9533..287ec7b5d 100644 --- a/scenario/utils/relayArbitrumMessage.ts +++ b/scenario/utils/relayArbitrumMessage.ts @@ -3,6 +3,7 @@ import { impersonateAddress } from '../../plugins/scenario/utils'; import { setNextBaseFeeToZero, setNextBlockTimestamp } from './hreUtils'; import { utils, BigNumber } from 'ethers'; import { Log } from '@ethersproject/abstract-provider'; +import { sourceTokens } from '../../plugins/scenario/utils/TokenSourcer'; export async function relayArbitrumMessage( governanceDeploymentManager: DeploymentManager, @@ -81,6 +82,25 @@ export async function relayArbitrumMessage( bridgeDeploymentManager, sender ); + // if method name == finalizeInboundTransfer(address,address,address,uint256,bytes) + if(data.slice(0, 10) == '0x2e567b36'){ + const _data = '0x' + data.slice(10, 266); + const [token,, to, amount] = utils.defaultAbiCoder.decode( + ['address', 'address', 'address', 'uint256'], + _data + ); + // if token is mainnet ETH -> than source arbitrum weth + if(token == '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'){ + await sourceTokens({ + dm: bridgeDeploymentManager, + amount: amount, + asset: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', + address: to, + blacklist: [], + }); + continue; + } + } const transactionRequest = await arbitrumSigner.populateTransaction({ to: toAddress, from: sender, diff --git a/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts b/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts index d073fb587..577bf2fef 100644 --- a/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts +++ b/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts @@ -57,7 +57,10 @@ const addresses = { WBTC: '0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f', USDT: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', USDC: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', - USDC_E: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8' + USDC_E: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + rETH: '0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8', + wstETH: '0x5979D7b546E38E414F7E9822514be443A4800529', + weETH: '0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe' } }; @@ -110,6 +113,10 @@ export const flashLoanPools = { tokenAddress: addresses.arbitrum.USDC_E, poolFee: 100 }, + weth: { + tokenAddress: addresses.arbitrum.USDC, + poolFee: 500 + } } }; @@ -233,6 +240,28 @@ export function getPoolConfig(tokenAddress: string) { uniswapPoolFee: 500 } }, + [addresses.arbitrum.rETH.toLowerCase()]: { + ...defaultPoolConfig, + ...{ + exchange: Exchange.Balancer, + balancerPoolId: '0xd0ec47c54ca5e20aaae4616c25c825c7f48d40690000000000000000000004ef' + } + }, + [addresses.arbitrum.wstETH.toLowerCase()]: { + ...defaultPoolConfig, + ...{ + exchange: Exchange.Balancer, + balancerPoolId: '0x9791d590788598535278552eecd4b211bfc790cb000000000000000000000498' + } + }, + [addresses.arbitrum.weETH.toLowerCase()]: { + ...defaultPoolConfig, + ...{ + exchange: Exchange.Uniswap, + swapViaWeth: false, + uniswapPoolFee: 100 + } + }, }; const poolConfig = poolConfigs[tokenAddress.toLowerCase()]; @@ -260,7 +289,9 @@ function getMaxAmountToPurchase(tokenAddress: string): bigint { [addresses.arbitrum.ARB.toLowerCase()]: exp(500000, 18), [addresses.arbitrum.GMX.toLowerCase()]: exp(4000, 18), [addresses.arbitrum.WETH.toLowerCase()]: exp(2000, 18), - [addresses.arbitrum.WBTC.toLowerCase()]: exp(100, 8) + [addresses.arbitrum.WBTC.toLowerCase()]: exp(100, 8), + [addresses.arbitrum.rETH.toLowerCase()]: exp(2000, 18), + [addresses.arbitrum.wstETH.toLowerCase()]: exp(2000, 18) }; const max = maxAmountsToPurchase[tokenAddress.toLowerCase()]; diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 209bfff19..f912c38bf 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -96,9 +96,21 @@ export const WHALES = { '0x167384319b41f7094e62f7506409eb38079abff8' // WMATIC whale ], arbitrum: [ + '0x8eb270e296023e9d92081fdf967ddd7878724424', // rETH whale + '0x78e88887d80451cb08fdc4b9046c9d01fb8d048d', // rETH whale + '0xc0cf4b266be5b3229c49590b59e67a09c15b22f4', // rETH whale + '0x84446698694b348eaece187b55df06ab4ce72b35', // rETH whale + '0x42c248d137512907048021b30d9da17f48b5b7b2', // wstETH whale + '0xc3e5607cd4ca0d5fe51e09b60ed97a0ae6f874dd', // WETH whale '0xf89d7b9c864f589bbf53a82105107622b35eaa40', // USDC whale '0x7b7b957c284c2c227c980d6e2f804311947b84d0', // WBTC whale - '0x88730d254A2f7e6AC8388c3198aFd694bA9f7fae', // COMP whale + '0x1c6b5795be43ddff8812b3e577ac752764635bc5', // COMP whale + '0xdead767ba9f8072c986a4619c27ae46bcc226c13', // COMP whale + '0xde5167c19a5286889752cb0f31a1c7f28a99fefb', // COMP whale + '0xdfa19e743421c394d904f5a113121c2227d2364b', // COMP whale + '0xee3273f6d29ddfff08ffd9d513cff314734f01a2', // COMP whale + '0x9e786a8fc88ee74b758b125071d45853356024c3', // COMP whale + '0xd93f76944e870900779c09ddf1c46275f9d8bf9b', // COMP whale '0xe68ee8a12c611fd043fb05d65e1548dc1383f2b9' // native USDC whale ], base: [ From 5f9d5a47c799631a6cb82b6c671adc15e2285962 Mon Sep 17 00:00:00 2001 From: MishaShWoof Date: Mon, 1 Jul 2024 20:15:15 +0300 Subject: [PATCH 3/3] On-chain Compound v3 USDT Arbitrum market (#864) Co-authored-by: dmitriy-bergman-works Co-authored-by: GitHub Actions Bot <> Co-authored-by: dmitriy-woof-software --- .github/workflows/run-scenarios.yaml | 2 +- contracts/Comet.sol | 125 +++++-- contracts/CometCore.sol | 7 + contracts/CometMainInterface.sol | 1 + contracts/IERC20NonStandard.sol | 31 +- contracts/test/EvilToken.sol | 35 +- contracts/test/FaucetToken.sol | 4 +- contracts/test/NonStandardFaucetFeeToken.sol | 104 ++++++ deployments/arbitrum/usdt/configuration.json | 73 ++++ deployments/arbitrum/usdt/deploy.ts | 51 +++ .../1717936901_configurate_and_end.ts | 345 ++++++++++++++++++ deployments/arbitrum/usdt/relations.ts | 33 ++ deployments/arbitrum/usdt/roots.json | 8 + deployments/mainnet/usdc/roots.json | 2 +- hardhat.config.ts | 8 + package.json | 2 +- scenario/ApproveThisScenario.ts | 2 +- scenario/BulkerScenario.ts | 5 +- scenario/LiquidationBotScenario.ts | 32 +- scenario/LiquidationScenario.ts | 70 ++++ scenario/SupplyScenario.ts | 197 ++++++++++ scenario/constraints/ProposalConstraint.ts | 6 + scenario/utils/relayArbitrumMessage.ts | 8 +- .../liquidateUnderwaterBorrowers.ts | 7 +- test/buy-collateral-test.ts | 170 ++++++++- test/comet-ext-test.ts | 4 +- test/helpers.ts | 13 +- test/supply-test.ts | 139 ++++++- test/update-assets-in-test.ts | 11 +- test/withdraw-test.ts | 18 +- yarn.lock | 9 +- 31 files changed, 1414 insertions(+), 108 deletions(-) create mode 100644 contracts/test/NonStandardFaucetFeeToken.sol create mode 100644 deployments/arbitrum/usdt/configuration.json create mode 100644 deployments/arbitrum/usdt/deploy.ts create mode 100644 deployments/arbitrum/usdt/migrations/1717936901_configurate_and_end.ts create mode 100644 deployments/arbitrum/usdt/relations.ts create mode 100644 deployments/arbitrum/usdt/roots.json diff --git a/.github/workflows/run-scenarios.yaml b/.github/workflows/run-scenarios.yaml index 0d9805358..30fb925d4 100644 --- a/.github/workflows/run-scenarios.yaml +++ b/.github/workflows/run-scenarios.yaml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, arbitrum-usdc.e, arbitrum-usdc, arbitrum-weth, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-goerli, base-goerli-weth, linea-goerli, optimism-usdc, optimism-usdt, scroll-goerli, scroll-usdc] + bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, arbitrum-usdc.e, arbitrum-usdc, arbitrum-weth, arbitrum-usdt, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-goerli, base-goerli-weth, linea-goerli, optimism-usdc, optimism-usdt, scroll-goerli, scroll-usdc] name: Run scenarios env: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }} diff --git a/contracts/Comet.sol b/contracts/Comet.sol index 3c5d56442..e0f9bcf89 100644 --- a/contracts/Comet.sol +++ b/contracts/Comet.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; import "./CometMainInterface.sol"; -import "./ERC20.sol"; +import "./IERC20NonStandard.sol"; import "./IPriceFeed.sol"; /** @@ -126,12 +126,6 @@ contract Comet is CometMainInterface { uint256 internal immutable asset10_b; uint256 internal immutable asset11_a; uint256 internal immutable asset11_b; - uint256 internal immutable asset12_a; - uint256 internal immutable asset12_b; - uint256 internal immutable asset13_a; - uint256 internal immutable asset13_b; - uint256 internal immutable asset14_a; - uint256 internal immutable asset14_b; /** * @notice Construct a new protocol instance @@ -139,7 +133,7 @@ contract Comet is CometMainInterface { **/ constructor(Configuration memory config) { // Sanity checks - uint8 decimals_ = ERC20(config.baseToken).decimals(); + uint8 decimals_ = IERC20NonStandard(config.baseToken).decimals(); if (decimals_ > MAX_BASE_DECIMALS) revert BadDecimals(); if (config.storeFrontPriceFactor > FACTOR_SCALE) revert BadDiscount(); if (config.assetConfigs.length > MAX_ASSETS) revert TooManyAssets(); @@ -196,9 +190,44 @@ contract Comet is CometMainInterface { (asset09_a, asset09_b) = getPackedAssetInternal(config.assetConfigs, 9); (asset10_a, asset10_b) = getPackedAssetInternal(config.assetConfigs, 10); (asset11_a, asset11_b) = getPackedAssetInternal(config.assetConfigs, 11); - (asset12_a, asset12_b) = getPackedAssetInternal(config.assetConfigs, 12); - (asset13_a, asset13_b) = getPackedAssetInternal(config.assetConfigs, 13); - (asset14_a, asset14_b) = getPackedAssetInternal(config.assetConfigs, 14); + } + + /** + * @dev Prevents marked functions from being reentered + * Note: this restrict contracts from calling comet functions in their hooks. + * Doing so will cause the transaction to revert. + */ + modifier nonReentrant() { + nonReentrantBefore(); + _; + nonReentrantAfter(); + } + + /** + * @dev Checks that the reentrancy flag is not set and then sets the flag + */ + function nonReentrantBefore() internal { + bytes32 slot = REENTRANCY_GUARD_FLAG_SLOT; + uint256 status; + assembly ("memory-safe") { + status := sload(slot) + } + + if (status == REENTRANCY_GUARD_ENTERED) revert ReentrantCallBlocked(); + assembly ("memory-safe") { + sstore(slot, REENTRANCY_GUARD_ENTERED) + } + } + + /** + * @dev Unsets the reentrancy flag + */ + function nonReentrantAfter() internal { + bytes32 slot = REENTRANCY_GUARD_FLAG_SLOT; + uint256 status; + assembly ("memory-safe") { + sstore(slot, REENTRANCY_GUARD_NOT_ENTERED) + } } /** @@ -241,7 +270,7 @@ contract Comet is CometMainInterface { // Sanity check price feed and asset decimals if (IPriceFeed(priceFeed).decimals() != PRICE_FEED_DECIMALS) revert BadDecimals(); - if (ERC20(asset).decimals() != decimals_) revert BadDecimals(); + if (IERC20NonStandard(asset).decimals() != decimals_) revert BadDecimals(); // Ensure collateral factors are within range if (assetConfig.borrowCollateralFactor >= assetConfig.liquidateCollateralFactor) revert BorrowCFTooLarge(); @@ -319,15 +348,6 @@ contract Comet is CometMainInterface { } else if (i == 11) { word_a = asset11_a; word_b = asset11_b; - } else if (i == 12) { - word_a = asset12_a; - word_b = asset12_b; - } else if (i == 13) { - word_a = asset13_a; - word_b = asset13_b; - } else if (i == 14) { - word_a = asset14_a; - word_b = asset14_b; } else { revert Absurd(); } @@ -482,7 +502,7 @@ contract Comet is CometMainInterface { * @param asset The collateral asset */ function getCollateralReserves(address asset) override public view returns (uint) { - return ERC20(asset).balanceOf(address(this)) - totalsCollateral[asset].totalSupplyAsset; + return IERC20NonStandard(asset).balanceOf(address(this)) - totalsCollateral[asset].totalSupplyAsset; } /** @@ -490,7 +510,7 @@ contract Comet is CometMainInterface { */ function getReserves() override public view returns (int) { (uint64 baseSupplyIndex_, uint64 baseBorrowIndex_) = accruedInterestIndices(getNowInternal() - lastAccrualTime); - uint balance = ERC20(baseToken).balanceOf(address(this)); + uint balance = IERC20NonStandard(baseToken).balanceOf(address(this)); uint totalSupply_ = presentValueSupply(baseSupplyIndex_, totalSupplyBase); uint totalBorrow_ = presentValueBorrow(baseBorrowIndex_, totalBorrowBase); return signed256(balance) - signed256(totalSupply_) + signed256(totalBorrow_); @@ -760,18 +780,50 @@ contract Comet is CometMainInterface { } /** - * @dev Safe ERC20 transfer in, assumes no fee is charged and amount is transferred + * @dev Safe ERC20 transfer in and returns the final amount transferred (taking into account any fees) + * @dev Note: 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 doTransferIn(address asset, address from, uint amount) internal { - bool success = ERC20(asset).transferFrom(from, address(this), amount); + function doTransferIn(address asset, address from, uint amount) internal returns (uint) { + uint256 preTransferBalance = IERC20NonStandard(asset).balanceOf(address(this)); + IERC20NonStandard(asset).transferFrom(from, address(this), amount); + bool success; + assembly ("memory-safe") { + 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 TransferInFailed(); + return IERC20NonStandard(asset).balanceOf(address(this)) - preTransferBalance; } /** * @dev Safe ERC20 transfer out + * @dev Note: 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 { - bool success = ERC20(asset).transfer(to, amount); + IERC20NonStandard(asset).transfer(to, amount); + bool success; + assembly ("memory-safe") { + 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(); } @@ -809,7 +861,7 @@ contract Comet is CometMainInterface { * @dev Supply either collateral or base asset, depending on the asset, if operator is allowed * @dev Note: Specifying an `amount` of uint256.max will repay all of `dst`'s accrued base borrow balance */ - function supplyInternal(address operator, address from, address dst, address asset, uint amount) internal { + function supplyInternal(address operator, address from, address dst, address asset, uint amount) internal nonReentrant { if (isSupplyPaused()) revert Paused(); if (!hasPermission(from, operator)) revert Unauthorized(); @@ -827,7 +879,7 @@ contract Comet is CometMainInterface { * @dev Supply an amount of base asset from `from` to dst */ function supplyBase(address from, address dst, uint256 amount) internal { - doTransferIn(baseToken, from, amount); + amount = doTransferIn(baseToken, from, amount); accrueInternal(); @@ -854,7 +906,7 @@ contract Comet is CometMainInterface { * @dev Supply an amount of collateral asset from `from` to dst */ function supplyCollateral(address from, address dst, address asset, uint128 amount) internal { - doTransferIn(asset, from, amount); + amount = safe128(doTransferIn(asset, from, amount)); AssetInfo memory assetInfo = getAssetInfoByAddress(asset); TotalsCollateral memory totals = totalsCollateral[asset]; @@ -920,7 +972,7 @@ contract Comet is CometMainInterface { * @dev Transfer either collateral or base asset, depending on the asset, if operator is allowed * @dev Note: Specifying an `amount` of uint256.max will transfer all of `src`'s accrued base balance */ - function transferInternal(address operator, address src, address dst, address asset, uint amount) internal { + function transferInternal(address operator, address src, address dst, address asset, uint amount) internal nonReentrant { if (isTransferPaused()) revert Paused(); if (!hasPermission(src, operator)) revert Unauthorized(); if (src == dst) revert NoSelfTransfer(); @@ -1031,7 +1083,7 @@ contract Comet is CometMainInterface { * @dev Withdraw either collateral or base asset, depending on the asset, if operator is allowed * @dev Note: Specifying an `amount` of uint256.max will withdraw all of `src`'s accrued base balance */ - function withdrawInternal(address operator, address src, address to, address asset, uint amount) internal { + function withdrawInternal(address operator, address src, address to, address asset, uint amount) internal nonReentrant { if (isWithdrawPaused()) revert Paused(); if (!hasPermission(src, operator)) revert Unauthorized(); @@ -1192,14 +1244,14 @@ contract Comet is CometMainInterface { * @param baseAmount The amount of base tokens used to buy the collateral * @param recipient The recipient address */ - function buyCollateral(address asset, uint minAmount, uint baseAmount, address recipient) override external { + function buyCollateral(address asset, uint minAmount, uint baseAmount, address recipient) override external nonReentrant { if (isBuyPaused()) revert Paused(); int reserves = getReserves(); if (reserves >= 0 && uint(reserves) >= targetReserves) revert NotForSale(); // Note: Re-entrancy can skip the reserves check above on a second buyCollateral call. - doTransferIn(baseToken, msg.sender, baseAmount); + baseAmount = doTransferIn(baseToken, msg.sender, baseAmount); uint collateralAmount = quoteCollateral(asset, baseAmount); if (collateralAmount < minAmount) revert TooMuchSlippage(); @@ -1254,6 +1306,7 @@ contract Comet is CometMainInterface { * @dev Only callable by governor * @dev Note: Setting the `asset` as Comet's address will allow the manager * to withdraw from Comet's Comet balance + * @dev Note: For USDT, if there is non-zero prior allowance, it must be reset to 0 first before setting a new value in proposal * @param asset The asset that the manager will gain approval of * @param manager The account which will be allowed or disallowed * @param amount The amount of an asset to approve @@ -1261,7 +1314,7 @@ contract Comet is CometMainInterface { function approveThis(address manager, address asset, uint amount) override external { if (msg.sender != governor) revert Unauthorized(); - ERC20(asset).approve(manager, amount); + IERC20NonStandard(asset).approve(manager, amount); } /** @@ -1322,4 +1375,4 @@ contract Comet is CometMainInterface { default { return(0, returndatasize()) } } } -} +} \ No newline at end of file diff --git a/contracts/CometCore.sol b/contracts/CometCore.sol index 94e17d7f0..534f2701b 100644 --- a/contracts/CometCore.sol +++ b/contracts/CometCore.sol @@ -56,6 +56,13 @@ abstract contract CometCore is CometConfiguration, CometStorage, CometMath { /// @dev The scale for factors uint64 internal constant FACTOR_SCALE = 1e18; + /// @dev The storage slot for reentrancy guard flags + bytes32 internal constant REENTRANCY_GUARD_FLAG_SLOT = bytes32(keccak256("comet.reentrancy.guard")); + + /// @dev The reentrancy guard statuses + uint256 internal constant REENTRANCY_GUARD_NOT_ENTERED = 0; + uint256 internal constant REENTRANCY_GUARD_ENTERED = 1; + /** * @notice Determine if the manager has permission to act on behalf of the owner * @param owner The owner account diff --git a/contracts/CometMainInterface.sol b/contracts/CometMainInterface.sol index 651821908..5347b22f7 100644 --- a/contracts/CometMainInterface.sol +++ b/contracts/CometMainInterface.sol @@ -25,6 +25,7 @@ abstract contract CometMainInterface is CometCore { error NotForSale(); error NotLiquidatable(); error Paused(); + error ReentrantCallBlocked(); error SupplyCapExceeded(); error TimestampTooLarge(); error TooManyAssets(); diff --git a/contracts/IERC20NonStandard.sol b/contracts/IERC20NonStandard.sol index 93dd3e276..8ee78bfce 100644 --- a/contracts/IERC20NonStandard.sol +++ b/contracts/IERC20NonStandard.sol @@ -7,8 +7,37 @@ pragma solidity 0.8.15; * See https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca */ interface IERC20NonStandard { + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + + /** + * @notice Approve `spender` to transfer up to `amount` from `src` + * @dev This will overwrite the approval amount for `spender` + * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) + * @param spender The address of the account which may transfer tokens + * @param amount The number of tokens that are approved (-1 means infinite) + */ function approve(address spender, uint256 amount) external; + + /** + * @notice Transfer `value` tokens from `msg.sender` to `to` + * @param to The address of the destination account + * @param value The number of tokens to transfer + */ function transfer(address to, uint256 value) external; + + /** + * @notice Transfer `value` tokens from `from` to `to` + * @param from The address of the source account + * @param to The address of the destination account + * @param value The number of tokens to transfer + */ function transferFrom(address from, address to, uint256 value) external; + + /** + * @notice Gets the balance of the specified address + * @param account The address from which the balance will be retrieved + */ function balanceOf(address account) external view returns (uint256); -} \ No newline at end of file +} diff --git a/contracts/test/EvilToken.sol b/contracts/test/EvilToken.sol index b17b7ae58..5bc7826af 100644 --- a/contracts/test/EvilToken.sol +++ b/contracts/test/EvilToken.sol @@ -13,7 +13,8 @@ contract EvilToken is FaucetToken { enum AttackType { TRANSFER_FROM, WITHDRAW_FROM, - SUPPLY_FROM + SUPPLY_FROM, + BUY_COLLATERAL } struct ReentryAttack { @@ -52,20 +53,27 @@ contract EvilToken is FaucetToken { attack = attack_; } - function transfer(address, uint256) external override returns (bool) { - return performAttack(); + function transfer(address dst, uint256 amount) public override returns (bool) { + numberOfCalls++; + if (numberOfCalls > attack.maxCalls){ + return super.transfer(dst, amount); + } else { + return performAttack(address(this), dst, amount); + } } - function transferFrom(address, address, uint256) external override returns (bool) { - return performAttack(); + function transferFrom(address src, address dst, uint256 amount) public override returns (bool) { + numberOfCalls++; + if (numberOfCalls > attack.maxCalls) { + return super.transferFrom(src, dst, amount); + } else { + return performAttack(src, dst, amount); + } } - function performAttack() internal returns (bool) { + function performAttack(address src, address dst, uint256 amount) internal returns (bool) { ReentryAttack memory reentryAttack = attack; - numberOfCalls++; - if (numberOfCalls > reentryAttack.maxCalls) { - // do nothing - } else if (reentryAttack.attackType == AttackType.TRANSFER_FROM) { + if (reentryAttack.attackType == AttackType.TRANSFER_FROM) { Comet(payable(msg.sender)).transferFrom( reentryAttack.source, reentryAttack.destination, @@ -85,6 +93,13 @@ contract EvilToken is FaucetToken { reentryAttack.asset, reentryAttack.amount ); + } else if (reentryAttack.attackType == AttackType.BUY_COLLATERAL) { + Comet(payable(msg.sender)).buyCollateral( + reentryAttack.asset, + 0, + reentryAttack.amount, + reentryAttack.destination + ); } else { revert("invalid reentry attack"); } diff --git a/contracts/test/FaucetToken.sol b/contracts/test/FaucetToken.sol index 5c4a8a4cf..0122e9629 100644 --- a/contracts/test/FaucetToken.sol +++ b/contracts/test/FaucetToken.sol @@ -24,7 +24,7 @@ contract StandardToken { decimals = _decimalUnits; } - function transfer(address dst, uint256 amount) external virtual returns (bool) { + function transfer(address dst, uint256 amount) public virtual returns (bool) { require(amount <= balanceOf[msg.sender], "ERC20: transfer amount exceeds balance"); balanceOf[msg.sender] = balanceOf[msg.sender] - amount; balanceOf[dst] = balanceOf[dst] + amount; @@ -32,7 +32,7 @@ contract StandardToken { return true; } - function transferFrom(address src, address dst, uint256 amount) external virtual returns (bool) { + function transferFrom(address src, address dst, uint256 amount) public virtual returns (bool) { require(amount <= allowance[src][msg.sender], "ERC20: transfer amount exceeds allowance"); require(amount <= balanceOf[src], "ERC20: transfer amount exceeds balance"); allowance[src][msg.sender] = allowance[src][msg.sender] - amount; diff --git a/contracts/test/NonStandardFaucetFeeToken.sol b/contracts/test/NonStandardFaucetFeeToken.sol new file mode 100644 index 000000000..e7af6769f --- /dev/null +++ b/contracts/test/NonStandardFaucetFeeToken.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "../IERC20NonStandard.sol"; + +/** + * @title Non-standard ERC20 token + * @dev Implementation of the basic standard token. + * See https://github.com/ethereum/EIPs/issues/20 + * @dev With USDT fee token mechanism + * @dev Note: `transfer` and `transferFrom` do not return a boolean + */ +contract NonStandardFeeToken is IERC20NonStandard { + string public name; + string public symbol; + uint8 public decimals; + address public owner; + uint256 public totalSupply; + mapping(address => mapping (address => uint256)) public allowance; + mapping(address => uint256) public balanceOf; + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); + event Params(uint feeBasisPoints, uint maxFee); + + // additional variables for use if transaction fees ever became necessary + uint public basisPointsRate = 0; + uint public maximumFee = 0; + + constructor(uint256 _initialAmount, string memory _tokenName, uint8 _decimalUnits, string memory _tokenSymbol) { + totalSupply = _initialAmount; + balanceOf[msg.sender] = _initialAmount; + name = _tokenName; + symbol = _tokenSymbol; + decimals = _decimalUnits; + } + + function transfer(address dst, uint256 amount) external virtual { + require(amount <= balanceOf[msg.sender], "ERC20: transfer amount exceeds balance"); + uint256 fee = amount * basisPointsRate / 10000; + uint256 sendAmount = amount - fee; + if (fee > maximumFee) { + fee = maximumFee; + } + + // For testing purpose, just forward fee to contract itself + if (fee > 0) { + balanceOf[address(this)] = balanceOf[address(this)] + fee; + } + + balanceOf[msg.sender] = balanceOf[msg.sender] - amount; + balanceOf[dst] = balanceOf[dst] + sendAmount; + emit Transfer(msg.sender, dst, sendAmount); + } + + function transferFrom(address src, address dst, uint256 amount) external virtual { + require(amount <= allowance[src][msg.sender], "ERC20: transfer amount exceeds allowance"); + require(amount <= balanceOf[src], "ERC20: transfer amount exceeds balance"); + uint256 fee = amount * basisPointsRate / 10000; + uint256 sendAmount = amount - fee; + if (fee > maximumFee) { + fee = maximumFee; + } + + // For testing purpose, just forward fee to contract itself + if (fee > 0) { + balanceOf[address(this)] = balanceOf[address(this)] + fee; + } + + allowance[src][msg.sender] = allowance[src][msg.sender] - amount; + balanceOf[src] = balanceOf[src] - amount; + balanceOf[dst] = balanceOf[dst] + sendAmount; + emit Transfer(src, dst, sendAmount); + } + + function approve(address _spender, uint256 amount) external { + allowance[msg.sender][_spender] = amount; + emit Approval(msg.sender, _spender, amount); + } + + // For testing, just don't limit access on setting fees + function setParams(uint256 newBasisPoints, uint256 newMaxFee) public { + basisPointsRate = newBasisPoints; + maximumFee = newMaxFee * (10**decimals); + + emit Params(basisPointsRate, maximumFee); + } +} + +/** + * @title The Compound Faucet Test Token + * @author Compound + * @notice A simple test token that lets anyone get more of it. + */ +contract NonStandardFaucetFeeToken is NonStandardFeeToken { + constructor(uint256 _initialAmount, string memory _tokenName, uint8 _decimalUnits, string memory _tokenSymbol) + NonStandardFeeToken(_initialAmount, _tokenName, _decimalUnits, _tokenSymbol) { + } + + function allocateTo(address _owner, uint256 value) public { + balanceOf[_owner] += value; + totalSupply += value; + emit Transfer(address(this), _owner, value); + } +} diff --git a/deployments/arbitrum/usdt/configuration.json b/deployments/arbitrum/usdt/configuration.json new file mode 100644 index 000000000..8868f4d51 --- /dev/null +++ b/deployments/arbitrum/usdt/configuration.json @@ -0,0 +1,73 @@ +{ + "name": "Compound USDT", + "symbol": "cUSDTv3", + "baseToken": "USDT", + "baseTokenAddress": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + "baseTokenPriceFeed": "0x3f3f5dF88dC9F13eac63DF89EC16ef6e7E25DdE7", + "borrowMin": "1e6", + "pauseGuardian": "0x78E6317DD6D43DdbDa00Dce32C2CbaFc99361a9d", + "storeFrontPriceFactor": 0.6, + "targetReserves": "20000000e6", + "rates": { + "supplyKink": 0.9, + "supplySlopeLow": 0.059, + "supplySlopeHigh": 2.9, + "supplyBase": 0, + "borrowKink": 0.9, + "borrowSlopeLow": 0.061, + "borrowSlopeHigh": 3.2, + "borrowBase": 0.015 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "138888888888e0", + "baseBorrowSpeed": "115740740740e0", + "baseMinForRewards": "10000e6" + }, + "assets": { + "ARB": { + "address": "0x912ce59144191c1204e64559fe8253a0e49e6548", + "priceFeed": "0xb2A824043730FE05F3DA2efaFa1CBbe83fa548D6", + "decimals": "18", + "borrowCF": 0.7, + "liquidateCF": 0.8, + "liquidationFactor": 0.9, + "supplyCap": "7_500_000e18" + }, + "WETH": { + "address": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "priceFeed": "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612", + "decimals": "18", + "borrowCF": 0.78, + "liquidateCF": 0.85, + "liquidationFactor": 0.95, + "supplyCap": "7_500e18" + }, + "wstETH": { + "address": "0x5979D7b546E38E414F7E9822514be443A4800529", + "decimals": "18", + "borrowCF": 0.7, + "liquidateCF": 0.8, + "liquidationFactor": 0.9, + "supplyCap": "1_500e18" + }, + "WBTC": { + "address": "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", + "priceFeed": "0xd0C7101eACbB49F3deCcCc166d238410D6D46d57", + "decimals": "8", + "borrowCF": 0.7, + "liquidateCF": 0.8, + "liquidationFactor": 0.9, + "supplyCap": "250e8" + }, + "GMX": { + "address": "0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a", + "priceFeed": "0xDB98056FecFff59D032aB628337A4887110df3dB", + "decimals": "18", + "borrowCF": 0.6, + "liquidateCF": 0.7, + "liquidationFactor": 0.8, + "supplyCap": "100_000e18" + } + } +} \ No newline at end of file diff --git a/deployments/arbitrum/usdt/deploy.ts b/deployments/arbitrum/usdt/deploy.ts new file mode 100644 index 000000000..0ebe246de --- /dev/null +++ b/deployments/arbitrum/usdt/deploy.ts @@ -0,0 +1,51 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet } from '../../../src/deploy'; + +export default async function deploy(deploymentManager: DeploymentManager, deploySpec: DeploySpec): Promise { + const trace = deploymentManager.tracer() + const ethers = deploymentManager.hre.ethers; + + // pull in existing assets + // USDC native + const WETH = await deploymentManager.existing('WETH', '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', 'arbitrum'); + const WBTC = await deploymentManager.existing('WBTC', '0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f', 'arbitrum'); + //wstETH + const wstETH = await deploymentManager.existing('wstETH', '0x5979D7b546E38E414F7E9822514be443A4800529', 'arbitrum'); + const USDT = await deploymentManager.existing('USDT', '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', 'arbitrum'); + const ARB = await deploymentManager.existing('ARB', '0x912ce59144191c1204e64559fe8253a0e49e6548', 'arbitrum'); + const GMX = await deploymentManager.existing('GMX', '0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a', 'arbitrum'); + const COMP = await deploymentManager.existing('COMP', '0x354A6dA3fcde098F8389cad84b0182725c6C91dE', 'arbitrum'); + + // Deploy scaling price feed for cbETH + const wstETHMultiplicativePriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'pricefeeds/MultiplicativePriceFeed.sol', + [ + '0xb523AE262D20A936BC152e6023996e46FDC2A95D', // wstETH / ETH price feed + '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612', // ETH / USD price feed + 8, // decimals + 'wstETH/USD price feed' // description + ] + ); + + // Import shared contracts from the USDC.e market + const cometAdmin = await deploymentManager.fromDep('cometAdmin', 'arbitrum', 'usdc.e'); + const $configuratorImpl = await deploymentManager.fromDep('configurator:implementation', 'arbitrum', 'usdc.e'); + const configurator = await deploymentManager.fromDep('configurator', 'arbitrum', 'usdc.e'); + const rewards = await deploymentManager.fromDep('rewards', 'arbitrum', 'usdc.e'); + // should use this bulker, not MainnetBulker + const bulker = await deploymentManager.fromDep('bulker', 'arbitrum', 'usdc.e'); + const localTimelock = await deploymentManager.fromDep('timelock', 'arbitrum', 'usdc.e'); + const bridgeReceiver = await deploymentManager.fromDep('bridgeReceiver', 'arbitrum', 'usdc.e'); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + + return { + ...deployed, + bridgeReceiver, + bulker, + rewards, + COMP + }; +} \ No newline at end of file diff --git a/deployments/arbitrum/usdt/migrations/1717936901_configurate_and_end.ts b/deployments/arbitrum/usdt/migrations/1717936901_configurate_and_end.ts new file mode 100644 index 000000000..f6f3694a8 --- /dev/null +++ b/deployments/arbitrum/usdt/migrations/1717936901_configurate_and_end.ts @@ -0,0 +1,345 @@ + +import { Contract, ethers } from 'ethers'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, getConfigurationStruct, proposal } from '../../../../src/deploy'; +import { expect } from 'chai'; +import { applyL1ToL2Alias, estimateL2Transaction, estimateTokenBridge } from '../../../../scenario/utils/arbitrumUtils'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +const ENSRegistryAddress = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; + +const USDTAmountToBridge = exp(50_000, 6); +const arbitrumCOMPAddress = '0x354A6dA3fcde098F8389cad84b0182725c6C91dE'; +const mainnetUSDTAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'; +const cUSDTAddress = '0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9'; + +export default migration('1717936901_configurate_and_end', { + async prepare(deploymentManager: DeploymentManager) { + const cometFactory = await deploymentManager.deploy('cometFactory', 'CometFactory.sol', [], true); + return { newFactoryAddress: cometFactory.address }; + }, + + enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, { newFactoryAddress }) => { + 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, + } = await govDeploymentManager.getContracts(); + const refundAddress = l2Timelock.address; + const usdtGatewayAddress = await arbitrumL1GatewayRouter.getGateway(mainnetUSDTAddress); + + const usdtGasParams = await estimateTokenBridge( + { + token: mainnetUSDTAddress, + from: timelock.address, + to: comet.address, + amount: USDTAmountToBridge + }, + govDeploymentManager, + deploymentManager + ); + + const configuration = await getConfigurationStruct(deploymentManager); + const setFactoryCalldata = await calldata( + configurator.populateTransaction.setFactory(comet.address, newFactoryAddress) + ); + 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, configurator.address, cometAdmin.address, rewards.address], + [0, 0, 0, 0], + [ + 'setFactory(address,address)', + '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)' + ], + [ + setFactoryCalldata, + setConfigurationCalldata, + deployAndUpgradeToCalldata, + setRewardConfigCalldata + ] + ] + ); + + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const ArbitrumChainId = (await deploymentManager.hre.ethers.provider.getNetwork()).chainId.toString(); + const newMarketObject = { baseSymbol: 'USDT', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse(await ENSResolver.text(subdomainHash, ENSTextRecordKey)); + + // add optimism-usdt comet (0x995E394b8B2437aC8Ce61Ee0bC610D617962B214) + // optimism chain id is 10 + // and arbitrum-weth comet (0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486) + // arbitrum chain id is 42161 + // if there is no usdt comet on optimism chain, add it + if (!officialMarketsJSON[10].find(market => market.baseSymbol === 'USDT')){ + officialMarketsJSON[10].push({ baseSymbol: 'USDT', cometAddress: '0x995E394b8B2437aC8Ce61Ee0bC610D617962B214' }); + } + + if (!officialMarketsJSON[42161].find(market => market.baseSymbol === 'WETH')) { + officialMarketsJSON[42161].push({ baseSymbol: 'WETH', cometAddress: '0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486' }); + } + + if (officialMarketsJSON[ArbitrumChainId]) { + officialMarketsJSON[ArbitrumChainId].push(newMarketObject); + } else { + officialMarketsJSON[ArbitrumChainId] = [newMarketObject]; + } + + const createRetryableTicketGasParams = await estimateL2Transaction( + { + from: applyL1ToL2Alias(timelock.address), + to: bridgeReceiver.address, + data: l2ProposalData + }, + deploymentManager + ); + + const _reduceReservesCalldata = utils.defaultAbiCoder.encode( + ['uint256'], + [USDTAmountToBridge] + ); + + const zeroApproveCalldata = utils.defaultAbiCoder.encode( + ['address', 'uint256'], + [usdtGatewayAddress, 0] + ); + + const approveCalldata = utils.defaultAbiCoder.encode( + ['address', 'uint256'], + [usdtGatewayAddress, USDTAmountToBridge] + ); + + const outboundTransferCalldata = utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes'], + [ + mainnetUSDTAddress, + comet.address, + USDTAmountToBridge, + usdtGasParams.gasLimit, + usdtGasParams.maxFeePerGas, + utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [usdtGasParams.maxSubmissionCost, '0x'] + ) + ] + ); + + 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. Get USDT reserves from cUSDT contract + { + target: cUSDTAddress, + signature: '_reduceReserves(uint256)', + calldata: _reduceReservesCalldata + }, + // 3. Reset approve of USDT from Timelock's to Gateway + { + target: mainnetUSDTAddress, + signature: 'approve(address,uint256)', + calldata: zeroApproveCalldata + }, + // 4. Approve the USDT gateway to take Timelock's USDT for bridging + { + target: mainnetUSDTAddress, + signature: 'approve(address,uint256)', + calldata: approveCalldata + }, + // 5. Bridge USDT from mainnet to Arbitrum Comet + { + target: arbitrumL1GatewayRouter.address, + signature: 'outboundTransfer(address,address,uint256,uint256,uint256,bytes)', + calldata: outboundTransferCalldata, + value: usdtGasParams.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(officialMarketsJSON)] + ) + } + ]; + + const description = "# Initialize cUSDTv3 on Arbitrum\n\n## Proposal summary\n\nFranklinDAO team with advice support from WOOF Software team proposes deployment of Compound III to the Arbitrum network. This proposal takes the governance steps recommended and necessary to initialize a Compound III USDT market on Arbitrum; upon execution, cUSDTv3 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). The new parameters include setting the risk parameters based off of the [recommendations from Gauntlet](https://www.comp.xyz/t/launch-usdt-market-on-compound-v3-arbitrum/5004/2).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/864), [deploy market GitHub action run](https://github.com/woof-software/comet/actions/runs/9595156999) and [forum discussion](https://www.comp.xyz/t/launch-usdt-market-on-compound-v3-arbitrum/5004).\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 `setFactory`, `setConfiguration`, `deployAndUpgradeTo` calls across the bridge to the governance receiver on Arbitrum. It also calls `setRewardConfig` on the Arbitrum rewards contract, to establish Artitrum’s bridged version of COMP as the reward token for the deployment and set the initial supply speed to be 12 COMP/day and borrow speed to be 10 COMP/day.\n\nThe second action reduces Compound’s [cUSDT](https://etherscan.io/address/0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9) reserves and transfers it to Timelock, in order to seed the market reserves for the cUSDTv3 Comet.\n\nThe third action approves 0 USDT from Timelock to [ArbitrumL1CustomGateway](https://etherscan.io/address/0xcEe284F754E854890e311e3280b767F80797180d) to reset potential previous approves.\n\nThe fourth action approves 50K USDT to [ArbitrumL1CustomGateway](https://etherscan.io/address/0xcEe284F754E854890e311e3280b767F80797180d) to take Timelock's USDT on Mainnet, in order to seed the market reserves through the [ArbitrumL1GatewayRouter](https://etherscan.io/address/0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef).\n\nThe fifth action bridges USDT from mainnet via ‘outboundTransfer’ function on ArbitrumL1GatewayRouter’s contract and sends it to Comet on Arbitrum.\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 cUSDTv3 market."; + 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) { + const ethers = deploymentManager.hre.ethers; + + const { + comet, + rewards, + ARB, + WETH, + wstETH, + WBTC, + GMX + } = await deploymentManager.getContracts(); + + const { + timelock + } = await govDeploymentManager.getContracts(); + + // 1. + const ARBInfo = await comet.getAssetInfoByAddress(ARB.address); + const WETHInfo = await comet.getAssetInfoByAddress(WETH.address); + const wstETHInfo = await comet.getAssetInfoByAddress(wstETH.address); + const WBTCInfo = await comet.getAssetInfoByAddress(WBTC.address); + const GMXInfo = await comet.getAssetInfoByAddress(GMX.address); + + // check suplly caps + expect(await ARBInfo.supplyCap).to.be.eq(exp(7_500_000, 18)); + expect(await WETHInfo.supplyCap).to.be.eq(exp(7_500, 18)); + expect(await wstETHInfo.supplyCap).to.be.eq(exp(1_500, 18)); + expect(await WBTCInfo.supplyCap).to.be.eq(exp(250, 8)); + expect(await GMXInfo.supplyCap).to.be.eq(exp(100_000, 18)); + + expect(await comet.pauseGuardian()).to.be.eq('0x78E6317DD6D43DdbDa00Dce32C2CbaFc99361a9d'); + + // 2. & 3. & 4. & 5. + expect(await comet.getReserves()).to.be.equal(USDTAmountToBridge); + + // 6. + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const ENSRegistry = await govDeploymentManager.existing('ENSRegistry', ENSRegistryAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + const officialMarkets = JSON.parse(officialMarketsJSON); + expect(await ENSRegistry.recordExists(subdomainHash)).to.be.equal(true); + expect(await ENSRegistry.owner(subdomainHash)).to.be.equal(timelock.address); + expect(await ENSRegistry.resolver(subdomainHash)).to.be.equal(ENSResolverAddress); + expect(await ENSRegistry.ttl(subdomainHash)).to.be.equal(0); + expect(officialMarkets).to.deep.equal({ + 1: [ + { + baseSymbol: 'USDC', + cometAddress: '0xc3d688B66703497DAA19211EEdff47f25384cdc3', + }, + { + baseSymbol: 'WETH', + cometAddress: '0xA17581A9E3356d9A858b789D68B4d866e593aE94', + }, + ], + 137: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF25212E676D1F7F89Cd72fFEe66158f541246445', + }, + ], + 8453: [ + { + baseSymbol: 'USDbC', + cometAddress: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + }, + { + baseSymbol: 'WETH', + cometAddress: '0x46e6b214b524310239732D51387075E0e70970bf', + }, + { + baseSymbol: 'USDC', + cometAddress: '0xb125E6687d4313864e53df431d5425969c15Eb2F', + }, + ], + 42161: [ + { + baseSymbol: 'USDC.e', + cometAddress: '0xA5EDBDD9646f8dFF606d7448e414884C7d905dCA', + }, + { + baseSymbol: 'USDC', + cometAddress: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + }, + { + baseSymbol: 'WETH', + cometAddress: '0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486', + }, + { + baseSymbol: 'USDT', + cometAddress: comet.address, + }, + ], + 534352: [ + { + baseSymbol: 'USDC', + cometAddress: '0xB2f97c1Bd3bf02f5e74d13f02E3e26F93D77CE44', + }, + ], + 10: [ + { + baseSymbol: 'USDC', + cometAddress: '0x2e44e174f7D53F0212823acC11C01A11d58c5bCB', + }, + { + baseSymbol: 'USDT', + cometAddress: '0x995E394b8B2437aC8Ce61Ee0bC610D617962B214', + }, + ], + }); + + expect(await comet.baseTrackingSupplySpeed()).to.be.equal(exp(12 / 86400, 15, 18)); // 138888888888 + expect(await comet.baseTrackingBorrowSpeed()).to.be.equal(exp(10 / 86400, 15, 18)); // 115740740740 + } +}); \ No newline at end of file diff --git a/deployments/arbitrum/usdt/relations.ts b/deployments/arbitrum/usdt/relations.ts new file mode 100644 index 000000000..d53c3a05d --- /dev/null +++ b/deployments/arbitrum/usdt/relations.ts @@ -0,0 +1,33 @@ +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + governor: { + artifact: 'contracts/bridges/arbitrum/ArbitrumBridgeReceiver.sol:ArbitrumBridgeReceiver' + }, + // WBTC + ClonableBeaconProxy: { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50' + } + } + }, + OssifiableProxy: { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + TransparentUpgradeableProxy: { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, +}; \ No newline at end of file diff --git a/deployments/arbitrum/usdt/roots.json b/deployments/arbitrum/usdt/roots.json new file mode 100644 index 000000000..3dd795a9a --- /dev/null +++ b/deployments/arbitrum/usdt/roots.json @@ -0,0 +1,8 @@ +{ + "comet": "0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07", + "configurator": "0xb21b06D71c75973babdE35b49fFDAc3F82Ad3775", + "rewards": "0x88730d254A2f7e6AC8388c3198aFd694bA9f7fae", + "bridgeReceiver": "0x42480C37B249e33aABaf4c22B20235656bd38068", + "bulker": "0xbdE8F31D2DdDA895264e27DD990faB3DC87b372d", + "COMP": "0x354A6dA3fcde098F8389cad84b0182725c6C91dE" +} \ No newline at end of file diff --git a/deployments/mainnet/usdc/roots.json b/deployments/mainnet/usdc/roots.json index 637e34be5..d25e3fbcd 100644 --- a/deployments/mainnet/usdc/roots.json +++ b/deployments/mainnet/usdc/roots.json @@ -15,4 +15,4 @@ "opL1StandardBridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", "scrollMessenger": "0x6774Bcbd5ceCeF1336b5300fb5186a12DDD8b367", "scrollL1USDCGateway": "0xf1AF3b23DE0A5Ca3CAb7261cb0061C0D779A5c7B" -} +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 02b603d6e..87059717a 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -31,6 +31,7 @@ import arbitrumNativeUsdcRelationConfigMap from './deployments/arbitrum/usdc/rel import arbitrumWETHRelationConfigMap from './deployments/arbitrum/weth/relations'; import arbitrumBridgedUsdcGoerliRelationConfigMap from './deployments/arbitrum-goerli/usdc.e/relations'; import arbitrumGoerliNativeUsdcRelationConfigMap from './deployments/arbitrum-goerli/usdc/relations'; +import arbitrumUsdtRelationConfigMap from './deployments/arbitrum/usdt/relations'; import baseUsdbcRelationConfigMap from './deployments/base/usdbc/relations'; import baseWethRelationConfigMap from './deployments/base/weth/relations'; import baseUsdcRelationConfigMap from './deployments/base/usdc/relations'; @@ -356,6 +357,7 @@ const config: HardhatUserConfig = { arbitrum: { 'usdc.e': arbitrumBridgedUsdcRelationConfigMap, usdc: arbitrumNativeUsdcRelationConfigMap, + usdt: arbitrumUsdtRelationConfigMap, weth: arbitrumWETHRelationConfigMap }, 'arbitrum-goerli': { @@ -448,6 +450,12 @@ const config: HardhatUserConfig = { deployment: 'usdc.e', auxiliaryBase: 'mainnet' }, + { + name: 'arbitrum-usdt', + network: 'arbitrum', + deployment: 'usdt', + auxiliaryBase: 'mainnet' + }, { name: 'arbitrum-usdc', network: 'arbitrum', diff --git a/package.json b/package.json index f1630e10d..c5a99c696 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "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", + "hardhat-contract-sizer": "^2.10.0", "hardhat-cover": "compound-finance/hardhat-cover", "hardhat-gas-reporter": "^1.0.7", "mocha-junit-reporter": "^2.0.2", diff --git a/scenario/ApproveThisScenario.ts b/scenario/ApproveThisScenario.ts index 2c4dc184c..c952e2185 100644 --- a/scenario/ApproveThisScenario.ts +++ b/scenario/ApproveThisScenario.ts @@ -36,4 +36,4 @@ scenario('Comet#approveThis > allows governor to authorize and rescind authoriza scenario('Comet#approveThis > reverts if not called by governor', {}, async ({ comet, timelock }) => { await expectRevertCustom(comet.approveThis(timelock.address, comet.address, constants.MaxUint256), 'Unauthorized()'); -}); +}); \ No newline at end of file diff --git a/scenario/BulkerScenario.ts b/scenario/BulkerScenario.ts index a73d84d2c..42597dda1 100644 --- a/scenario/BulkerScenario.ts +++ b/scenario/BulkerScenario.ts @@ -24,8 +24,9 @@ scenario( const baseAssetAddress = await comet.baseToken(); const baseAsset = context.getAssetByAddress(baseAssetAddress); const baseScale = (await comet.baseScale()).toBigInt(); - const { asset: collateralAssetAddress, scale: scaleBN } = await comet.getAssetInfo(0); - const collateralAsset = context.getAssetByAddress(collateralAssetAddress); + const { asset: asset0, scale: scale0 } = await comet.getAssetInfo(0); + const { asset: asset1, scale: scale1 } = await comet.getAssetInfo(1); + const { asset: collateralAssetAddress, scale: scaleBN } = asset0 === wrappedNativeToken ? { asset: asset1, scale: scale1 } : { asset: asset0, scale: scale0 }; const collateralAsset = context.getAssetByAddress(collateralAssetAddress); const collateralScale = scaleBN.toBigInt(); const toSupplyCollateral = 3000n * collateralScale; const toBorrowBase = 1000n * baseScale; diff --git a/scenario/LiquidationBotScenario.ts b/scenario/LiquidationBotScenario.ts index 763468c9e..e0645a7a9 100644 --- a/scenario/LiquidationBotScenario.ts +++ b/scenario/LiquidationBotScenario.ts @@ -99,7 +99,8 @@ for (let i = 0; i < MAX_ASSETS; i++) { }, arbitrum: { 'usdc.e': 10000000, - usdc: 10000000 + usdc: 10000000, + usdt: 10000000 } }; const assetAmounts = { @@ -284,7 +285,8 @@ for (let i = 0; i < MAX_ASSETS; i++) { }, arbitrum: { 'usdc.e': 10000000, - usdc: 10000000 + usdc: 10000000, + usdt: 10000000 } }; const assetAmounts = { @@ -340,6 +342,18 @@ for (let i = 0; i < MAX_ASSETS; i++) { ' == 5000', // WBTC ' == 300' + ], + usdt: [ + // ARB + ' == 1000000', + // WETH + ' == 5000', + // wstETH + ' == 5000', + // WBTC + ' == 300', + // GMX + ' == 10000' ] } }; @@ -396,6 +410,18 @@ for (let i = 0; i < MAX_ASSETS; i++) { exp(500, 18), // WBTC exp(50, 8), + ], + usdt: [ + // ARB + exp(300000, 18), + // WETH + exp(500, 18), + // wstETH + exp(500, 18), + // WBTC + exp(50, 8), + // GMX + exp(3000, 18) ] } }; @@ -739,7 +765,7 @@ scenario( const assetAmounts = { mainnet: { usdc: ' == 5000', // COMP - weth: ' == 10000', // CB_ETH + weth: ' == 10000' // CB_ETH }, }; diff --git a/scenario/LiquidationScenario.ts b/scenario/LiquidationScenario.ts index 567c72cf0..c39389d65 100644 --- a/scenario/LiquidationScenario.ts +++ b/scenario/LiquidationScenario.ts @@ -1,6 +1,7 @@ import { scenario } from './context/CometContext'; import { event, expect } from '../test/helpers'; import { expectRevertCustom, timeUntilUnderwater } from './utils'; +import { matchesDeployment } from './utils'; scenario( 'Comet#liquidation > isLiquidatable=true for underwater position', @@ -32,6 +33,75 @@ scenario( } ); +scenario( + 'Comet#liquidation > allows liquidation of underwater positions with token fees', + { + tokenBalances: { + $comet: { $base: 1000 }, + }, + cometBalances: { + albert: { + $base: -1000, + $asset0: .001 + }, + betty: { $base: 10 } + }, + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet', deployment: 'usdt' }]), + }, + async ({ comet, actors }, context, world) => { + // Set fees for USDT for testing + const USDT = await world.deploymentManager.existing('USDT', await comet.baseToken(), world.base.network); + const USDTAdminAddress = await USDT.owner(); + await world.deploymentManager.hre.network.provider.send('hardhat_setBalance', [ + USDTAdminAddress, + world.deploymentManager.hre.ethers.utils.hexStripZeros(world.deploymentManager.hre.ethers.utils.parseEther('100').toHexString()), + ]); + await world.deploymentManager.hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [USDTAdminAddress], + }); + // mine a block to ensure the impersonation is effective + const USDTAdminSigner = await world.deploymentManager.hre.ethers.getSigner(USDTAdminAddress); + // 10 basis points, and max 10 USDT + await USDT.connect(USDTAdminSigner).setParams(10, 10); + + const { albert, betty } = actors; + + await world.increaseTime( + await timeUntilUnderwater({ + comet, + actor: albert, + fudgeFactor: 60n * 10n // 10 minutes past when position is underwater + }) + ); + + const lp0 = await comet.liquidatorPoints(betty.address); + + await betty.absorb({ absorber: betty.address, accounts: [albert.address] }); + + const lp1 = await comet.liquidatorPoints(betty.address); + + // increments absorber's numAbsorbs + expect(lp1.numAbsorbs).to.eq(lp0.numAbsorbs + 1); + // increases absorber's numAbsorbed + expect(lp1.numAbsorbed.toNumber()).to.eq(lp0.numAbsorbed.toNumber() + 1); + // XXX test approxSpend? + + const baseBalance = await albert.getCometBaseBalance(); + expect(Number(baseBalance)).to.be.greaterThanOrEqual(0); + + // clears out all of liquidated user's collateral + const numAssets = await comet.numAssets(); + for (let i = 0; i < numAssets; i++) { + const { asset } = await comet.getAssetInfo(i); + expect(await comet.collateralBalanceOf(albert.address, asset)).to.eq(0); + } + + // clears assetsIn + expect((await comet.userBasic(albert.address)).assetsIn).to.eq(0); + } +); + scenario( 'Comet#liquidation > prevents liquidation when absorb is paused', { diff --git a/scenario/SupplyScenario.ts b/scenario/SupplyScenario.ts index a3ec668af..5c12fc065 100644 --- a/scenario/SupplyScenario.ts +++ b/scenario/SupplyScenario.ts @@ -2,6 +2,9 @@ import { CometContext, scenario } from './context/CometContext'; import { expect } from 'chai'; import { expectApproximately, expectBase, expectRevertCustom, expectRevertMatches, getExpectedBaseBalance, getInterest, isTriviallySourceable, isValidAssetIndex, MAX_ASSETS, UINT256_MAX } from './utils'; import { ContractReceipt } from 'ethers'; +import { matchesDeployment } from './utils'; +import { exp } from '../test/helpers'; +import { ethers } from 'hardhat'; // XXX introduce a SupplyCapConstraint to separately test the happy path and revert path instead // of testing them conditionally @@ -136,6 +139,52 @@ scenario( } ); +scenario( + 'Comet#supply > base asset with token fees', + { + tokenBalances: { + albert: { $base: 1000 }, // in units of asset, not wei + }, + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet', deployment: 'usdt' }]) + }, + async ({ comet, actors }, context, world) => { + // Set fees for USDT for testing + const USDT = await world.deploymentManager.existing('USDT', await comet.baseToken(), world.base.network); + const USDTAdminAddress = await USDT.owner(); + await world.deploymentManager.hre.network.provider.send('hardhat_setBalance', [ + USDTAdminAddress, + ethers.utils.hexStripZeros(ethers.utils.parseEther('100').toHexString()), + ]); + await world.deploymentManager.hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [USDTAdminAddress], + }); + // mine a block to ensure the impersonation is effective + const USDTAdminSigner = await world.deploymentManager.hre.ethers.getSigner(USDTAdminAddress); + // 10 basis points, and max 10 USDT + await USDT.connect(USDTAdminSigner).setParams(10, 10); + + const { albert } = actors; + const baseAssetAddress = await comet.baseToken(); + const baseAsset = context.getAssetByAddress(baseAssetAddress); + const scale = (await comet.baseScale()).toBigInt(); + + expect(await baseAsset.balanceOf(albert.address)).to.be.equal(1000n * scale); + + // Albert supplies 1000 units of base to Comet + await baseAsset.approve(albert, comet.address); + const txn = await albert.supplyAsset({ asset: baseAsset.address, amount: 1000n * scale }); + + const baseIndexScale = (await comet.baseIndexScale()).toBigInt(); + const baseSupplyIndex = (await comet.totalsBasic()).baseSupplyIndex.toBigInt(); + const baseSupplied = getExpectedBaseBalance(999n * scale, baseIndexScale, baseSupplyIndex); + + expect(await comet.balanceOf(albert.address)).to.be.equal(baseSupplied); + + return txn; // return txn to measure gas + } +); + scenario( 'Comet#supply > repay borrow', { @@ -167,6 +216,104 @@ scenario( } ); +scenario( + 'Comet#supply > repay borrow with token fees', + { + tokenBalances: { + albert: { $base: '==1000' } + }, + cometBalances: { + albert: { $base: -1000 } // in units of asset, not wei + }, + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet', deployment: 'usdt' }]), + }, + async ({ comet, actors }, context, world) => { + // Set fees for USDT for testing + const USDT = await world.deploymentManager.existing('USDT', await comet.baseToken(), world.base.network); + const USDTAdminAddress = await USDT.owner(); + await world.deploymentManager.hre.network.provider.send('hardhat_setBalance', [ + USDTAdminAddress, + ethers.utils.hexStripZeros(ethers.utils.parseEther('100').toHexString()), + ]); + await world.deploymentManager.hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [USDTAdminAddress], + }); + // mine a block to ensure the impersonation is effective + const USDTAdminSigner = await world.deploymentManager.hre.ethers.getSigner(USDTAdminAddress); + // 10 basis points, and max 10 USDT + await USDT.connect(USDTAdminSigner).setParams(10, 10); + + const { albert } = actors; + const baseAssetAddress = await comet.baseToken(); + const baseAsset = context.getAssetByAddress(baseAssetAddress); + const scale = (await comet.baseScale()).toBigInt(); + const utilization = await comet.getUtilization(); + const borrowRate = (await comet.getBorrowRate(utilization)).toBigInt(); + + expectApproximately(await albert.getCometBaseBalance(), -1000n * scale, getInterest(1000n * scale, borrowRate, 1n) + 1n); + + // Albert repays 1000 units of base borrow + await baseAsset.approve(albert, comet.address); + const txn = await albert.supplyAsset({ asset: baseAsset.address, amount: 1000n * scale }); + + // XXX all these timings are crazy + // Expect to have -1000000, due to token fee, alber only repay 999 USDT instead of 1000 USDT, thus alber still owe 1 USDT which is 1000000 + expectApproximately(await albert.getCometBaseBalance(), -1n * exp(1, 6), getInterest(1000n * scale, borrowRate, 4n) + 2n); + + return txn; // return txn to measure gas + } +); + +scenario( + 'Comet#supply > repay all borrow with token fees', + { + tokenBalances: { + albert: { $base: '==1000' } + }, + cometBalances: { + albert: { $base: -999 } // in units of asset, not wei + }, + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet', deployment: 'usdt' }]), + }, + async ({ comet, actors }, context, world) => { + // Set fees for USDT for testing + const USDT = await world.deploymentManager.existing('USDT', await comet.baseToken(), world.base.network); + const USDTAdminAddress = await USDT.owner(); + await world.deploymentManager.hre.network.provider.send('hardhat_setBalance', [ + USDTAdminAddress, + ethers.utils.hexStripZeros(ethers.utils.parseEther('100').toHexString()), + ]); + await world.deploymentManager.hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [USDTAdminAddress], + }); + // mine a block to ensure the impersonation is effective + const USDTAdminSigner = await world.deploymentManager.hre.ethers.getSigner(USDTAdminAddress); + // 10 basis points, and max 10 USDT + await USDT.connect(USDTAdminSigner).setParams(10, 10); + + const { albert } = actors; + const baseAssetAddress = await comet.baseToken(); + const baseAsset = context.getAssetByAddress(baseAssetAddress); + const scale = (await comet.baseScale()).toBigInt(); + const utilization = await comet.getUtilization(); + const borrowRate = (await comet.getBorrowRate(utilization)).toBigInt(); + + expectApproximately(await albert.getCometBaseBalance(), -999n * scale, getInterest(999n * scale, borrowRate, 1n) + 1n); + + // Albert repays 1000 units of base borrow + await baseAsset.approve(albert, comet.address); + const txn = await albert.supplyAsset({ asset: baseAsset.address, amount: 1000n * scale }); + + // XXX all these timings are crazy + // albert supply 1000 USDT to repay, 1000USDT * (99.9%) = 999 USDT, thus albert should have just enough to repay his debt of 999 USDT. + expectApproximately(await albert.getCometBaseBalance(), 0n, getInterest(1000n * scale, borrowRate, 4n) + 2n); + + return txn; // return txn to measure gas + } +); + scenario( 'Comet#supplyFrom > base asset', { @@ -200,6 +347,56 @@ scenario( } ); +scenario( + 'Comet#supplyFrom > base asset with token fees', + { + tokenBalances: { + albert: { $base: 1000 }, // in units of asset, not wei + }, + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet', deployment: 'usdt' }]), + }, + async ({ comet, actors }, context, world) => { + // Set fees for USDT for testing + const USDT = await world.deploymentManager.existing('USDT', await comet.baseToken(), world.base.network); + const USDTAdminAddress = await USDT.owner(); + await world.deploymentManager.hre.network.provider.send('hardhat_setBalance', [ + USDTAdminAddress, + ethers.utils.hexStripZeros(ethers.utils.parseEther('100').toHexString()), + ]); + await world.deploymentManager.hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [USDTAdminAddress], + }); + // mine a block to ensure the impersonation is effective + const USDTAdminSigner = await world.deploymentManager.hre.ethers.getSigner(USDTAdminAddress); + // 10 basis points, and max 10 USDT + await USDT.connect(USDTAdminSigner).setParams(10, 10); + + const { albert, betty } = actors; + const baseAssetAddress = await comet.baseToken(); + const baseAsset = context.getAssetByAddress(baseAssetAddress); + const scale = (await comet.baseScale()).toBigInt(); + + expect(await baseAsset.balanceOf(albert.address)).to.be.equal(1000n * scale); + expect(await comet.balanceOf(betty.address)).to.be.equal(0n); + + await baseAsset.approve(albert, comet.address); + await albert.allow(betty, true); + + // Betty supplies 1000 units of base from Albert + const txn = await betty.supplyAssetFrom({ src: albert.address, dst: betty.address, asset: baseAsset.address, amount: 1000n * scale }); + + const baseIndexScale = (await comet.baseIndexScale()).toBigInt(); + const baseSupplyIndex = (await comet.totalsBasic()).baseSupplyIndex.toBigInt(); + const baseSupplied = getExpectedBaseBalance(999n * scale, baseIndexScale, baseSupplyIndex); + + expect(await baseAsset.balanceOf(albert.address)).to.be.equal(0n); + expect(await comet.balanceOf(betty.address)).to.be.equal(baseSupplied); + + return txn; // return txn to measure gas + } +); + scenario( 'Comet#supplyFrom > repay borrow', { diff --git a/scenario/constraints/ProposalConstraint.ts b/scenario/constraints/ProposalConstraint.ts index 83e5c967d..02ccc7407 100644 --- a/scenario/constraints/ProposalConstraint.ts +++ b/scenario/constraints/ProposalConstraint.ts @@ -62,6 +62,12 @@ export class ProposalConstraint implements StaticConstra ); } + // temporary hack to skip proposal 259 + if (proposal.id.eq(259)) { + console.log('Skipping proposal 259'); + continue; + } + try { // Execute the proposal debug(`${label} Processing pending proposal ${proposal.id}`); diff --git a/scenario/utils/relayArbitrumMessage.ts b/scenario/utils/relayArbitrumMessage.ts index 287ec7b5d..442d45a27 100644 --- a/scenario/utils/relayArbitrumMessage.ts +++ b/scenario/utils/relayArbitrumMessage.ts @@ -30,7 +30,13 @@ export async function relayArbitrumMessage( 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(); + let toAddress = BigNumber.from(`0x${toValue}`).toHexString(); + + // if lenght of toAddress is less than 42, then it is padded with 0s and we need to add them after 0x + if(toAddress.length < 42) { + toAddress = `0x${toAddress.slice(2).padStart(40, '0')}`; + } + const messageNum = topics[1]; return { data: innnerData, diff --git a/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts b/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts index 577bf2fef..74cb6e25f 100644 --- a/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts +++ b/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts @@ -47,7 +47,6 @@ const addresses = { WBTC: '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6', WETH: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', WMATIC: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', - USDT: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', BOB: '0xb0b195aefa3650a6908f15cdac7d92f8a5791b0b' }, arbitrum: { @@ -55,9 +54,9 @@ const addresses = { GMX: '0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a', WETH: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', WBTC: '0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f', - USDT: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', USDC: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', USDC_E: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + USDT: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', rETH: '0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8', wstETH: '0x5979D7b546E38E414F7E9822514be443A4800529', weETH: '0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe' @@ -113,6 +112,10 @@ export const flashLoanPools = { tokenAddress: addresses.arbitrum.USDC_E, poolFee: 100 }, + usdt: { + tokenAddress: addresses.arbitrum.USDT, + poolFee: 100 + }, weth: { tokenAddress: addresses.arbitrum.USDC, poolFee: 500 diff --git a/test/buy-collateral-test.ts b/test/buy-collateral-test.ts index ca271af03..b82ad10ab 100644 --- a/test/buy-collateral-test.ts +++ b/test/buy-collateral-test.ts @@ -1,4 +1,4 @@ -import { EvilToken, EvilToken__factory, FaucetToken } from '../build/types'; +import { EvilToken, EvilToken__factory, NonStandardFaucetFeeToken__factory, FaucetToken, NonStandardFaucetFeeToken } from '../build/types'; import { ethers, event, expect, exp, getBlock, makeProtocol, portfolio, ReentryAttack, wait } from './helpers'; describe('buyCollateral', function () { @@ -323,12 +323,93 @@ describe('buyCollateral', function () { await expect(cometAsA.buyCollateral(COMP.address, exp(50, 18), 50e6, alice.address)).to.be.revertedWith("custom error 'Paused()'"); }); - it.skip('buys the correct amount in a fee-like situation', async () => { - // Note: fee-tokens are not currently supported (for efficiency) and should not be added + it('buys the correct amount in a fee-like situation', async () => { + const protocol = await makeProtocol({ + base: 'USDT', + storeFrontPriceFactor: exp(0.5, 18), + targetReserves: 100, + assets: { + USDT: { + initial: 1e6, + decimals: 6, + initialPrice: 1, + factory: (await ethers.getContractFactory('NonStandardFaucetFeeToken')) as NonStandardFaucetFeeToken__factory, + }, + COMP: { + initial: 1e7, + decimals: 18, + initialPrice: 1, + liquidationFactor: exp(0.8, 18), + factory: (await ethers.getContractFactory('NonStandardFaucetFeeToken')) as NonStandardFaucetFeeToken__factory, + }, + } + }); + + const { comet, tokens, users: [alice] } = protocol; + const { USDT, COMP } = tokens; + + // Set both COMP and USDT with 1% fees + // So we can test internal accounting works correctly in both ways: 1. correctly deducting fees from payment during buyCollateral 2. correctly deducting fees from collateral token to buyer + await (COMP as NonStandardFaucetFeeToken).setParams(100, 10000); + await (USDT as NonStandardFaucetFeeToken).setParams(100, 10000); + + const cometAsA = comet.connect(alice); + const baseAsA = USDT.connect(alice); + + // Reserves are at 0 wei + + // Set up token balances and accounting + await USDT.allocateTo(alice.address, 100e6); + await COMP.allocateTo(comet.address, exp(60, 18)); + + const r0 = await comet.getReserves(); + const p0 = await portfolio(protocol, alice.address); + await wait(baseAsA.approve(comet.address, exp(50, 6))); + // Alice buys 50e6 wei USDT worth of COMP + + // Some math writeup for better understanding in each expects number: + // assetPriceDiscount = 1 - (storeFrontPriceFactor * (1 - liquidationFactor)) * assetPrice + // assetPriceDiscount = 1 - (0.5 * (1 - 0.8)) * 1 = 0.9 + // collateralAmount = basePrice * baseAmount / assetPriceDiscount + // collateralAmount = 1 * 50 * (1 - Token Fee) / 0.9 = 1 * 50 * 0.99 / 0.9 = 55 + // actualReceiveCollateral = 55 * (1 - Token Fee) = 55 * 0.99 = 54.45 + const txn = await wait(cometAsA.buyCollateral(COMP.address, exp(50, 18), 50e6, alice.address)); + const p1 = await portfolio(protocol, alice.address); + const r1 = await comet.getReserves(); + + expect(r0).to.be.equal(0n); + expect(r0).to.be.lt(await comet.targetReserves()); + expect(p0.internal).to.be.deep.equal({ USDT: 0n, COMP: 0n }); + expect(p0.external).to.be.deep.equal({ USDT: exp(100, 6), COMP: 0n }); + expect(p1.internal).to.be.deep.equal({ USDT: 0n, COMP: 0n }); + expect(p1.external).to.be.deep.equal({ USDT: exp(50, 6), COMP: exp(54.45, 18) }); + expect(r1).to.be.equal(exp(49.5, 6)); // 50 * 0.99 = 49.5 + expect(event(txn, 0)).to.be.deep.equal({ + Transfer: { + from: alice.address, + to: comet.address, + amount: exp(49.5, 6), + } + }); + expect(event(txn, 1)).to.be.deep.equal({ + Transfer: { + from: comet.address, + to: alice.address, + amount: exp(54.45, 18), + } + }); + expect(event(txn, 2)).to.be.deep.equal({ + BuyCollateral: { + buyer: alice.address, + asset: COMP.address, + baseAmount: exp(49.5, 6), + collateralAmount: exp(55, 18), + } + }); }); describe('reentrancy', function() { - it('is not broken by reentrancy supply ', async () => { + it('is blocked during reentrant supply', async () => { const wethArgs = { initial: 1e4, decimals: 18, @@ -379,7 +460,7 @@ describe('buyCollateral', function () { source: evilAlice.address, destination: evilBob.address, asset: EVIL.address, - amount: 1e6, + amount: 3000e6, maxCalls: 1 }); await EVIL.setAttack(attack); @@ -409,7 +490,7 @@ describe('buyCollateral', function () { // approve Comet to move funds await normalUSDC.connect(normalAlice).approve(normalComet.address, exp(5000, 6)); await EVIL.connect(evilAlice).approve(EVIL.address, exp(5000, 6)); - + await EVIL.connect(evilAlice).approve(evilComet.address, exp(5000, 6)); // perform the supplies for each protocol in the same block, so that the // same amount of time elapses for each when calculating interest await ethers.provider.send('evm_setAutomine', [false]); @@ -441,10 +522,11 @@ describe('buyCollateral', function () { .connect(evilAlice) .buyCollateral( evilWETH.address, - exp(.5, 18), + exp(0, 18), exp(3000, 6), evilAlice.address ); + await evilComet.accrueAccount(evilAlice.address); // !important; reenable automine @@ -460,7 +542,9 @@ describe('buyCollateral', function () { expect(normalTotalsBasic.baseBorrowIndex).to.equal(evilTotalsBasic.baseBorrowIndex); expect(normalTotalsBasic.trackingSupplyIndex).to.equal(evilTotalsBasic.trackingSupplyIndex); expect(normalTotalsBasic.trackingBorrowIndex).to.equal(evilTotalsBasic.trackingBorrowIndex); - expect(normalTotalsBasic.totalSupplyBase).to.equal(evilTotalsBasic.totalSupplyBase); + expect(normalTotalsBasic.totalSupplyBase).to.equal(1e6); + // EvilToken attack should be blocked + expect(evilTotalsBasic.totalSupplyBase).to.equal(0); expect(normalTotalsBasic.totalBorrowBase).to.equal(evilTotalsBasic.totalBorrowBase); expect(normalTotalsCollateral.totalSupplyAsset).to.eq(evilTotalsCollateral.totalSupplyAsset); @@ -474,7 +558,73 @@ describe('buyCollateral', function () { const normalBobPortfolio = await portfolio(normalProtocol, normalBob.address); const evilBobPortfolio = await portfolio(evilProtocol, evilBob.address); - expect(normalBobPortfolio.internal.USDC).to.equal(evilBobPortfolio.internal.EVIL); + expect(normalBobPortfolio.internal.USDC).to.equal(1e6); + // EvilToken attack should be blocked, so totalSupplyBase should be 0 + expect(evilBobPortfolio.internal.EVIL).to.equal(0); + }); + + it('reentrant buyCollateral is reverted', async () => { + const wethArgs = { + initial: 1e4, + decimals: 18, + initialPrice: 3000, + }; + const baseTokenArgs = { + decimals: 6, + initial: 1e6, + initialPrice: 1, + }; + + // malicious scenario, EVIL token is base + const evilProtocol = await makeProtocol({ + base: 'EVIL', + assets: { + EVIL: { + ...baseTokenArgs, + factory: await ethers.getContractFactory('EvilToken') as EvilToken__factory, + }, + WETH: wethArgs, + }, + targetReserves: 1 + }); + const { + comet: evilComet, + tokens: evilTokens, + users: [evilAlice, evilBob] + } = evilProtocol; + const { WETH: evilWETH, EVIL } = <{ WETH: FaucetToken, EVIL: EvilToken }>evilTokens; + + // add attack to EVIL token + const attack = Object.assign({}, await EVIL.getAttack(), { + attackType: ReentryAttack.BuyCollateral, + source: evilAlice.address, + destination: evilBob.address, + asset: evilWETH.address, + amount: 3000e6, + maxCalls: 1 + }); + await EVIL.setAttack(attack); + + // allocate tokens (evil) + await evilWETH.allocateTo(evilComet.address, exp(100, 18)); + await EVIL.allocateTo(evilAlice.address, exp(5000, 6)); + + // approve Comet to move funds + await EVIL.connect(evilAlice).approve(EVIL.address, exp(5000, 6)); + await EVIL.connect(evilAlice).approve(evilComet.address, exp(5000, 6)); + + // authorize EVIL, since callback will originate from EVIL token address + await evilComet.connect(evilAlice).allow(EVIL.address, true); + + // call buyCollateral; supplyFrom is called in callback + await expect(evilComet + .connect(evilAlice) + .buyCollateral( + evilWETH.address, + exp(0, 18), + exp(3000, 6), + evilAlice.address + )).to.be.revertedWith("custom error 'ReentrantCallBlocked()'"); }); }); -}); +}); \ No newline at end of file diff --git a/test/comet-ext-test.ts b/test/comet-ext-test.ts index 265130296..d297bf22b 100644 --- a/test/comet-ext-test.ts +++ b/test/comet-ext-test.ts @@ -1,11 +1,11 @@ -import { CometHarnessInterface, FaucetToken } from '../build/types'; +import { CometHarnessInterface, FaucetToken, NonStandardFaucetFeeToken } from '../build/types'; import { expect, exp, makeProtocol, setTotalsBasic } from './helpers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; describe('CometExt', function () { let comet: CometHarnessInterface; let user: SignerWithAddress; - let tokens: { [symbol: string]: FaucetToken }; + let tokens: { [symbol: string]: FaucetToken | NonStandardFaucetFeeToken }; beforeEach(async () => { ({ diff --git a/test/helpers.ts b/test/helpers.ts index 31ebf1d81..58acf19af 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -30,6 +30,8 @@ import { Configurator__factory, CometHarnessInterface, CometInterface, + NonStandardFaucetFeeToken, + NonStandardFaucetFeeToken__factory, } from '../build/types'; import { BigNumber } from 'ethers'; import { TransactionReceipt, TransactionResponse } from '@ethersproject/abstract-provider'; @@ -42,7 +44,8 @@ export type Numeric = number | bigint; export enum ReentryAttack { TransferFrom = 0, WithdrawFrom = 1, - SupplyFrom = 2 + SupplyFrom = 2, + BuyCollateral = 3, } export type ProtocolOpts = { @@ -58,7 +61,7 @@ export type ProtocolOpts = { supplyCap?: Numeric; initialPrice?: number; priceFeedDecimals?: number; - factory?: FaucetToken__factory | EvilToken__factory | FaucetWETH__factory; + factory?: FaucetToken__factory | EvilToken__factory | FaucetWETH__factory | NonStandardFaucetFeeToken__factory; }; }; name?: string; @@ -96,7 +99,7 @@ export type Protocol = { reward: string; comet: Comet; tokens: { - [symbol: string]: FaucetToken; + [symbol: string]: FaucetToken | NonStandardFaucetFeeToken; }; unsupportedToken: FaucetToken; priceFeeds: { @@ -114,7 +117,7 @@ export type ConfiguratorAndProtocol = { export type RewardsOpts = { governor?: SignerWithAddress; - configs?: [Comet, FaucetToken, Numeric?][]; + configs?: [Comet, FaucetToken | NonStandardFaucetFeeToken, Numeric?][]; }; export type Rewards = { @@ -503,7 +506,7 @@ export async function makeBulker(opts: BulkerOpts): Promise { bulker }; } -export async function bumpTotalsCollateral(comet: CometHarnessInterface, token: FaucetToken, delta: bigint): Promise { +export async function bumpTotalsCollateral(comet: CometHarnessInterface, token: FaucetToken | NonStandardFaucetFeeToken, delta: bigint): Promise { const t0 = await comet.totalsCollateral(token.address); const t1 = Object.assign({}, t0, { totalSupplyAsset: t0.totalSupplyAsset.toBigInt() + delta }); await token.allocateTo(comet.address, delta); diff --git a/test/supply-test.ts b/test/supply-test.ts index 0987a2f1c..c883fdb8f 100644 --- a/test/supply-test.ts +++ b/test/supply-test.ts @@ -1,5 +1,5 @@ -import { ethers, event, expect, exp, makeProtocol, portfolio, ReentryAttack, setTotalsBasic, wait, fastForward } from './helpers'; -import { EvilToken, EvilToken__factory } from '../build/types'; +import { ethers, event, expect, exp, makeProtocol, portfolio, ReentryAttack, setTotalsBasic, wait, fastForward, defaultAssets } from './helpers'; +import { EvilToken, EvilToken__factory, NonStandardFaucetFeeToken__factory, NonStandardFaucetFeeToken } from '../build/types'; describe('supplyTo', function () { it('supplies base from sender if the asset is base', async () => { @@ -52,7 +52,7 @@ describe('supplyTo', function () { expect(q1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(t1.totalSupplyBase).to.be.equal(t0.totalSupplyBase.add(100e6)); expect(t1.totalBorrowBase).to.be.equal(t0.totalBorrowBase); - expect(Number(s0.receipt.gasUsed)).to.be.lessThan(120000); + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(124000); }); it('supplies max base borrow balance (including accrued) from sender if the asset is base', async () => { @@ -259,7 +259,7 @@ describe('supplyTo', function () { expect(p1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(t1.totalSupplyBase).to.be.equal(109); expect(t1.totalBorrowBase).to.be.equal(t0.totalBorrowBase); - expect(Number(s0.receipt.gasUsed)).to.be.lessThan(120000); + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(124000); }); it('supplies collateral from sender if the asset is collateral', async () => { @@ -305,7 +305,7 @@ describe('supplyTo', function () { expect(q1.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(q1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(t1.totalSupplyAsset).to.be.equal(t0.totalSupplyAsset.add(8e8)); - expect(Number(s0.receipt.gasUsed)).to.be.lessThan(140000); + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(153000); }); it('calculates base principal correctly', async () => { @@ -405,11 +405,129 @@ describe('supplyTo', function () { await expect(cometAsB.supplyTo(alice.address, COMP.address, ethers.constants.MaxUint256)).to.be.revertedWith("custom error 'InvalidUInt128()'"); }); - it.skip('supplies the correct amount in a fee-like situation', async () => { - // Note: fee-tokens are not currently supported (for efficiency) and should not be added + it('supplies base the correct amount in a fee-like situation', async () => { + const assets = defaultAssets(); + // Add USDT to assets on top of default assets + assets['USDT'] = { + initial: 1e6, + decimals: 6, + factory: (await ethers.getContractFactory('NonStandardFaucetFeeToken')) as NonStandardFaucetFeeToken__factory, + }; + const protocol = await makeProtocol({ base: 'USDT', assets: assets }); + const { comet, tokens, users: [alice, bob] } = protocol; + const { USDT } = tokens; + + // Set fee to 0.1% + await (USDT as NonStandardFaucetFeeToken).setParams(10, 10); + + const _i0 = await USDT.allocateTo(bob.address, 1000e6); + const baseAsB = USDT.connect(bob); + const cometAsB = comet.connect(bob); + + const t0 = await comet.totalsBasic(); + const p0 = await portfolio(protocol, alice.address); + const q0 = await portfolio(protocol, bob.address); + const _a0 = await wait(baseAsB.approve(comet.address, 1000e6)); + const s0 = await wait(cometAsB.supplyTo(alice.address, USDT.address, 1000e6)); + const t1 = await comet.totalsBasic(); + const p1 = await portfolio(protocol, alice.address); + const q1 = await portfolio(protocol, bob.address); + + expect(event(s0, 0)).to.be.deep.equal({ + Transfer: { + from: bob.address, + to: comet.address, + amount: BigInt(999e6), + } + }); + expect(event(s0, 1)).to.be.deep.equal({ + Supply: { + from: bob.address, + dst: alice.address, + amount: BigInt(999e6), + } + }); + expect(event(s0, 2)).to.be.deep.equal({ + Transfer: { + from: ethers.constants.AddressZero, + to: alice.address, + amount: BigInt(999e6), + } + }); + + expect(p0.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, USDT: 0n }); + expect(p0.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, USDT: 0n }); + expect(q0.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, USDT: 0n }); + expect(q0.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, USDT: exp(1000, 6) }); + expect(p1.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, USDT: exp(999, 6) }); + expect(p1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, USDT: 0n }); + expect(q1.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, USDT: 0n }); + expect(q1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, USDT: 0n }); + expect(t1.totalSupplyBase).to.be.equal(t0.totalSupplyBase.add(999e6)); + expect(t1.totalBorrowBase).to.be.equal(t0.totalBorrowBase); + // Fee Token logics will cost a bit more gas than standard ERC20 token with no fee calculation + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(151000); + }); + + it('supplies collateral the correct amount in a fee-like situation', async () => { + const assets = defaultAssets(); + // Add FeeToken Collateral to assets on top of default assets + assets['FeeToken'] = { + initial: 1e8, + decimals: 18, + factory: (await ethers.getContractFactory('NonStandardFaucetFeeToken')) as NonStandardFaucetFeeToken__factory, + }; + + const protocol = await makeProtocol({ base: 'USDC', assets: assets }); + const { comet, tokens, users: [alice, bob] } = protocol; + const { FeeToken } = tokens; + + // Set fee to 0.1% + await (FeeToken as NonStandardFaucetFeeToken).setParams(10, 10); + + const _i0 = await FeeToken.allocateTo(bob.address, 2000e8); + const baseAsB = FeeToken.connect(bob); + const cometAsB = comet.connect(bob); + + const t0 = await comet.totalsCollateral(FeeToken.address); + const p0 = await portfolio(protocol, alice.address); + const q0 = await portfolio(protocol, bob.address); + const _a0 = await wait(baseAsB.approve(comet.address, 2000e8)); + const s0 = await wait(cometAsB.supplyTo(alice.address, FeeToken.address, 2000e8)); + const t1 = await comet.totalsCollateral(FeeToken.address); + const p1 = await portfolio(protocol, alice.address); + const q1 = await portfolio(protocol, bob.address); + + expect(event(s0, 0)).to.be.deep.equal({ + Transfer: { + from: bob.address, + to: comet.address, + amount: BigInt(1998e8), + } + }); + expect(event(s0, 1)).to.be.deep.equal({ + SupplyCollateral: { + from: bob.address, + dst: alice.address, + asset: FeeToken.address, + amount: BigInt(1998e8), + } + }); + + expect(p0.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, FeeToken: 0n }); + expect(p0.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, FeeToken: 0n }); + expect(q0.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, FeeToken: 0n }); + expect(q0.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, FeeToken: exp(2000, 8) }); + expect(p1.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, FeeToken: exp(1998, 8) }); + expect(p1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, FeeToken: 0n }); + expect(q1.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, FeeToken: 0n }); + expect(q1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n, FeeToken: 0n }); + expect(t1.totalSupplyAsset).to.be.equal(t0.totalSupplyAsset.add(1998e8)); + // Fee Token logics will cost a bit more gas than standard ERC20 token with no fee calculation + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(186000); }); - it('prevents exceeding the supply cap via re-entrancy', async () => { + it('blocks reentrancy from exceeding the supply cap', async () => { const { comet, tokens, users: [alice, bob] } = await makeProtocol({ assets: { USDC: { @@ -436,10 +554,11 @@ describe('supplyTo', function () { await EVIL.setAttack(attack); await comet.connect(alice).allow(EVIL.address, true); - + await wait(EVIL.connect(alice).approve(comet.address, 75e6)); + await EVIL.allocateTo(alice.address, 75e6); await expect( comet.connect(alice).supplyTo(bob.address, EVIL.address, 75e6) - ).to.be.revertedWith("custom error 'SupplyCapExceeded()'"); + ).to.be.revertedWithCustomError(comet, 'ReentrantCallBlocked'); }); }); diff --git a/test/update-assets-in-test.ts b/test/update-assets-in-test.ts index 347000b4c..7c105f3b9 100644 --- a/test/update-assets-in-test.ts +++ b/test/update-assets-in-test.ts @@ -24,7 +24,7 @@ describe('updateAssetsIn', function () { ]); }); - it('works for up to 15 assets', async () => { + it('works for up to 12 assets', async () => { const { comet, tokens, users } = await makeProtocol({ assets: { USDC: {}, @@ -40,16 +40,13 @@ describe('updateAssetsIn', function () { ASSET10: {}, ASSET11: {}, ASSET12: {}, - ASSET13: {}, - ASSET14: {}, - ASSET15: {}, }, }); const [user] = users; - const asset15address = tokens['ASSET15'].address; + const asset12address = tokens['ASSET12'].address; - await comet.updateAssetsInExternal(user.address, asset15address, 0, 1); - expect(await comet.getAssetList(user.address)).to.deep.equal([asset15address]); + await comet.updateAssetsInExternal(user.address, asset12address, 0, 1); + expect(await comet.getAssetList(user.address)).to.deep.equal([asset12address]); }); it('does not change state when both initialUserBalance and finalUserBalance are 0', async () => { diff --git a/test/withdraw-test.ts b/test/withdraw-test.ts index a28d8735b..7fe2c0f71 100644 --- a/test/withdraw-test.ts +++ b/test/withdraw-test.ts @@ -54,7 +54,7 @@ describe('withdrawTo', function () { expect(q1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(t1.totalSupplyBase).to.be.equal(0n); expect(t1.totalBorrowBase).to.be.equal(0n); - expect(Number(s0.receipt.gasUsed)).to.be.lessThan(100000); + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(106000); }); it('does not emit Transfer for 0 burn', async () => { @@ -144,7 +144,7 @@ describe('withdrawTo', function () { expect(b1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(t1.totalSupplyBase).to.be.equal(0n); expect(t1.totalBorrowBase).to.be.equal(exp(50, 6)); - expect(Number(s0.receipt.gasUsed)).to.be.lessThan(110000); + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(115000); }); it('withdraw max base should withdraw 0 if user has a borrow position', async () => { @@ -190,7 +190,7 @@ describe('withdrawTo', function () { expect(b1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(t1.totalSupplyBase).to.be.equal(t0.totalSupplyBase); expect(t1.totalBorrowBase).to.be.equal(t0.totalBorrowBase); - expect(Number(s0.receipt.gasUsed)).to.be.lessThan(110000); + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(121000); }); // This demonstrates a weird quirk of the present value/principal value rounding down math. @@ -279,7 +279,7 @@ describe('withdrawTo', function () { expect(q1.internal).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(q1.external).to.be.deep.equal({ USDC: 0n, COMP: 0n, WETH: 0n, WBTC: 0n }); expect(t1.totalSupplyAsset).to.be.equal(0n); - expect(Number(s0.receipt.gasUsed)).to.be.lessThan(80000); + expect(Number(s0.receipt.gasUsed)).to.be.lessThan(85000); }); it('calculates base principal correctly', async () => { @@ -475,7 +475,7 @@ describe('withdraw', function () { }); describe('reentrancy', function () { - it('is not broken by malicious reentrancy transferFrom', async () => { + it('blocks malicious reentrant transferFrom', async () => { const { comet, tokens, users: [alice, bob] } = await makeProtocol({ assets: { USDC: { @@ -508,10 +508,10 @@ describe('withdraw', function () { await comet.setCollateralBalance(alice.address, EVIL.address, exp(1, 6)); await comet.connect(alice).allow(EVIL.address, true); - // in callback, EVIL token calls transferFrom(alice.address, bob.address, 1e6) + // In callback, EVIL token calls transferFrom(alice.address, bob.address, 1e6) await expect( comet.connect(alice).withdraw(EVIL.address, 1e6) - ).to.be.revertedWith("custom error 'NotCollateralized()'"); + ).to.be.revertedWithCustomError(comet, 'ReentrantCallBlocked'); // no USDC transferred expect(await USDC.balanceOf(comet.address)).to.eq(100e6); @@ -521,7 +521,7 @@ describe('withdraw', function () { expect(await USDC.balanceOf(bob.address)).to.eq(0); }); - it('is not broken by malicious reentrancy withdrawFrom', async () => { + it('blocks malicious reentrant withdrawFrom', async () => { const { comet, tokens, users: [alice, bob] } = await makeProtocol({ assets: { USDC: { @@ -558,7 +558,7 @@ describe('withdraw', function () { // in callback, EvilToken attempts to withdraw USDC to bob's address await expect( comet.connect(alice).withdraw(EVIL.address, 1e6) - ).to.be.revertedWith("custom error 'NotCollateralized()'"); + ).to.be.revertedWithCustomError(comet, 'ReentrantCallBlocked'); // no USDC transferred expect(await USDC.balanceOf(comet.address)).to.eq(100e6); diff --git a/yarn.lock b/yarn.lock index a3ef4190f..eb9d46f58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3238,13 +3238,14 @@ hardhat-change-network@^0.0.7: resolved "https://registry.yarnpkg.com/hardhat-change-network/-/hardhat-change-network-0.0.7.tgz#9f9b7943ff966515658b70bf5e44bc2f073af402" integrity sha512-Usp9fJan9SOJnOlVcv/jMJDchseE7bIDA5ZsBnracgVk4MiBwkvMqpmLWn5G1aDBvnUCthvS2gO3odfahgkV0Q== -hardhat-contract-sizer@^2.4.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/hardhat-contract-sizer/-/hardhat-contract-sizer-2.6.1.tgz#2b0046a55fa1ec96f19fdab7fde372377401c874" - integrity sha512-b8wS7DBvyo22kmVwpzstAQTdDCThpl/ySBqZh5ga9Yxjf61/uTL12TEg5nl7lDeWy73ntEUzxMwY6XxbQEc2wA== +hardhat-contract-sizer@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/hardhat-contract-sizer/-/hardhat-contract-sizer-2.10.0.tgz#72646f43bfe50e9a5702c9720c9bc3e77d93a2c9" + integrity sha512-QiinUgBD5MqJZJh1hl1jc9dNnpJg7eE/w4/4GEnrcmZJJTDbVFNe3+/3Ep24XqISSkYxRz36czcPHKHd/a0dwA== dependencies: chalk "^4.0.0" cli-table3 "^0.6.0" + strip-ansi "^6.0.0" hardhat-cover@compound-finance/hardhat-cover: version "1.0.0"