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"