diff --git a/.github/workflows/run-scenarios.yaml b/.github/workflows/run-scenarios.yaml index 0d9805358..156f1aa1c 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, mainnet-usdt, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, polygon-usdt, 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/deployments/mainnet/usdt/configuration.json b/deployments/mainnet/usdt/configuration.json new file mode 100644 index 000000000..4d9481493 --- /dev/null +++ b/deployments/mainnet/usdt/configuration.json @@ -0,0 +1,83 @@ +{ + "name": "Compound USDT", + "symbol": "cUSDTv3", + "baseToken": "USDT", + "baseTokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "borrowMin": "100e6", + "governor": "0x6d903f6003cca6255d85cca4d3b5e5146dc33925", + "pauseGuardian": "0xbbf3f1421d886e9b2c5d716b5192ac998af2012c", + "baseTokenPriceFeed": "0x3E7d1eAB13ad0104d2750B8863b489D65364e32D", + "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": "810185185185e0", + "baseBorrowSpeed": "578703703703e0", + "baseMinForRewards": "100000e6" + }, + "rewardTokenAddress": "0xc00e94Cb662C3520282E6f5717214004A7f26888", + "assets": { + "COMP": { + "address": "0xc00e94Cb662C3520282E6f5717214004A7f26888", + "priceFeed": "0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5", + "decimals": "18", + "borrowCF": 0.65, + "liquidateCF": 0.70, + "liquidationFactor": 0.75, + "supplyCap": "100000e18" + }, + "WETH": { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "priceFeed": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "decimals": "18", + "borrowCF": 0.83, + "liquidateCF": 0.9, + "liquidationFactor": 0.95, + "supplyCap": "500000e18" + }, + "WBTC": { + "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "decimals": "8", + "borrowCF": 0.80, + "liquidateCF": 0.85, + "liquidationFactor": 0.95, + "supplyCap": "18000e8" + }, + "UNI": { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "priceFeed": "0x553303d460EE0afB37EdFf9bE42922D8FF63220e", + "decimals": "18", + "borrowCF": 0.75, + "liquidateCF": 0.81, + "liquidationFactor": 0.85, + "supplyCap": "3000000e18" + }, + "LINK": { + "address": "0x514910771af9ca656af840dff83e8264ecf986ca", + "priceFeed": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c", + "decimals": "18", + "borrowCF": 0.79, + "liquidateCF": 0.85, + "liquidationFactor": 0.93, + "supplyCap": "2000000e18" + }, + "wstETH": { + "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "decimals": "18", + "borrowCF": 0.80, + "liquidateCF": 0.85, + "liquidationFactor": 0.95, + "supplyCap": "9000e18" + } + } +} diff --git a/deployments/mainnet/usdt/deploy.ts b/deployments/mainnet/usdt/deploy.ts new file mode 100644 index 000000000..be446f8ea --- /dev/null +++ b/deployments/mainnet/usdt/deploy.ts @@ -0,0 +1,54 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet } from '../../../src/deploy'; + +export default async function deploy(deploymentManager: DeploymentManager, deploySpec: DeploySpec): Promise { + const USDT = await deploymentManager.existing('USDT', '0xdAC17F958D2ee523a2206206994597C13D831ec7'); + const WBTC = await deploymentManager.existing('WBTC', '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'); + const WETH = await deploymentManager.existing('WETH', '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'); + const COMP = await deploymentManager.existing('COMP', '0xc00e94Cb662C3520282E6f5717214004A7f26888'); + const LINK = await deploymentManager.existing('LINK', '0x514910771af9ca656af840dff83e8264ecf986ca'); + const UNI = await deploymentManager.existing('UNI', '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'); + const stETH = await deploymentManager.existing('stETH', '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84'); + const wstETH = await deploymentManager.existing('wstETH', '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'); + + const wbtcScalingPriceFeed = await deploymentManager.deploy( + 'WBTC:priceFeed', + 'pricefeeds/WBTCPriceFeed.sol', + [ + '0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23', // WBTC / BTC price feed + '0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c', // BTC / USD price feed + 8 // decimals + ] + ); + + // WETH Mainnet makret uses custom price feed wstETH / ETH (WstETHPriceFeed.sol) + // We uses this already existing price feed on address https://etherscan.io/address/0x4F67e4d9BD67eFa28236013288737D39AeF48e79 + // As we have wstETH / ETH, we just need ETH / USD to receive wstETH / USD price feed + const wstETHtoUsdPriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'pricefeeds/MultiplicativePriceFeed.sol', + [ + '0x4F67e4d9BD67eFa28236013288737D39AeF48e79', // stETH / ETH price feed + '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', // ETH / USD price feed + 8, + "Custom price feed for wstETH / USD" + ] + ); + + // Import shared contracts from cUSDCv3 + const cometAdmin = await deploymentManager.fromDep('cometAdmin', 'mainnet', 'usdc'); + const $configuratorImpl = await deploymentManager.fromDep('configurator:implementation', 'mainnet', 'usdc'); + const configurator = await deploymentManager.fromDep('configurator', 'mainnet', 'usdc'); + const rewards = await deploymentManager.fromDep('rewards', 'mainnet', 'usdc'); + const bulker = await deploymentManager.fromDep('bulker', 'mainnet', 'weth'); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + + return { + ...deployed, + bulker, + rewards, + COMP + }; +} \ No newline at end of file diff --git a/deployments/mainnet/usdt/migrations/1717664323_configurate_and_ens.ts b/deployments/mainnet/usdt/migrations/1717664323_configurate_and_ens.ts new file mode 100644 index 000000000..05ade4402 --- /dev/null +++ b/deployments/mainnet/usdt/migrations/1717664323_configurate_and_ens.ts @@ -0,0 +1,273 @@ +import { ethers, utils } from 'ethers'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { 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 cUSDTAddress = '0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9'; +const USDTAmount = ethers.BigNumber.from(exp(250_000, 6)); + +export default migration('1717664323_configurate_and_ens', { + async prepare(deploymentManager: DeploymentManager) { + const cometFactory = await deploymentManager.deploy('cometFactory', 'CometFactory.sol', [], true); + return { newFactoryAddress: cometFactory.address }; + }, + + async enact(deploymentManager: DeploymentManager, _, { newFactoryAddress }) { + const trace = deploymentManager.tracer(); + const ethers = deploymentManager.hre.ethers; + + const { + comet, + cometAdmin, + configurator, + rewards, + COMP, + USDT, + governor + } = await deploymentManager.getContracts(); + + const configuration = await getConfigurationStruct(deploymentManager); + + const ENSResolver = await deploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const currentChainId = (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 arbitrum-usdt comet (0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07) + // arbitrum chain id is 42161 + if (!(officialMarketsJSON[42161].find(market => market.baseSymbol === 'USDT'))) { + officialMarketsJSON[42161].push({ baseSymbol: 'USDT', cometAddress: '0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07' }); + } + + // add arbitrum-weth comet (0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486) + // arbitrum chain id is 42161 + if (!(officialMarketsJSON[42161].find(market => market.baseSymbol === 'WETH'))) { + officialMarketsJSON[42161].push({ baseSymbol: 'WETH', cometAddress: '0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486' }); + } + + // add optimism-usdt comet (0x995E394b8B2437aC8Ce61Ee0bC610D617962B214) + // optimism chain id is 10 + if (!(officialMarketsJSON[10].find(market => market.baseSymbol === 'USDT'))) { + officialMarketsJSON[10].push({ baseSymbol: 'USDT', cometAddress: '0x995E394b8B2437aC8Ce61Ee0bC610D617962B214' }); + } + + // add polygon-usdt comet (0xaeB318360f27748Acb200CE616E389A6C9409a07) + // optimism chain id is 137 + if (!(officialMarketsJSON[137].find(market => market.baseSymbol === 'USDT'))) { + officialMarketsJSON[137].push({ baseSymbol: 'USDT', cometAddress: '0xaeB318360f27748Acb200CE616E389A6C9409a07' }); + } + + if (officialMarketsJSON[currentChainId]) { + officialMarketsJSON[currentChainId].push(newMarketObject); + } else { + officialMarketsJSON[currentChainId] = [newMarketObject]; + } + + const _reduceReservesCalldata = utils.defaultAbiCoder.encode( + ['uint256'], + [USDTAmount] + ); + + const actions = [ + // 1. Set the Comet factory in configuration + { + contract: configurator, + signature: 'setFactory(address,address)', + args: [comet.address, newFactoryAddress], + }, + // 2. Set the Comet configuration + { + contract: configurator, + signature: '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)[]))', + args: [comet.address, configuration], + }, + // 3. Deploy Comet and upgrade it to the new implementation + { + contract: cometAdmin, + signature: 'deployAndUpgradeTo(address,address)', + args: [configurator.address, comet.address], + }, + // 4. Set the reward configuration + { + contract: rewards, + signature: 'setRewardConfig(address,address)', + args: [comet.address, COMP.address], + }, + // 5. Get USDT reserves from cUSDT contract + { + target: cUSDTAddress, + signature: '_reduceReserves(uint256)', + calldata: _reduceReservesCalldata + }, + // 6. Transfer USDT to the Comet contract + { + contract: USDT, + signature: 'transfer(address,uint256)', + args: [comet.address, USDTAmount], + }, + // 7. 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 Ethereum Mainnet\n\n## Proposal summary\n\nFranklinDAO team with advice support from Woof Software team proposes deployment of Compound III to the Ethereum Mainnet network. This proposal takes the governance steps recommended and necessary to initialize a Compound III USDT market on Ethereum Mainnet; 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-ethereum/4977/5).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/862), [deploy market GitHub action run](https://github.com/woof-software/comet/actions/runs/9713953378/job/26811913447) and [forum discussion](https://www.comp.xyz/t/launch-usdt-market-on-compound-v3-ethereum/4977).\n\n\n## Proposal Actions\n\nThe first proposal action sets the CometFactory for the new Comet instance in the existing Configurator.\n\nThe second action configures the Comet instance in the Configurator.\n\nThe third action deploys an instance of the newly configured factory and upgrades the Comet instance to use that implementation.\n\nThe fourth action configures the existing rewards contract for the newly deployed Comet instance.\n\nThe fifth 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 sixth action transfers reserves from Timelock to the cUSDTv3 Comet.\n\nThe seventh 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 Ethereum Mainnet cUSDTv3 market.'; + const txn = await deploymentManager.retry( + async () => 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 true; + }, + + async verify(deploymentManager: DeploymentManager) { + const ethers = deploymentManager.hre.ethers; + + const { + comet, + rewards, + timelock, + COMP, + WBTC, + WETH, + UNI, + LINK, + wstETH + } = await deploymentManager.getContracts(); + + // 1. & 2. & 3. + const compInfo = await comet.getAssetInfoByAddress(COMP.address); + const wbtcInfo = await comet.getAssetInfoByAddress(WBTC.address); + const wethInfo = await comet.getAssetInfoByAddress(WETH.address); + const uniInfo = await comet.getAssetInfoByAddress(UNI.address); + const linkInfo = await comet.getAssetInfoByAddress(LINK.address); + const wstETHInfo = await comet.getAssetInfoByAddress(wstETH.address); + + expect(compInfo.supplyCap).to.be.eq(exp(100000, 18)); + expect(wbtcInfo.supplyCap).to.be.eq(exp(18000, 8)); + expect(wethInfo.supplyCap).to.be.eq(exp(500000, 18)); + expect(uniInfo.supplyCap).to.be.eq(exp(3000000, 18)); + expect(linkInfo.supplyCap).to.be.eq(exp(2000000, 18)); + expect(wstETHInfo.supplyCap).to.be.eq(exp(9000, 18)); + + // 4 + 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); + + expect((await comet.pauseGuardian()).toLowerCase()).to.be.eq('0xbbf3f1421d886e9b2c5d716b5192ac998af2012c'); + + // 5. & 6. + expect(await comet.getReserves()).to.be.equal(USDTAmount); + + // 7. + const ENSResolver = await deploymentManager.existing('ENSResolver', ENSResolverAddress); + const ENSRegistry = await deploymentManager.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', + }, + { + baseSymbol: 'USDT', + cometAddress: comet.address, + }, + ], + 137: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF25212E676D1F7F89Cd72fFEe66158f541246445', + }, + { + baseSymbol: 'USDT', + cometAddress: '0xaeB318360f27748Acb200CE616E389A6C9409a07', + }, + ], + 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: '0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07', + }, + ], + 534352: [ + { + baseSymbol: 'USDC', + cometAddress: '0xB2f97c1Bd3bf02f5e74d13f02E3e26F93D77CE44', + }, + ], + 10: [ + { + baseSymbol: 'USDC', + cometAddress: '0x2e44e174f7D53F0212823acC11C01A11d58c5bCB', + }, + { + baseSymbol: 'USDT', + cometAddress: '0x995E394b8B2437aC8Ce61Ee0bC610D617962B214', + }, + ], + }); + + // 8. + expect(await comet.baseTrackingSupplySpeed()).to.be.equal(exp(70 / 86400, 15, 18)); + expect(await comet.baseTrackingBorrowSpeed()).to.be.equal(exp(50 / 86400, 15, 18)); + } +}); \ No newline at end of file diff --git a/deployments/mainnet/usdt/relations.ts b/deployments/mainnet/usdt/relations.ts new file mode 100644 index 000000000..7d7c02b2a --- /dev/null +++ b/deployments/mainnet/usdt/relations.ts @@ -0,0 +1,23 @@ +import { RelationConfigMap } from '../../../plugins/deployment_manager/RelationConfig'; +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + 'wstETH': { + artifact: 'contracts/bulkers/IWstETH.sol', + relations: { + stETH: { + field: async (wstETH) => wstETH.stETH() + } + } + }, + 'USDT': { + artifact: 'contracts/test/NonStandardFaucetFeeToken.sol:NonStandardFeeToken', + }, + 'TetherToken': { + artifact: 'contracts/test/NonStandardFaucetFeeToken.sol:NonStandardFeeToken', + }, + 'AppProxyUpgradeable': { + artifact: 'contracts/ERC20.sol:ERC20', + } +}; \ No newline at end of file diff --git a/deployments/mainnet/usdt/roots.json b/deployments/mainnet/usdt/roots.json new file mode 100644 index 000000000..da8384791 --- /dev/null +++ b/deployments/mainnet/usdt/roots.json @@ -0,0 +1,7 @@ +{ + "comet": "0x3Afdc9BCA9213A35503b077a6072F3D0d5AB0840", + "configurator": "0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3", + "rewards": "0x1B0e765F6224C21223AeA2af16c1C46E38885a40", + "bulker": "0xa397a8C2086C554B531c02E29f3291c9704B00c7", + "COMP": "0xc00e94Cb662C3520282E6f5717214004A7f26888" +} \ No newline at end of file diff --git a/deployments/mainnet/weth/migrations/1718968177_add_weeth_collateral.ts b/deployments/mainnet/weth/migrations/1718968177_add_weeth_collateral.ts new file mode 100644 index 000000000..cd9baa4f2 --- /dev/null +++ b/deployments/mainnet/weth/migrations/1718968177_add_weeth_collateral.ts @@ -0,0 +1,179 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { exp, proposal } from '../../../../src/deploy'; + +const WEETH_ADDRESS = '0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee'; +const WEETH_PRICE_FEED_ADDRESS = '0x5c9C449BbC9a6075A2c061dF312a35fd1E05fF22'; + +export default migration('1718968177_add_weeth_collateral', { + async prepare(deploymentManager: DeploymentManager) { + const _weETHScalingPriceFeed = await deploymentManager.deploy( + 'weETH:priceFeed', + 'pricefeeds/ScalingPriceFeed.sol', + [ + WEETH_PRICE_FEED_ADDRESS, // weETH / ETH price feed + 8 // decimals + ] + ); + return { weETHScalingPriceFeed: _weETHScalingPriceFeed.address }; + }, + + async enact(deploymentManager: DeploymentManager, _, { weETHScalingPriceFeed }) { + + const trace = deploymentManager.tracer(); + + const weETH = await deploymentManager.existing( + 'weETH', + WEETH_ADDRESS, + 'mainnet', + 'contracts/ERC20.sol:ERC20' + ); + const weEthPricefeed = await deploymentManager.existing( + 'weETH:priceFeed', + weETHScalingPriceFeed, + 'mainnet' + ); + + const { + governor, + comet, + cometAdmin, + configurator, + } = await deploymentManager.getContracts(); + + // https://www.comp.xyz/t/add-weeth-market-on-ethereum/5179/3 + const weETHAssetConfig = { + asset: weETH.address, + priceFeed: weEthPricefeed.address, + decimals: await weETH.decimals(), + borrowCollateralFactor: exp(0.82, 18), + liquidateCollateralFactor: exp(0.87, 18), + liquidationFactor: exp(0.92, 18), + supplyCap: exp(22_500, 18), + }; + + const mainnetActions = [ + // 1. Add weETH as asset + { + contract: configurator, + signature: 'addAsset(address,(address,address,uint8,uint64,uint64,uint64,uint128))', + args: [comet.address, weETHAssetConfig], + }, + // 2. Set new Annual Supply Interest Rate Slope High to 100% + { + contract: configurator, + signature: 'setSupplyPerYearInterestRateSlopeHigh(address,uint64)', + args: [ + comet.address, + exp(1, 18) // newSupplyPerYearInterestRateSlopeHigh + ], + }, + // 3. Set new Annual Borrow Interest Rate Slope High to 115% + { + contract: configurator, + signature: 'setBorrowPerYearInterestRateSlopeHigh(address,uint64)', + args: [ + comet.address, + exp(1.15, 18) // newBorrowPerYearInterestRateSlopeHigh + ], + }, + // 4. Deploy and upgrade to a new version of Comet + { + contract: cometAdmin, + signature: 'deployAndUpgradeTo(address,address)', + args: [configurator.address, comet.address], + }, + ]; + + const description = '# Add weETH as collateral into cWETHv3 on Mainnet\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes to add weETH into cWETHv3 on Ethereum network. This proposal takes the governance steps recommended and necessary to update a Compound III WETH market on Ethereum. 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 weETH](https://www.comp.xyz/t/add-weeth-market-on-ethereum/5179/3).\n\nFurther detailed information can be found on the corresponding [proposal pull request](PR - https://github.com/compound-finance/comet/pull/869) and [forum discussion weETH](https://www.comp.xyz/t/add-weeth-market-on-ethereum/5179).\n\n\n## Proposal Actions\n\nThe first proposal action adds weETH asset as collateral with corresponding configurations.\n\nThe second action sets new Annual Supply Interest Rate Slope High to 100%.\n\nThe third action sets new Annual Borrow Interest Rate Slope High to 115%.\n\nThe fourth action deploys and upgrades Comet to a new version.'; + const txn = await deploymentManager.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) { + const { comet, configurator } = await deploymentManager.getContracts(); + + const weETHAssetIndex = Number(await comet.numAssets()) - 1; + + const weETH = await deploymentManager.existing( + 'weETH', + WEETH_ADDRESS, + 'mainnet', + 'contracts/ERC20.sol:ERC20' + ); + + const weETHAssetConfig = { + asset: weETH.address, + priceFeed: '', + decimals: await weETH.decimals(), + borrowCollateralFactor: exp(0.82, 18), + liquidateCollateralFactor: exp(0.87, 18), + liquidationFactor: exp(0.92, 18), + supplyCap: exp(22_500, 18) + }; + + // 1. Compare weETH asset config with Comet and Configurator asset config + const cometWeETHAssetInfo = await comet.getAssetInfoByAddress( + WEETH_ADDRESS + ); + expect(weETHAssetIndex).to.be.equal(cometWeETHAssetInfo.offset); + expect(weETHAssetConfig.asset).to.be.equal(cometWeETHAssetInfo.asset); + expect(exp(1, weETHAssetConfig.decimals)).to.be.equal( + cometWeETHAssetInfo.scale + ); + expect(weETHAssetConfig.borrowCollateralFactor).to.be.equal( + cometWeETHAssetInfo.borrowCollateralFactor + ); + expect(weETHAssetConfig.liquidateCollateralFactor).to.be.equal( + cometWeETHAssetInfo.liquidateCollateralFactor + ); + expect(weETHAssetConfig.liquidationFactor).to.be.equal( + cometWeETHAssetInfo.liquidationFactor + ); + expect(weETHAssetConfig.supplyCap).to.be.equal( + cometWeETHAssetInfo.supplyCap + ); + + const configuratorWeETHAssetConfig = ( + await configurator.getConfiguration(comet.address) + ).assetConfigs[weETHAssetIndex]; + expect(weETHAssetConfig.asset).to.be.equal( + configuratorWeETHAssetConfig.asset + ); + expect(weETHAssetConfig.decimals).to.be.equal( + configuratorWeETHAssetConfig.decimals + ); + expect(weETHAssetConfig.borrowCollateralFactor).to.be.equal( + configuratorWeETHAssetConfig.borrowCollateralFactor + ); + expect(weETHAssetConfig.liquidateCollateralFactor).to.be.equal( + configuratorWeETHAssetConfig.liquidateCollateralFactor + ); + expect(weETHAssetConfig.liquidationFactor).to.be.equal( + configuratorWeETHAssetConfig.liquidationFactor + ); + expect(weETHAssetConfig.supplyCap).to.be.equal( + configuratorWeETHAssetConfig.supplyCap + ); + + // 2. Check new Annual Supply Interest Rate Slope High + expect(exp(1, 18) / BigInt(31_536_000)).to.be.equal(await comet.supplyPerSecondInterestRateSlopeHigh()); + + // 3. Check new Annual Borrow Interest Rate Slope High + expect(exp(1.15, 18) / BigInt(31_536_000)).to.be.equal(await comet.borrowPerSecondInterestRateSlopeHigh()); + }, +}); diff --git a/deployments/mainnet/weth/migrations/1718972942_add_rseth_collateral.ts b/deployments/mainnet/weth/migrations/1718972942_add_rseth_collateral.ts new file mode 100644 index 000000000..dc32ca483 --- /dev/null +++ b/deployments/mainnet/weth/migrations/1718972942_add_rseth_collateral.ts @@ -0,0 +1,177 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { exp, proposal } from '../../../../src/deploy'; + +const RSETH_ADDRESS = '0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7'; +const RSETH_PRICE_FEED_ADDRESS = '0x03c68933f7a3F76875C0bc670a58e69294cDFD01'; + +export default migration('1718972942_add_rseth_collateral', { + async prepare(deploymentManager: DeploymentManager) { + const _rsETHScalingPriceFeed = await deploymentManager.deploy( + 'rsETH:priceFeed', + 'pricefeeds/ScalingPriceFeed.sol', + [ + RSETH_PRICE_FEED_ADDRESS, // rsETH / ETH price feed + 8 // decimals + ] + ); + return { rsETHScalingPriceFeed: _rsETHScalingPriceFeed.address }; + }, + + async enact(deploymentManager: DeploymentManager, _, { rsETHScalingPriceFeed }) { + + const trace = deploymentManager.tracer(); + + const rsETH = await deploymentManager.existing( + 'rsETH', + RSETH_ADDRESS, + 'mainnet', + 'contracts/ERC20.sol:ERC20' + ); + const rsEthPricefeed = await deploymentManager.existing( + 'rsETH:priceFeed', + rsETHScalingPriceFeed, + 'mainnet' + ); + + const { + governor, + comet, + cometAdmin, + configurator, + } = await deploymentManager.getContracts(); + + const rsETHAssetConfig = { + asset: rsETH.address, + priceFeed: rsEthPricefeed.address, + decimals: await rsETH.decimals(), + borrowCollateralFactor: exp(0.80, 18), + liquidateCollateralFactor: exp(0.85, 18), + liquidationFactor: exp(0.9, 18), + supplyCap: exp(5_000, 18), + }; + + const mainnetActions = [ + // 1. Add rsETH as asset + { + contract: configurator, + signature: 'addAsset(address,(address,address,uint8,uint64,uint64,uint64,uint128))', + args: [comet.address, rsETHAssetConfig], + }, + // 2. Set new Annual Supply Interest Rate Slope High to 100% + { + contract: configurator, + signature: 'setSupplyPerYearInterestRateSlopeHigh(address,uint64)', + args: [ + comet.address, + exp(1, 18) // newSupplyPerYearInterestRateSlopeHigh + ], + }, + // 3. Set new Annual Borrow Interest Rate Slope High to 115% + { + contract: configurator, + signature: 'setBorrowPerYearInterestRateSlopeHigh(address,uint64)', + args: [ + comet.address, + exp(1.15, 18) // newBorrowPerYearInterestRateSlopeHigh + ], + }, + // 4. Deploy and upgrade to a new version of Comet + { + contract: cometAdmin, + signature: 'deployAndUpgradeTo(address,address)', + args: [configurator.address, comet.address], + }, + ]; + + const description = '# Add rsETH as collateral into cWETHv3 on Mainnet\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes to add rsETH into cWETHv3 on Ethereum network. This proposal takes the governance steps recommended and necessary to update a Compound III WETH market on Ethereum. 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 rsETH](https://www.comp.xyz/t/add-rseth-market-on-ethereum-mainnet/5118/8).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/870) and [forum discussion rsETH](https://www.comp.xyz/t/add-rseth-market-on-ethereum-mainnet/5118).\n\n\n## Proposal Actions\n\nThe first proposal action adds rsETH asset as collateral with corresponding configurations.\n\nThe second action sets new Annual Supply Interest Rate Slope High to 100%.\n\nThe third action sets new Annual Borrow Interest Rate Slope High to 115%.\n\nThe fourth action deploys and upgrades Comet to a new version.'; + const txn = await deploymentManager.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) { + const { comet, configurator } = await deploymentManager.getContracts(); + + const rsETHAssetIndex = Number(await comet.numAssets()) - 1; + + const rsETH = await deploymentManager.existing( + 'rsETH', + RSETH_ADDRESS, + 'mainnet', + 'contracts/ERC20.sol:ERC20' + ); + + const rsETHAssetConfig = { + asset: rsETH.address, + priceFeed: '', + decimals: await rsETH.decimals(), + borrowCollateralFactor: exp(0.80, 18), + liquidateCollateralFactor: exp(0.85, 18), + liquidationFactor: exp(0.9, 18), + supplyCap: exp(5_000, 18), // 5_000 + }; + + // 1. Compare rsETH asset config with Comet and Configurator asset info + const cometRsETHAssetInfo = await comet.getAssetInfoByAddress( + RSETH_ADDRESS + ); + expect(rsETHAssetIndex).to.be.equal(cometRsETHAssetInfo.offset); + expect(rsETHAssetConfig.asset).to.be.equal(cometRsETHAssetInfo.asset); + expect(exp(1, rsETHAssetConfig.decimals)).to.be.equal( + cometRsETHAssetInfo.scale + ); + expect(rsETHAssetConfig.borrowCollateralFactor).to.be.equal( + cometRsETHAssetInfo.borrowCollateralFactor + ); + expect(rsETHAssetConfig.liquidateCollateralFactor).to.be.equal( + cometRsETHAssetInfo.liquidateCollateralFactor + ); + expect(rsETHAssetConfig.liquidationFactor).to.be.equal( + cometRsETHAssetInfo.liquidationFactor + ); + expect(rsETHAssetConfig.supplyCap).to.be.equal( + cometRsETHAssetInfo.supplyCap + ); + const configuratorRsETHAssetConfig = ( + await configurator.getConfiguration(comet.address) + ).assetConfigs[rsETHAssetIndex]; + expect(rsETHAssetConfig.asset).to.be.equal( + configuratorRsETHAssetConfig.asset + ); + expect(rsETHAssetConfig.decimals).to.be.equal( + configuratorRsETHAssetConfig.decimals + ); + expect(rsETHAssetConfig.borrowCollateralFactor).to.be.equal( + configuratorRsETHAssetConfig.borrowCollateralFactor + ); + expect(rsETHAssetConfig.liquidateCollateralFactor).to.be.equal( + configuratorRsETHAssetConfig.liquidateCollateralFactor + ); + expect(rsETHAssetConfig.liquidationFactor).to.be.equal( + configuratorRsETHAssetConfig.liquidationFactor + ); + expect(rsETHAssetConfig.supplyCap).to.be.equal( + configuratorRsETHAssetConfig.supplyCap + ); + + // 2. Check new Annual Supply Interest Rate Slope High + expect(exp(1, 18) / BigInt(31_536_000)).to.be.equal(await comet.supplyPerSecondInterestRateSlopeHigh()); + + // 3. Check new Annual Borrow Interest Rate Slope High + expect(exp(1.15, 18) / BigInt(31_536_000)).to.be.equal(await comet.borrowPerSecondInterestRateSlopeHigh()); + }, +}); diff --git a/deployments/mainnet/weth/migrations/1718974114_add_oseth_collateral.ts b/deployments/mainnet/weth/migrations/1718974114_add_oseth_collateral.ts new file mode 100644 index 000000000..91e31c6e2 --- /dev/null +++ b/deployments/mainnet/weth/migrations/1718974114_add_oseth_collateral.ts @@ -0,0 +1,153 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { exp, proposal } from '../../../../src/deploy'; + +const OSETH_ADDRESS = '0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38'; +const OSETH_PRICE_FEED_ADDRESS = '0x8023518b2192FB5384DAdc596765B3dD1cdFe471'; + +export default migration('1718974114_add_oseth_collateral', { + async prepare(deploymentManager: DeploymentManager) { + const _osETHScalingPriceFeed = await deploymentManager.deploy( + 'osETH:priceFeed', + 'pricefeeds/ScalingPriceFeed.sol', + [ + OSETH_PRICE_FEED_ADDRESS, // osETH / ETH price feed + 8 // decimals + ] + ); + return { osETHScalingPriceFeed: _osETHScalingPriceFeed.address }; + }, + + async enact(deploymentManager: DeploymentManager, _, { osETHScalingPriceFeed }) { + + const trace = deploymentManager.tracer(); + + const osETH = await deploymentManager.existing( + 'osETH', + OSETH_ADDRESS, + 'mainnet', + 'contracts/ERC20.sol:ERC20' + ); + const osEthPricefeed = await deploymentManager.existing( + 'osETH:priceFeed', + osETHScalingPriceFeed, + 'mainnet' + ); + + const { + governor, + comet, + cometAdmin, + configurator, + } = await deploymentManager.getContracts(); + + const osETHAssetConfig = { + asset: osETH.address, + priceFeed: osEthPricefeed.address, + decimals: await osETH.decimals(), + borrowCollateralFactor: exp(0.80, 18), + liquidateCollateralFactor: exp(0.85, 18), + liquidationFactor: exp(0.9, 18), + supplyCap: exp(10_000, 18), + }; + + const mainnetActions = [ + // 1. Add osETH as asset + { + contract: configurator, + signature: 'addAsset(address,(address,address,uint8,uint64,uint64,uint64,uint128))', + args: [comet.address, osETHAssetConfig], + }, + // 2. Deploy and upgrade to a new version of Comet + { + contract: cometAdmin, + signature: 'deployAndUpgradeTo(address,address)', + args: [configurator.address, comet.address], + }, + ]; + + const description = '# Add osETH as collateral into cWETHv3 on Mainnet\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes to add osETH into cWETHv3 on Ethereum network. This proposal takes the governance steps recommended and necessary to update a Compound III WETH market on Ethereum. 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 osETH](https://www.comp.xyz/t/add-oseth-as-a-collateral-on-ethereum-mainnet/5272/2).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/871) and [forum discussion osETH](https://www.comp.xyz/t/add-oseth-as-a-collateral-on-ethereum-mainnet/5272).\n\n## Price feed\n\nChainlink does not have osETH/ETH price feed on their website, however, Chainlink team ensured us that this address is the native exchange rate [price feed](https://etherscan.io/address/0x8023518b2192FB5384DAdc596765B3dD1cdFe471)\n\n\n## Proposal Actions\n\nThe first action adds osETH asset as collateral with corresponding configurations.\n\nThe second action deploys and upgrades Comet to a new version.'; + const txn = await deploymentManager.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) { + const { comet, configurator } = await deploymentManager.getContracts(); + + const osETHAssetIndex = Number(await comet.numAssets()) - 1; + + const osETH = await deploymentManager.existing( + 'osETH', + OSETH_ADDRESS, + 'mainnet', + 'contracts/ERC20.sol:ERC20' + ); + const osETHAssetConfig = { + asset: osETH.address, + priceFeed: '', + decimals: await osETH.decimals(), + borrowCollateralFactor: exp(0.80, 18), + liquidateCollateralFactor: exp(0.85, 18), + liquidationFactor: exp(0.9, 18), + supplyCap: exp(10_000, 18), // 10_000 + }; + + // 1. Compare osETH asset config with Comet and Configurator asset info + const cometOsETHAssetInfo = await comet.getAssetInfoByAddress( + OSETH_ADDRESS + ); + expect(osETHAssetIndex).to.be.equal(cometOsETHAssetInfo.offset); + expect(osETHAssetConfig.asset).to.be.equal(cometOsETHAssetInfo.asset); + expect(exp(1, osETHAssetConfig.decimals)).to.be.equal( + cometOsETHAssetInfo.scale + ); + expect(osETHAssetConfig.borrowCollateralFactor).to.be.equal( + cometOsETHAssetInfo.borrowCollateralFactor + ); + expect(osETHAssetConfig.liquidateCollateralFactor).to.be.equal( + cometOsETHAssetInfo.liquidateCollateralFactor + ); + expect(osETHAssetConfig.liquidationFactor).to.be.equal( + cometOsETHAssetInfo.liquidationFactor + ); + expect(osETHAssetConfig.supplyCap).to.be.equal( + cometOsETHAssetInfo.supplyCap + ); + + const configuratorOsETHAssetConfig = ( + await configurator.getConfiguration(comet.address) + ).assetConfigs[osETHAssetIndex]; + expect(osETHAssetConfig.asset).to.be.equal( + configuratorOsETHAssetConfig.asset + ); + expect(osETHAssetConfig.decimals).to.be.equal( + configuratorOsETHAssetConfig.decimals + ); + expect(osETHAssetConfig.borrowCollateralFactor).to.be.equal( + configuratorOsETHAssetConfig.borrowCollateralFactor + ); + expect(osETHAssetConfig.liquidateCollateralFactor).to.be.equal( + configuratorOsETHAssetConfig.liquidateCollateralFactor + ); + expect(osETHAssetConfig.liquidationFactor).to.be.equal( + configuratorOsETHAssetConfig.liquidationFactor + ); + expect(osETHAssetConfig.supplyCap).to.be.equal( + configuratorOsETHAssetConfig.supplyCap + ); + }, +}); diff --git a/deployments/mainnet/weth/relations.ts b/deployments/mainnet/weth/relations.ts index d4bbd40b4..40c0368ee 100644 --- a/deployments/mainnet/weth/relations.ts +++ b/deployments/mainnet/weth/relations.ts @@ -14,20 +14,20 @@ export default { 'AppProxyUpgradeable': { artifact: 'contracts/ERC20.sol:ERC20', }, - 'UUPSProxy': { + UUPSProxy: { artifact: 'contracts/ERC20.sol:ERC20', delegates: { field: { - slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' } - }, + } }, - 'TransparentUpgradeableProxy': { + TransparentUpgradeableProxy: { artifact: 'contracts/ERC20.sol:ERC20', delegates: { field: { - slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' } - }, + } }, }; \ No newline at end of file diff --git a/deployments/polygon/usdt/configuration.json b/deployments/polygon/usdt/configuration.json new file mode 100644 index 000000000..b1820a4a5 --- /dev/null +++ b/deployments/polygon/usdt/configuration.json @@ -0,0 +1,76 @@ +{ + "name": "Compound USDT", + "symbol": "cUSDTv3", + "baseToken": "USDT", + "baseTokenAddress": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "baseTokenPriceFeed": "0x0A6513e40db6EB1b165753AD52E80663aeA50545", + "borrowMin": "1e6", + "governor": "0xCC3E7c85Bb0EE4f09380e041fee95a0caeDD4a02", + "pauseGuardian": "0x8Ab717CAC3CbC4934E63825B88442F5810aAF6e5", + "storeFrontPriceFactor": 0.6, + "targetReserves": "20000000e6", + "rates": { + "supplyKink": 0.9, + "supplySlopeLow": 0.075, + "supplySlopeHigh": 3.6, + "supplyBase": 0, + "borrowKink": 0.9, + "borrowSlopeLow": 0.0833, + "borrowSlopeHigh": 4.3, + "borrowBase": 0.015 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "92592592592e0", + "baseBorrowSpeed": "46296296296e0", + "baseMinForRewards": "1000e6" + }, + "rewardTokenAddress": "0x8505b9d2254A7Ae468c0E9dd10Ccea3A837aef5c", + "assets": { + "WMATIC": { + "address": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", + "priceFeed": "0xAB594600376Ec9fD91F8e885dADF0CE036862dE0", + "decimals": "18", + "borrowCF": 0.65, + "liquidateCF": 0.80, + "liquidationFactor": 0.85, + "supplyCap": "5000000e18" + }, + "WETH": { + "address": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", + "priceFeed": "0xF9680D99D6C9589e2a93a78A04A279e509205945", + "decimals": "18", + "borrowCF": 0.80, + "liquidateCF": 0.85, + "liquidationFactor": 0.95, + "supplyCap": "2000e18" + }, + "MaticX": { + "address": "0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6", + "priceFeed": "0x5d37E4b374E6907de8Fc7fb33EE3b0af403C7403", + "decimals": "18", + "borrowCF": 0.60, + "liquidateCF": 0.70, + "liquidationFactor": 0.80, + "supplyCap": "2600000e18" + }, + "stMATIC": { + "address": "0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4", + "priceFeed": "0x97371dF4492605486e23Da797fA68e55Fc38a13f", + "decimals": "18", + "borrowCF": 0.60, + "liquidateCF": 0.70, + "liquidationFactor": 0.80, + "supplyCap": "1500000e18" + }, + "WBTC": { + "address": "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6", + "priceFeed": "0xDE31F8bFBD8c84b5360CFACCa3539B938dd78ae6", + "decimals": "8", + "borrowCF": 0.75, + "liquidateCF": 0.85, + "liquidationFactor": 0.90, + "supplyCap": "90e8" + } + } +} \ No newline at end of file diff --git a/deployments/polygon/usdt/deploy.ts b/deployments/polygon/usdt/deploy.ts new file mode 100644 index 000000000..cc7989ddc --- /dev/null +++ b/deployments/polygon/usdt/deploy.ts @@ -0,0 +1,39 @@ +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 + const USDT = await deploymentManager.existing('USDT', '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', 'polygon'); + const WBTC = await deploymentManager.existing('WBTC', '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6', 'polygon'); + const WETH = await deploymentManager.existing('WETH', '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', 'polygon'); + const WMATIC = await deploymentManager.existing('WMATIC', '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', 'polygon'); + const MaticX = await deploymentManager.existing('MaticX', '0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6', 'polygon'); + const stMATIC = await deploymentManager.existing('stMATIC', '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', 'polygon'); + const COMP = await deploymentManager.existing('COMP', '0x8505b9d2254A7Ae468c0E9dd10Ccea3A837aef5c', 'polygon'); + + const fxChild = await deploymentManager.existing('fxChild', '0x8397259c983751DAf40400790063935a11afa28a', 'polygon'); + + const cometAdmin = await deploymentManager.fromDep('cometAdmin', 'polygon', 'usdc'); + const cometFactory = await deploymentManager.fromDep('cometFactory', 'polygon', 'usdc'); + const $configuratorImpl = await deploymentManager.fromDep('configurator:implementation', 'polygon', 'usdc'); + const configurator = await deploymentManager.fromDep('configurator', 'polygon', 'usdc'); + const rewards = await deploymentManager.fromDep('rewards', 'polygon', 'usdc'); + const bulker = await deploymentManager.fromDep('bulker', 'polygon', 'usdc'); + const localTimelock = await deploymentManager.fromDep('timelock', 'polygon', 'usdc'); + const bridgeReceiver = await deploymentManager.fromDep('bridgeReceiver', 'polygon', 'usdc'); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + + return { + ...deployed, + bridgeReceiver, + bulker, + fxChild, + rewards, + COMP + }; +} \ No newline at end of file diff --git a/deployments/polygon/usdt/migrations/1713283675_configurate_and_ens.ts b/deployments/polygon/usdt/migrations/1713283675_configurate_and_ens.ts new file mode 100644 index 000000000..8b2e0d258 --- /dev/null +++ b/deployments/polygon/usdt/migrations/1713283675_configurate_and_ens.ts @@ -0,0 +1,319 @@ +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 { diffState, getCometConfig } from '../../../../plugins/deployment_manager/DiffState'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; + +const ERC20PredicateAddress = '0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf'; +const RootChainManagerAddress = '0xA0c68C638235ee32657e8f720a23ceC1bFc77C77'; + +const mainnetUSDTAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'; +const polygonCOMPAddress = '0x8505b9d2254A7Ae468c0E9dd10Ccea3A837aef5c'; +const cUSDTAddress = '0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9'; + +const USDTAmountToBridge = ethers.BigNumber.from(exp(10_000, 6)); + +export default migration('1713283675_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', 'polygon', 'usdc'); + const { + bridgeReceiver, + comet, + cometAdmin, + configurator, + rewards, + USDT, + WBTC, + WETH, + WMATIC + } = await deploymentManager.getContracts(); + + const { + fxRoot, + timelock, + governor + } = await govDeploymentManager.getContracts(); + + 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, polygonCOMPAddress] + ); + + 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: 'USDT', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse(await ENSResolver.text(subdomainHash, ENSTextRecordKey)); + + // add arbitrum-usdt comet (0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07) + // arbitrum chain id is 42161 + if (!(officialMarketsJSON[42161].find(market => market.baseSymbol === 'USDT'))) { + officialMarketsJSON[42161].push({ baseSymbol: 'USDT', cometAddress: '0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07' }); + } + + // add arbitrum-weth comet (0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486) + // arbitrum chain id is 42161 + if (!(officialMarketsJSON[42161].find(market => market.baseSymbol === 'WETH'))) { + officialMarketsJSON[42161].push({ baseSymbol: 'WETH', cometAddress: '0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486' }); + } + + // add optimism-usdt comet (0x995E394b8B2437aC8Ce61Ee0bC610D617962B214) + // optimism chain id is 10 + if (!(officialMarketsJSON[10].find(market => market.baseSymbol === 'USDT'))) { + officialMarketsJSON[10].push({ baseSymbol: 'USDT', cometAddress: '0x995E394b8B2437aC8Ce61Ee0bC610D617962B214' }); + } + + if (officialMarketsJSON[polygonChainId]) { + officialMarketsJSON[polygonChainId].push(newMarketObject); + } else { + officialMarketsJSON[polygonChainId] = [newMarketObject]; + } + + const RootChainManager = await deploymentManager.existing( + 'RootChainManager', + RootChainManagerAddress + ); + const USDTMainnet = new Contract( + mainnetUSDTAddress, + [ + 'function balanceOf(address account) external view returns (uint256)', + 'function approve(address,uint256) external' + ], + govDeploymentManager.hre.ethers.provider + ); + + const depositUSDTData = utils.defaultAbiCoder.encode(['uint256'], [USDTAmountToBridge]); + const depositForUSDTCalldata = utils.defaultAbiCoder.encode( + ['address', 'address', 'bytes'], + [comet.address, USDTMainnet.address, depositUSDTData] + ); + 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] + ); + + const mainnetActions = [ + // 1. Set Comet configuration and deployAndUpgradeTo new Comet on Polygon. + { + contract: fxRoot, + signature: 'sendMessageToChild(address,bytes)', + args: [bridgeReceiver.address, l2ProposalData] + }, + // 2. Get USDT reserves from cUSDT contract + { + target: cUSDTAddress, + signature: '_reduceReserves(uint256)', + calldata: _reduceReservesCalldata + }, + // 3. Approve Polygon's ERC20Predicate to take Timelock's USDT (for bridging) + { + contract: USDTMainnet, + signature: 'approve(address,uint256)', + args: [ERC20PredicateAddress, USDTAmountToBridge] + }, + // 4. Bridge USDT from mainnet to Polygon Comet using RootChainManager + { + target: RootChainManager.address, + signature: 'depositFor(address,address,bytes)', + calldata: depositForUSDTCalldata + }, + // 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 cUSDTv3 on Polygon\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes the deployment of Compound III to the Polygon network. This proposal takes the governance steps recommended and necessary to initialize a Compound III USDT market on Polygon; 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/add-market-usdt-on-polygon/5190/3).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/858), [market deployment action](https://github.com/woof-software/comet/actions/runs/9627561011) and [forum discussion](https://www.comp.xyz/t/add-market-usdt-on-polygon/5190).\n\n\n## Proposal Actions\n\nThe first proposal action sets the Comet configuration and deploys a new Comet implementation on Polygon. This sends the encoded `setFactory`, `setConfiguration` and `deployAndUpgradeTo` calls across the bridge to the governance receiver on Polygon. It also calls `setRewardConfig` on the Polygon rewards contract, to establish Polygon’s bridged version of COMP as the reward token for the deployment and set the initial supply speed to be 8 COMP/day and borrow speed to be 4 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 Polygon RootChainManager.\n\nThe third action approves Polygon’s [RootChainManager](https://etherscan.io/address/0xA0c68C638235ee32657e8f720a23ceC1bFc77C77) 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 Polygon RootChainManager 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 Polygon 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, preMigrationBlockNumber: number) { + const ethers = deploymentManager.hre.ethers; + + const { + comet, + rewards, + WMATIC, + WETH, + MaticX, + stMATIC, + WBTC, + COMP + } = await deploymentManager.getContracts(); + + const { + timelock + } = await govDeploymentManager.getContracts(); + + const stateChanges = await diffState(comet, getCometConfig, preMigrationBlockNumber); + expect(stateChanges).to.deep.equal({ + WMATIC: { + supplyCap: exp(5_000_000, 18) + }, + WETH: { + supplyCap: exp(2_000, 18) + }, + MaticX: { + supplyCap: exp(2_600_000, 18), + }, + stMATIC: { + supplyCap: exp(1_500_000, 18) + }, + WBTC: { + supplyCap: exp(90, 8) + }, + baseTrackingSupplySpeed: exp(8 / 86400, 15, 18), + baseTrackingBorrowSpeed: exp(4 / 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', + }, + { + baseSymbol: 'USDT', + cometAddress: comet.address, + } + ], + 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: '0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07', + }, + ], + 534352: [ + { + baseSymbol: 'USDC', + cometAddress: '0xB2f97c1Bd3bf02f5e74d13f02E3e26F93D77CE44', + }, + ], + 10: [ + { + baseSymbol: 'USDC', + cometAddress: '0x2e44e174f7D53F0212823acC11C01A11d58c5bCB', + }, + { + baseSymbol: 'USDT', + cometAddress: '0x995E394b8B2437aC8Ce61Ee0bC610D617962B214', + }, + ], + }); + } +}); diff --git a/deployments/polygon/usdt/relations.ts b/deployments/polygon/usdt/relations.ts new file mode 100644 index 000000000..d5ff91f97 --- /dev/null +++ b/deployments/polygon/usdt/relations.ts @@ -0,0 +1,25 @@ +import { RelationConfigMap } from '../../../plugins/deployment_manager/RelationConfig'; +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + 'governor': { + artifact: 'contracts/bridges/polygon/PolygonBridgeReceiver.sol:PolygonBridgeReceiver', + }, + InitializableImmutableAdminUpgradeabilityProxy: { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + UChildERC20Proxy: { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0xbaab7dbf64751104133af04abc7d9979f0fda3b059a322a8333f533d3f32bf7f', + } + }, + } +}; \ No newline at end of file diff --git a/deployments/polygon/usdt/roots.json b/deployments/polygon/usdt/roots.json new file mode 100644 index 000000000..e3bf5ac24 --- /dev/null +++ b/deployments/polygon/usdt/roots.json @@ -0,0 +1,9 @@ +{ + "comet": "0xaeB318360f27748Acb200CE616E389A6C9409a07", + "configurator": "0x83E0F742cAcBE66349E3701B171eE2487a26e738", + "rewards": "0x45939657d1CA34A8FA39A924B71D28Fe8431e581", + "bridgeReceiver": "0x18281dfC4d00905DA1aaA6731414EABa843c468A", + "bulker": "0x59e242D352ae13166B4987aE5c990C232f7f7CD6", + "fxChild": "0x8397259c983751DAf40400790063935a11afa28a", + "COMP": "0x8505b9d2254A7Ae468c0E9dd10Ccea3A837aef5c" +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 02b603d6e..180b8b4d5 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -25,12 +25,15 @@ import sepoliaWethRelationConfigMap from './deployments/sepolia/weth/relations'; import mumbaiRelationConfigMap from './deployments/mumbai/usdc/relations'; import mainnetRelationConfigMap from './deployments/mainnet/usdc/relations'; import mainnetWethRelationConfigMap from './deployments/mainnet/weth/relations'; +import mainnetUsdtRelationConfigMap from './deployments/mainnet/usdt/relations'; import polygonRelationConfigMap from './deployments/polygon/usdc/relations'; +import polygonUsdtRelationConfigMap from './deployments/polygon/usdt/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 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'; @@ -348,14 +351,17 @@ const config: HardhatUserConfig = { }, mainnet: { usdc: mainnetRelationConfigMap, - weth: mainnetWethRelationConfigMap + weth: mainnetWethRelationConfigMap, + usdt: mainnetUsdtRelationConfigMap }, polygon: { - usdc: polygonRelationConfigMap + usdc: polygonRelationConfigMap, + usdt: polygonUsdtRelationConfigMap }, arbitrum: { 'usdc.e': arbitrumBridgedUsdcRelationConfigMap, usdc: arbitrumNativeUsdcRelationConfigMap, + usdt: arbitrumUsdtRelationConfigMap, weth: arbitrumWETHRelationConfigMap }, 'arbitrum-goerli': { @@ -400,6 +406,11 @@ const config: HardhatUserConfig = { network: 'mainnet', deployment: 'weth', }, + { + name: 'mainnet-usdt', + network: 'mainnet', + deployment: 'usdt' + }, { name: 'development', network: 'hardhat', @@ -442,12 +453,24 @@ const config: HardhatUserConfig = { deployment: 'usdc', auxiliaryBase: 'mainnet' }, + { + name: 'polygon-usdt', + network: 'polygon', + deployment: 'usdt', + auxiliaryBase: 'mainnet' + }, { name: 'arbitrum-usdc.e', network: 'arbitrum', 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/AddMaticxCollateralScenario.ts b/scenario/AddMaticxCollateralScenario.ts index b28f1aabd..783e58d9b 100644 --- a/scenario/AddMaticxCollateralScenario.ts +++ b/scenario/AddMaticxCollateralScenario.ts @@ -14,11 +14,14 @@ const MATICX_WHALES = { // TODO: add ability to run ad hoc scenarios against a single migration, to avoid needing the scenario to do all this setup of // listing an asset + +// This scenario should only run for polygon usdc, cause it simulates adding of the new asset +// It could be removed at all, because all scenarios will run for new collateral. For that should be crated migration script with enacted: false +// While running the scenario, it checks all not enacted migrations, creates proposal, executes it and only after it starts simulations scenario( 'add new asset maticx', { - filter: async (ctx) => - matchesDeployment(ctx, [{ network: 'polygon' }]), + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'polygon', deployment: 'usdc' }]), tokenBalances: { $comet: { $base: '>= 1' }, }, 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..056059c0e 100644 --- a/scenario/BulkerScenario.ts +++ b/scenario/BulkerScenario.ts @@ -8,13 +8,13 @@ import { exp } from '../test/helpers'; scenario( 'Comet#bulker > (non-WETH base) all non-reward actions in one txn', { - filter: async (ctx) => await isBulkerSupported(ctx) && !matchesDeployment(ctx, [{deployment: 'weth'}, {network: 'mumbai'}, { network: 'linea-goerli' }]), + filter: async (ctx) => await isBulkerSupported(ctx) && !matchesDeployment(ctx, [{ deployment: 'weth' }, { network: 'mumbai' }, { network: 'linea-goerli' }]), supplyCaps: { $asset0: 3000, $asset1: 3000, }, tokenBalances: { - albert: { $base: '== 0', $asset0: 3000 }, + albert: { $base: '== 0', $asset0: 3000, $asset1: 3000 }, $comet: { $base: 5000 }, }, }, @@ -24,7 +24,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 toSupplyCollateral = 3000n * collateralScale; @@ -86,7 +89,7 @@ scenario( scenario( 'Comet#bulker > (WETH base) all non-reward actions in one txn', { - filter: async (ctx) => await isBulkerSupported(ctx) && matchesDeployment(ctx, [{deployment: 'weth'}]), + filter: async (ctx) => await isBulkerSupported(ctx) && matchesDeployment(ctx, [{ deployment: 'weth' }]), supplyCaps: { $asset0: 3000, }, @@ -162,13 +165,13 @@ scenario( scenario( 'Comet#bulker > (non-WETH base) all actions in one txn', { - filter: async (ctx) => await isBulkerSupported(ctx) && await isRewardSupported(ctx) && !matchesDeployment(ctx, [{deployment: 'weth'}, { network: 'linea-goerli' }]), + 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 }, + albert: { $base: '== 1000000', $asset0: 3000, $asset1: 3000 }, $comet: { $base: 5000 }, } }, @@ -261,7 +264,7 @@ scenario( scenario( 'Comet#bulker > (WETH base) all actions in one txn', { - filter: async (ctx) => await isBulkerSupported(ctx) && await isRewardSupported(ctx) && matchesDeployment(ctx, [{deployment: 'weth'}]), + filter: async (ctx) => await isBulkerSupported(ctx) && await isRewardSupported(ctx) && matchesDeployment(ctx, [{ deployment: 'weth' }]), supplyCaps: { $asset0: 10, }, diff --git a/scenario/LiquidationBotScenario.ts b/scenario/LiquidationBotScenario.ts index 763468c9e..bf21963b7 100644 --- a/scenario/LiquidationBotScenario.ts +++ b/scenario/LiquidationBotScenario.ts @@ -22,7 +22,7 @@ const sharedAddresses = { uniswapV3Factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984' }; -const addresses: {[chain: string]: LiquidationAddresses} = { +const addresses: { [chain: string]: LiquidationAddresses } = { mainnet: { ...sharedAddresses, sushiswapRouter: '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F', @@ -74,8 +74,9 @@ async function canBeLiquidatedByBot(ctx: CometContext, assetNum: number): Promis // Reason: Most liquidity lives in MATICX / MATIC pools, which the liquidation bot cannot use if the base asset is not MATIC MaticX: { network: 'polygon', - deployments: ['usdc'] - } + deployments: ['usdc', 'usdt'] + }, + }; const comet = await ctx.getComet(); const assetInfo = await comet.getAssetInfo(assetNum); @@ -98,8 +99,9 @@ for (let i = 0; i < MAX_ASSETS; i++) { usdc: 2250000 }, arbitrum: { - 'usdc.e': 10000000, - usdc: 10000000 + 'usdc.e': 10000000, + usdc: 10000000, + usdt: 10000000 } }; const assetAmounts = { @@ -145,7 +147,7 @@ for (let i = 0; i < MAX_ASSETS; i++) { ' == 500', // WBTC ' == 50' - ], + ], usdc: [ // ARB ' == 500000', @@ -165,7 +167,7 @@ for (let i = 0; i < MAX_ASSETS; i++) { upgrade: { targetReserves: exp(20_000, 18) }, - filter: async (ctx) => await isValidAssetIndex(ctx, i) && matchesDeployment(ctx, [{network: 'mainnet'}, {network: 'polygon'}, {network: 'arbitrum'}]) && canBeLiquidatedByBot(ctx, i), + filter: async (ctx) => await isValidAssetIndex(ctx, i) && matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]) && canBeLiquidatedByBot(ctx, i), tokenBalances: async (ctx) => ( { $comet: { @@ -283,8 +285,9 @@ for (let i = 0; i < MAX_ASSETS; i++) { usdc: 3000000 }, arbitrum: { - 'usdc.e': 10000000, - usdc: 10000000 + 'usdc.e': 10000000, + usdc: 10000000, + usdt: 10000000 } }; const assetAmounts = { @@ -330,7 +333,7 @@ for (let i = 0; i < MAX_ASSETS; i++) { ' == 5000', // WBTC ' == 300' - ], + ], usdc: [ // ARB ' == 1000000', @@ -340,6 +343,18 @@ for (let i = 0; i < MAX_ASSETS; i++) { ' == 5000', // WBTC ' == 300' + ], + usdt: [ + // ARB + ' == 1000000', + // WETH + ' == 5000', + // wstETH + ' == 5000', + // WBTC + ' == 300', + // GMX + ' == 10000' ] } }; @@ -386,7 +401,7 @@ for (let i = 0; i < MAX_ASSETS; i++) { exp(500, 18), // WBTC exp(50, 8), - ], + ], usdc: [ // ARB exp(300000, 18), @@ -396,6 +411,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) ] } }; @@ -406,7 +433,7 @@ for (let i = 0; i < MAX_ASSETS; i++) { upgrade: { targetReserves: exp(20_000, 18) }, - filter: async (ctx) => await isValidAssetIndex(ctx, i) && matchesDeployment(ctx, [{network: 'mainnet'}, {network: 'polygon'}, {network: 'arbitrum'}]) && canBeLiquidatedByBot(ctx, i), + filter: async (ctx) => await isValidAssetIndex(ctx, i) && matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]) && canBeLiquidatedByBot(ctx, i), tokenBalances: async (ctx) => ( { $comet: { @@ -510,7 +537,7 @@ for (let i = 0; i < MAX_ASSETS; i++) { scenario( `LiquidationBot > absorbs, but does not attempt to purchase collateral when value is beneath liquidationThreshold`, { - filter: async (ctx) => matchesDeployment(ctx, [{network: 'mainnet'}, {network: 'polygon'}, {network: 'arbitrum'}]), + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]), tokenBalances: { $comet: { $base: 100000 }, }, @@ -592,7 +619,7 @@ scenario( expect(event(tx, 3)).to.deep.equal({ Absorb: { initiator: betty.address, - accounts: [ albert.address ] + accounts: [albert.address] } }); @@ -621,7 +648,7 @@ scenario( scenario( `LiquidationBot > absorbs, but does not attempt to purchase collateral when maxAmountToPurchase=0`, { - filter: async (ctx) => matchesDeployment(ctx, [{network: 'mainnet'}, {network: 'polygon'}, {network: 'arbitrum'}]), + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]), tokenBalances: { $comet: { $base: 100000 }, }, @@ -703,7 +730,7 @@ scenario( expect(event(tx, 3)).to.deep.equal({ Absorb: { initiator: betty.address, - accounts: [ albert.address ] + accounts: [albert.address] } }); @@ -733,13 +760,15 @@ scenario( const baseTokenBalances = { mainnet: { usdc: 2250000, - weth: 20 + weth: 20, + usdt: 2250000 }, }; const assetAmounts = { mainnet: { usdc: ' == 5000', // COMP - weth: ' == 10000', // CB_ETH + weth: ' == 7000', // CB_ETH + usdt: ' == 5000', // COMP }, }; @@ -749,7 +778,7 @@ scenario( upgrade: { targetReserves: exp(20_000, 18) }, - filter: async (ctx) => matchesDeployment(ctx, [{network: 'mainnet'}]), + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }]), tokenBalances: async (ctx) => ( { $comet: { 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..ddbd993d5 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', { @@ -309,7 +506,8 @@ scenario( /ERC20: insufficient allowance/, /transfer amount exceeds spender allowance/, /Dai\/insufficient-allowance/, - symbol === 'WETH' ? /Transaction reverted without a reason string/ : /.^/ + symbol === 'WETH' ? /Transaction reverted without a reason string/ : /.^/, + symbol === 'WMATIC' ? /Transaction reverted without a reason string/ : /.^/ ] ); } @@ -393,7 +591,8 @@ scenario( [ /transfer amount exceeds balance/, /Dai\/insufficient-balance/, - symbol === 'WETH' ? /Transaction reverted without a reason string/ : /.^/ + symbol === 'WETH' ? /Transaction reverted without a reason string/ : /.^/, + symbol === 'WMATIC' ? /Transaction reverted without a reason string/ : /.^/ ] ); } 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..b2d5d4e0b 100644 --- a/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts +++ b/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts @@ -38,7 +38,8 @@ const addresses = { WBTC: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', WETH9: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', CB_ETH: '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704', - WST_ETH: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0' + WST_ETH: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', + USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7' }, goerli: { WETH: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d' @@ -47,7 +48,6 @@ const addresses = { WBTC: '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6', WETH: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', WMATIC: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', - USDT: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', BOB: '0xb0b195aefa3650a6908f15cdac7d92f8a5791b0b' }, arbitrum: { @@ -55,9 +55,9 @@ const addresses = { GMX: '0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a', WETH: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', WBTC: '0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f', - USDT: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', USDC: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', USDC_E: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + USDT: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', rETH: '0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8', wstETH: '0x5979D7b546E38E414F7E9822514be443A4800529', weETH: '0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe' @@ -90,6 +90,10 @@ export const flashLoanPools = { weth: { tokenAddress: addresses.mainnet.USDC, poolFee: 500 + }, + usdt: { + tokenAddress: addresses.mainnet.DAI, + poolFee: 100 } }, goerli: { @@ -102,6 +106,10 @@ export const flashLoanPools = { usdc: { tokenAddress: addresses.polygon.BOB, poolFee: 100 + }, + usdt: { + tokenAddress: addresses.polygon.WBTC, + poolFee: 500 } }, arbitrum: { @@ -113,6 +121,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"