diff --git a/common/configuration.ts b/common/configuration.ts index d88ab6218a..496849d098 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -70,6 +70,7 @@ export interface ITokens { cUSDCv3?: string wcUSDCv3?: string cUSDbCv3?: string + cUSDTv3?: string ONDO?: string sFRAX?: string sDAI?: string @@ -233,6 +234,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { apxETH: '0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6', cUSDCv3: '0xc3d688B66703497DAA19211EEdff47f25384cdc3', wcUSDCv3: '0x27F2f159Fe990Ba83D57f39Fd69661764BEbf37a', + cUSDTv3: '0x3Afdc9BCA9213A35503b077a6072F3D0d5AB0840', ONDO: '0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3', sFRAX: '0xA663B02CF0a4b149d2aD41910CB81e23e1c41c32', sDAI: '0x83f20f44975d03b1b09e64809b757c47f942beea', @@ -553,6 +555,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { USDC: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', USDT: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', cUSDCv3: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + cUSDTv3: '0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07', WETH: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', WBTC: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f', aArbUSDCn: '0x724dc807b04555b71ed48a6896b6f41593b8c637', // aArbUSDCn wraps USDC! diff --git a/contracts/facade/FacadeMonitor.sol b/contracts/facade/FacadeMonitor.sol index d14ad06a5c..28a1e69f1d 100644 --- a/contracts/facade/FacadeMonitor.sol +++ b/contracts/facade/FacadeMonitor.sol @@ -10,7 +10,7 @@ import "../interfaces/IRToken.sol"; import "../libraries/Fixed.sol"; import "../p1/RToken.sol"; import "../plugins/assets/compoundv2/DEPRECATED_CTokenWrapper.sol"; -import "../plugins/assets/compoundv3/ICusdcV3Wrapper.sol"; +import "../plugins/assets/compoundv3/ICFiatV3Wrapper.sol"; import "../plugins/assets/stargate/StargateRewardableWrapper.sol"; import { StaticATokenV3LM } from "../plugins/assets/aave-v3/vendor/StaticATokenV3LM.sol"; import "../plugins/assets/morpho-aave/MorphoAaveV2TokenisedDeposit.sol"; @@ -174,7 +174,7 @@ contract FacadeMonitor is Initializable, OwnableUpgradeable, UUPSUpgradeable, IF backingBalance = (cTokenBal * exchangeRate) / 1e18; availableLiquidity = underlying.balanceOf(address(cToken)); } else if (collType == CollPluginType.COMPOUND_V3) { - ICusdcV3Wrapper cTokenV3Wrapper = ICusdcV3Wrapper(address(erc20)); + ICFiatV3Wrapper cTokenV3Wrapper = ICFiatV3Wrapper(address(erc20)); CometInterface cTokenV3 = CometInterface(address(cTokenV3Wrapper.underlyingComet())); IERC20 underlying = IERC20(cTokenV3.baseToken()); diff --git a/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol b/contracts/plugins/assets/compoundv3/CFiatV3Wrapper.sol similarity index 79% rename from contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol rename to contracts/plugins/assets/compoundv3/CFiatV3Wrapper.sol index 66bd72efd4..e52225b30d 100644 --- a/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol +++ b/contracts/plugins/assets/compoundv3/CFiatV3Wrapper.sol @@ -5,62 +5,67 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./vendor/CometInterface.sol"; import "./WrappedERC20.sol"; import "./vendor/ICometRewards.sol"; -import "./ICusdcV3Wrapper.sol"; +import "./ICFiatV3Wrapper.sol"; import "./CometHelpers.sol"; /** - * @title CusdcV3Wrapper - * @notice Wrapper for cUSDCV3 / COMET that acts as a stable-balance ERC20, instead of rebasing - * token. {comet} will be used as the unit for the underlying token, and {wComet} will be used - * as the unit for wrapped tokens. + * @title CFiatV3Wrapper + * @notice Wrapper for Compound V3 fiat coins such as cUSDCv3, cUSDTv3 / COMET that acts + * as a stable-balance ERC20, instead of rebasing token. {comet} will be used as the unit + * for the underlying token, and {wComet} will be used as the unit for wrapped tokens. */ -contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { +contract CFiatV3Wrapper is ICFiatV3Wrapper, WrappedERC20, CometHelpers { using SafeERC20 for IERC20; - /// From cUSDCv3, used in principal <> present calculations - uint256 public constant TRACKING_INDEX_SCALE = 1e15; - /// From cUSDCv3, scaling factor for USDC rewards - uint256 public constant RESCALE_FACTOR = 1e12; - CometInterface public immutable underlyingComet; ICometRewards public immutable rewardsAddr; IERC20 public immutable rewardERC20; + uint256 public immutable trackingIndexScale; + uint256 public immutable rescaleFactor; + uint8 internal immutable cometDecimals; mapping(address => uint64) public baseTrackingIndex; // uint64 for consistency with CometHelpers mapping(address => uint256) public baseTrackingAccrued; // uint256 to avoid overflow in L:199 mapping(address => uint256) public rewardsClaimed; constructor( - address cusdcv3, + address ctokenv3, address rewardsAddr_, - address rewardERC20_ - ) WrappedERC20("Wrapped cUSDCv3", "wcUSDCv3") { - if (cusdcv3 == address(0)) revert ZeroAddress(); + address rewardERC20_, + string memory name, + string memory symbol + ) WrappedERC20(name, symbol) { + if (ctokenv3 == address(0)) revert ZeroAddress(); rewardsAddr = ICometRewards(rewardsAddr_); rewardERC20 = IERC20(rewardERC20_); - underlyingComet = CometInterface(cusdcv3); + underlyingComet = CometInterface(ctokenv3); + cometDecimals = underlyingComet.decimals(); + // for principal <> present calculations + trackingIndexScale = underlyingComet.trackingIndexScale(); + // scaling factor for rewards + rescaleFactor = 10**(18 - cometDecimals); } /// @return number of decimals - function decimals() public pure override(IERC20Metadata, WrappedERC20) returns (uint8) { - return 6; + function decimals() public view override(IERC20Metadata, WrappedERC20) returns (uint8) { + return cometDecimals; } - /// @param amount {Comet} The amount of cUSDCv3 to deposit + /// @param amount {Comet} The amount of cTokenV3 to deposit function deposit(uint256 amount) external { _deposit(msg.sender, msg.sender, msg.sender, amount); } /// @param dst The dst to deposit into - /// @param amount {Comet} The amount of cUSDCv3 to deposit + /// @param amount {Comet} The amount of cTokenV3 to deposit function depositTo(address dst, uint256 amount) external { _deposit(msg.sender, msg.sender, dst, amount); } /// @param src The address to deposit from /// @param dst The address to deposit to - /// @param amount {Comet} The amount of cUSDCv3 to deposit + /// @param amount {Comet} The amount of cTokenV3 to deposit function depositFrom( address src, address dst, @@ -70,11 +75,11 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { } /// Only called internally to run the deposit logic - /// Takes `amount` fo cUSDCv3 from `src` and deposits to `dst` account in the wrapper. + /// Takes `amount` fo cTokenV3 from `src` and deposits to `dst` account in the wrapper. /// @param operator The address calling the contract (msg.sender) /// @param src The address to deposit from /// @param dst The address to deposit to - /// @param amount {Comet} The amount of cUSDCv3 to deposit + /// @param amount {Comet} The amount of cTokenv3 to deposit function _deposit( address operator, address src, @@ -102,20 +107,20 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { _mint(dst, uint104(wrapperPostPrinc - wrapperPrePrinc)); } - /// @param amount {Comet} The amount of cUSDCv3 to withdraw + /// @param amount {Comet} The amount of cTokenV3 to withdraw function withdraw(uint256 amount) external { _withdraw(msg.sender, msg.sender, msg.sender, amount); } - /// @param dst The address to withdraw cUSDCv3 to - /// @param amount {Comet} The amount of cUSDCv3 to withdraw + /// @param dst The address to withdraw cTokenv3 to + /// @param amount {Comet} The amount of cTokenv3 to withdraw function withdrawTo(address dst, uint256 amount) external { _withdraw(msg.sender, msg.sender, dst, amount); } /// @param src The address to withdraw from - /// @param dst The address to withdraw cUSDCv3 to - /// @param amount {Comet} The amount of cUSDCv3 to withdraw + /// @param dst The address to withdraw cTokenv3 to + /// @param amount {Comet} The amount of cTokenv3 to withdraw function withdrawFrom( address src, address dst, @@ -125,12 +130,12 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { } /// Internally called to run the withdraw logic - /// Withdraws `amount` cUSDCv3 from `src` account in the wrapper and sends to `dst` + /// Withdraws `amount` cTokenv3 from `src` account in the wrapper and sends to `dst` /// @dev Rounds conservatively so as not to over-withdraw from the wrapper /// @param operator The address calling the contract (msg.sender) /// @param src The address to withdraw from - /// @param dst The address to withdraw cUSDCv3 to - /// @param amount {Comet} The amount of cUSDCv3 to withdraw + /// @param dst The address to withdraw cTokenv3 to + /// @param amount {Comet} The amount of cTokenv3 to withdraw function _withdraw( address operator, address src, @@ -195,7 +200,7 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { accrueAccount(src); uint256 claimed = rewardsClaimed[src]; - uint256 accrued = baseTrackingAccrued[src] * RESCALE_FACTOR; + uint256 accrued = baseTrackingAccrued[src] * rescaleFactor; uint256 owed; if (accrued > claimed) { owed = accrued - claimed; @@ -210,20 +215,20 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { emit RewardsClaimed(rewardERC20, owed); } - /// Accure the cUSDCv3 account of the wrapper + /// Accure the cTokenv3 account of the wrapper function accrue() public { underlyingComet.accrueAccount(address(this)); } - /// @param account The address to accrue, first in cUSDCv3, then locally + /// @param account The address to accrue, first in cTokenv3, then locally function accrueAccount(address account) public { underlyingComet.accrueAccount(address(this)); accrueAccountRewards(account); } - /// Get the balance of cUSDCv3 that is represented by the `accounts` wrapper value. - /// @param account The address to calculate the cUSDCv3 balance of - /// @return {Comet} The cUSDCv3 balance that `account` holds in the wrapper + /// Get the balance of cTokenv3 that is represented by the `accounts` wrapper value. + /// @param account The address to calculate the cTokenv3 balance of + /// @return {Comet} The cTokenv3 balance that `account` holds in the wrapper function underlyingBalanceOf(address account) public view returns (uint256) { uint256 balance = balanceOf(account); if (balance == 0) { @@ -239,7 +244,7 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { } /// @param amount The value of {wComet} to convert to {Comet} - /// @return {Comet} The amount of cUSDCv3 represented by `amount of {wComet} + /// @return {Comet} The amount of cTokenv3 represented by `amount of {wComet} function convertStaticToDynamic(uint104 amount) public view returns (uint256) { (uint64 baseSupplyIndex, ) = getUpdatedSupplyIndicies(); return presentValueSupply(baseSupplyIndex, amount); @@ -260,10 +265,10 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { uint256 indexDelta = uint256(trackingSupplyIndex - baseTrackingIndex[account]); uint256 newBaseTrackingAccrued = baseTrackingAccrued[account] + (safe104(balanceOf(account)) * indexDelta) / - TRACKING_INDEX_SCALE; + trackingIndexScale; uint256 claimed = rewardsClaimed[account]; - uint256 accrued = newBaseTrackingAccrued * RESCALE_FACTOR; + uint256 accrued = newBaseTrackingAccrued * rescaleFactor; uint256 owed = accrued > claimed ? accrued - claimed : 0; return owed; @@ -289,7 +294,7 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { (, uint64 trackingSupplyIndex) = getSupplyIndices(); uint256 indexDelta = uint256(trackingSupplyIndex - baseTrackingIndex[account]); - baseTrackingAccrued[account] += (safe104(accountBal) * indexDelta) / TRACKING_INDEX_SCALE; + baseTrackingAccrued[account] += (safe104(accountBal) * indexDelta) / trackingIndexScale; baseTrackingIndex[account] = trackingSupplyIndex; } diff --git a/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol b/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol index 42784057f5..f6537f71c3 100644 --- a/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol +++ b/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol @@ -7,14 +7,14 @@ import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "../../../libraries/Fixed.sol"; import "../AppreciatingFiatCollateral.sol"; import "../OracleLib.sol"; -import "./ICusdcV3Wrapper.sol"; +import "./ICFiatV3Wrapper.sol"; import "./vendor/IComet.sol"; /** * @title CTokenV3Collateral * @notice Collateral plugin for Compound V3, - * tok = wcUSDC - * ref = USDC + * tok = wcToken (wcUSDC, wcUSDT, etc) + * ref = USDC/USDT/etc * tar = USD * UoA = USD */ @@ -31,8 +31,8 @@ contract CTokenV3Collateral is AppreciatingFiatCollateral { AppreciatingFiatCollateral(config, revenueHiding) { require(config.defaultThreshold != 0, "defaultThreshold zero"); - comp = ICusdcV3Wrapper(address(config.erc20)).rewardERC20(); - comet = IComet(address(ICusdcV3Wrapper(address(erc20)).underlyingComet())); + comp = ICFiatV3Wrapper(address(config.erc20)).rewardERC20(); + comet = IComet(address(ICFiatV3Wrapper(address(erc20)).underlyingComet())); cometDecimals = comet.decimals(); } @@ -46,7 +46,7 @@ contract CTokenV3Collateral is AppreciatingFiatCollateral { function underlyingRefPerTok() public view virtual override returns (uint192) { return shiftl_toFix( - ICusdcV3Wrapper(address(erc20)).exchangeRate(), + ICFiatV3Wrapper(address(erc20)).exchangeRate(), -int8(cometDecimals), FLOOR ); @@ -55,7 +55,7 @@ contract CTokenV3Collateral is AppreciatingFiatCollateral { /// Refresh exchange rates and update default status. /// @dev Should not need to override: can handle collateral with variable refPerTok() function refresh() public virtual override { - ICusdcV3Wrapper(address(erc20)).accrue(); + ICFiatV3Wrapper(address(erc20)).accrue(); CollateralStatus oldStatus = status(); diff --git a/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol b/contracts/plugins/assets/compoundv3/ICFiatV3Wrapper.sol similarity index 96% rename from contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol rename to contracts/plugins/assets/compoundv3/ICFiatV3Wrapper.sol index de2ab80ebe..1d73dd6358 100644 --- a/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol +++ b/contracts/plugins/assets/compoundv3/ICFiatV3Wrapper.sol @@ -7,7 +7,7 @@ import "./vendor/CometInterface.sol"; import "./IWrappedERC20.sol"; import "../../../interfaces/IRewardable.sol"; -interface ICusdcV3Wrapper is IWrappedERC20, IRewardable { +interface ICFiatV3Wrapper is IWrappedERC20, IRewardable { struct UserBasic { uint104 principal; uint64 baseTrackingIndex; diff --git a/contracts/plugins/assets/compoundv3/WrappedERC20.sol b/contracts/plugins/assets/compoundv3/WrappedERC20.sol index 290a2da080..138d48e3cd 100644 --- a/contracts/plugins/assets/compoundv3/WrappedERC20.sol +++ b/contracts/plugins/assets/compoundv3/WrappedERC20.sol @@ -78,7 +78,7 @@ abstract contract WrappedERC20 is IWrappedERC20 { /** * @dev Returns the decimals places of the token. */ - function decimals() public pure virtual returns (uint8) { + function decimals() public view virtual returns (uint8) { return 18; } diff --git a/contracts/plugins/mocks/CusdcV3WrapperMock.sol b/contracts/plugins/mocks/CFiatV3WrapperMock.sol similarity index 86% rename from contracts/plugins/mocks/CusdcV3WrapperMock.sol rename to contracts/plugins/mocks/CFiatV3WrapperMock.sol index 3e26d77725..592d0d2569 100644 --- a/contracts/plugins/mocks/CusdcV3WrapperMock.sol +++ b/contracts/plugins/mocks/CFiatV3WrapperMock.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: BlueOak-1.0.0 pragma solidity 0.8.19; -import "../assets/compoundv3/CusdcV3Wrapper.sol"; -import "../assets/compoundv3/ICusdcV3Wrapper.sol"; +import "../assets/compoundv3/CFiatV3Wrapper.sol"; +import "../assets/compoundv3/ICFiatV3Wrapper.sol"; -interface ICusdcV3WrapperMock is ICusdcV3Wrapper { +interface ICFiatV3WrapperMock is ICFiatV3Wrapper { function setMockExchangeRate(bool setMock, uint256 mockValue) external; } -contract CusdcV3WrapperMock { +contract CFiatV3WrapperMock { uint256[20] private __gap; address internal mockTarget; mapping(bytes4 => bool) internal isMocking; @@ -33,7 +33,7 @@ contract CusdcV3WrapperMock { if (isMocking[this.exchangeRate.selector]) { return mockExchangeRate_; } else { - return CusdcV3Wrapper(mockTarget).exchangeRate(); + return CFiatV3Wrapper(mockTarget).exchangeRate(); } } diff --git a/scripts/addresses/1-tmp-assets-collateral.json b/scripts/addresses/1-tmp-assets-collateral.json index 503ea9fb9c..8c666fc0cd 100644 --- a/scripts/addresses/1-tmp-assets-collateral.json +++ b/scripts/addresses/1-tmp-assets-collateral.json @@ -62,7 +62,8 @@ "ETHx": "0x73a36258E6A48D0095D1997Fec7F51e191B4Ec81", "apxETH": "0x05ffDaAA2aF48e1De1CE34d633db018a28e3B3F5", "sUSDe": "0x35081Ca24319835e5f759163F7e75eaB753e0b7E", - "pyUSD": "0xa5cde4fB1132daF8f4a0D3140859271208d944E9" + "pyUSD": "0xa5cde4fB1132daF8f4a0D3140859271208d944E9", + "cUSDTv3": "0x753142cc768C05Ea66A1b92622aFE148Ad44Cb30" }, "erc20s": { "stkAAVE": "0x4da27a545c0c5B758a6BA100e3a049001de870f5", @@ -125,6 +126,7 @@ "ETHx": "0xA35b1B31Ce002FBF2058D22F30f95D405200A15b", "apxETH": "0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6", "sUSDe": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", - "pyUSD": "0x6c3ea9036406852006290770bedfcaba0e23a0e8" + "pyUSD": "0x6c3ea9036406852006290770bedfcaba0e23a0e8", + "cUSDTv3": "0xbeD348315d7327Cd81d26338c11976674825bb14" } -} +} \ No newline at end of file diff --git a/scripts/addresses/42161-tmp-assets-collateral.json b/scripts/addresses/42161-tmp-assets-collateral.json index 2c8f59431c..d11b1e47ba 100644 --- a/scripts/addresses/42161-tmp-assets-collateral.json +++ b/scripts/addresses/42161-tmp-assets-collateral.json @@ -12,7 +12,8 @@ "cUSDCv3": "0x8a5DfEa5cdA35AB374ac558951A3dF1437A6FcA6", "cvxCrvUSDUSDT": "0xf729b03AcbD60c8aF9B449d51444445815a56d0e", "cvxCrvUSDUSDC": "0x57547D29cf0D5B4d31c6c71Ec73b3A8c8416ade6", - "wUSDM": "0xA185a9fd314b61f33F740760a59cc46b31297e30" + "wUSDM": "0xA185a9fd314b61f33F740760a59cc46b31297e30", + "cUSDTv3": "0xF8660F3dd5076F4071e5E4C4D34E60A67d88657D" }, "erc20s": { "COMP": "0x354A6dA3fcde098F8389cad84b0182725c6C91dE", @@ -25,6 +26,7 @@ "ARB": "0x912ce59144191c1204e64559fe8253a0e49e6548", "cvxCrvUSDUSDT": "0xf74d4C9b0F49fb70D8Ff6706ddF39e3a16D61E67", "cvxCrvUSDUSDC": "0xBFEE9F3E015adC754066424AEd535313dc764116", - "wUSDM": "0x57f5e098cad7a3d1eed53991d4d66c45c9af7812" + "wUSDM": "0x57f5e098cad7a3d1eed53991d4d66c45c9af7812", + "cUSDTv3": "0x870fb8352B99Fd5Ed5C0AB18d458dA5933aA5266" } } \ No newline at end of file diff --git a/scripts/addresses/arbitrum-4.4.0/42161-tmp-assets-collateral.json b/scripts/addresses/arbitrum-4.4.0/42161-tmp-assets-collateral.json new file mode 100644 index 0000000000..fb77dd8eb0 --- /dev/null +++ b/scripts/addresses/arbitrum-4.4.0/42161-tmp-assets-collateral.json @@ -0,0 +1,9 @@ +{ + "assets": {}, + "collateral": { + "cUSDTv3": "0xF8660F3dd5076F4071e5E4C4D34E60A67d88657D" + }, + "erc20s": { + "cUSDTv3": "0x870fb8352B99Fd5Ed5C0AB18d458dA5933aA5266" + } +} \ No newline at end of file diff --git a/scripts/addresses/mainnet-4.0.0/1-tmp-assets-collateral.json b/scripts/addresses/mainnet-4.0.0/1-tmp-assets-collateral.json index 0814270f61..4510a3c433 100644 --- a/scripts/addresses/mainnet-4.0.0/1-tmp-assets-collateral.json +++ b/scripts/addresses/mainnet-4.0.0/1-tmp-assets-collateral.json @@ -1,9 +1,11 @@ { "assets": {}, "collateral": { - "ETHx": "0x73a36258E6A48D0095D1997Fec7F51e191B4Ec81" + "ETHx": "0x73a36258E6A48D0095D1997Fec7F51e191B4Ec81", + "cUSDTv3": "0x753142cc768C05Ea66A1b92622aFE148Ad44Cb30" }, "erc20s": { - "ETHx": "0xA35b1B31Ce002FBF2058D22F30f95D405200A15b" + "ETHx": "0xA35b1B31Ce002FBF2058D22F30f95D405200A15b", + "cUSDTv3": "0xbeD348315d7327Cd81d26338c11976674825bb14" } } diff --git a/scripts/deploy.ts b/scripts/deploy.ts index a1347953a3..25808a9657 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -61,6 +61,7 @@ async function main() { 'phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts', 'phase2-assets/collaterals/deploy_flux_finance_collateral.ts', 'phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts', + 'phase2-assets/collaterals/deploy_ctokenv3_usdt_collateral.ts', 'phase2-assets/collaterals/deploy_convex_3pool_collateral.ts', 'phase2-assets/collaterals/deploy_convex_paypool_collateral.ts', 'phase2-assets/collaterals/deploy_convex_crvusd_usdc_collateral.ts', @@ -114,6 +115,7 @@ async function main() { 'phase2-assets/collaterals/deploy_aave_v3_usdc.ts', 'phase2-assets/collaterals/deploy_aave_v3_usdt.ts', 'phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts', + 'phase2-assets/collaterals/deploy_ctokenv3_usdt_collateral.ts', 'phase2-assets/collaterals/deploy_convex_crvusd_usdc_collateral.ts', 'phase2-assets/collaterals/deploy_convex_crvusd_usdt_collateral.ts', 'phase2-assets/collaterals/deploy_usdm.ts', diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdbc_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdbc_collateral.ts index 0b2184a842..5039c42236 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdbc_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdbc_collateral.ts @@ -47,11 +47,13 @@ async function main() { /******** Deploy CompoundV3 USDC - cUSDbCv3 **************************/ - const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory('CusdcV3Wrapper') + const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory('CFiatV3Wrapper') const erc20 = await WrapperFactory.deploy( networkConfig[chainId].tokens.cUSDbCv3, networkConfig[chainId].COMET_REWARDS, - networkConfig[chainId].tokens.COMP + networkConfig[chainId].tokens.COMP, + 'Wrapped cUSDbCv3', + 'wcUSDbCv3' ) await erc20.deployed() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts index 0c81f7aa06..f47415fe6e 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts @@ -42,11 +42,13 @@ async function main() { /******** Deploy CompoundV3 USDC - cUSDCv3 **************************/ - const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory('CusdcV3Wrapper') + const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory('CFiatV3Wrapper') const erc20 = await WrapperFactory.deploy( networkConfig[chainId].tokens.cUSDCv3, networkConfig[chainId].COMET_REWARDS, - networkConfig[chainId].tokens.COMP + networkConfig[chainId].tokens.COMP, + 'Wrapped cUSDCv3', + 'wcUSDCv3' ) await erc20.deployed() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdt_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdt_collateral.ts new file mode 100644 index 0000000000..8f49176e9b --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdt_collateral.ts @@ -0,0 +1,101 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { baseL2Chains, networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { getUsdtOracleError, priceTimeout, revenueHiding } from '../../utils' +import { CTokenV3Collateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Does not exist on Base L2 + if (baseL2Chains.includes(hre.network.name)) { + throw new Error(`Invalid network ${hre.network.name} - Not available on Base`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy CompoundV3 USDT - cUSDTv3 **************************/ + + const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory('CFiatV3Wrapper') + const erc20 = await WrapperFactory.deploy( + networkConfig[chainId].tokens.cUSDTv3, + networkConfig[chainId].COMET_REWARDS, + networkConfig[chainId].tokens.COMP, + 'Wrapped cUSDTv3', + 'wcUSDTv3' + ) + await erc20.deployed() + + console.log(`Deployed wrapper for cUSDTv3 on ${hre.network.name} (${chainId}): ${erc20.address} `) + + const CTokenV3Factory: ContractFactory = await hre.ethers.getContractFactory('CTokenV3Collateral') + + const usdtOracleTimeout = '86400' // 24 hr + const usdtOracleError = getUsdtOracleError(hre.network.name) + + const collateral = await CTokenV3Factory.connect(deployer).deploy( + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDT, + oracleError: usdtOracleError.toString(), + erc20: erc20.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: usdtOracleTimeout, // 24h hr, + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(usdtOracleError).toString(), + delayUntilDefault: bn('86400').toString(), // 24h + }, + revenueHiding.toString() + ) + await collateral.deployed() + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log(`Deployed CompoundV3 USDT to ${hre.network.name} (${chainId}): ${collateral.address}`) + + assetCollDeployments.collateral.cUSDTv3 = collateral.address + assetCollDeployments.erc20s.cUSDTv3 = erc20.address + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_cusdbcv3.ts b/scripts/verification/collateral-plugins/verify_cusdbcv3.ts index 497b3e5be8..788d7c54f6 100644 --- a/scripts/verification/collateral-plugins/verify_cusdbcv3.ts +++ b/scripts/verification/collateral-plugins/verify_cusdbcv3.ts @@ -44,8 +44,10 @@ async function main() { networkConfig[chainId].tokens.cUSDbCv3, networkConfig[chainId].COMET_REWARDS, networkConfig[chainId].tokens.COMP, + 'Wrapped cUSDCv3', + 'wcUSDCv3', ], - 'contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol:CusdcV3Wrapper' + 'contracts/plugins/assets/compoundv3/CFiatV3Wrapper.sol:CFiatV3Wrapper' ) /******** Verify Collateral - wcUSDbCv3 **************************/ diff --git a/scripts/verification/collateral-plugins/verify_cusdcv3.ts b/scripts/verification/collateral-plugins/verify_cusdcv3.ts index ac5dda113b..7f058d39fe 100644 --- a/scripts/verification/collateral-plugins/verify_cusdcv3.ts +++ b/scripts/verification/collateral-plugins/verify_cusdcv3.ts @@ -44,8 +44,10 @@ async function main() { networkConfig[chainId].tokens.cUSDCv3, networkConfig[chainId].COMET_REWARDS, networkConfig[chainId].tokens.COMP, + 'Wrapped cUSDCv3', + 'wcUSDCv3', ], - 'contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol:CusdcV3Wrapper' + 'contracts/plugins/assets/compoundv3/CFiatV3Wrapper.sol:CFiatV3Wrapper' ) /******** Verify Collateral - wcUSDCv3 **************************/ diff --git a/scripts/verification/collateral-plugins/verify_cusdtv3.ts b/scripts/verification/collateral-plugins/verify_cusdtv3.ts new file mode 100644 index 0000000000..3d2f14a28c --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_cusdtv3.ts @@ -0,0 +1,87 @@ +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { baseL2Chains, developmentChains, networkConfig } from '../../../common/configuration' +import { fp, bn } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { + getUsdtOracleError, + priceTimeout, + verifyContract, + revenueHiding, +} from '../../deployment/utils' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + // Does not exist on Base L2 + if (baseL2Chains.includes(hre.network.name)) { + throw new Error(`Invalid network ${hre.network.name} - Not available on Base`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + const collateral = await ethers.getContractAt( + 'CTokenV3Collateral', + deployments.collateral.cUSDTv3 as string + ) + + /******** Verify Wrapper token - wcUSDTv3 **************************/ + + await verifyContract( + chainId, + await collateral.erc20(), + [ + networkConfig[chainId].tokens.cUSDTv3, + networkConfig[chainId].COMET_REWARDS, + networkConfig[chainId].tokens.COMP, + 'Wrapped cUSDTv3', + 'wcUSDTv3', + ], + 'contracts/plugins/assets/compoundv3/CFiatV3Wrapper.sol:CFiatV3Wrapper' + ) + + /******** Verify Collateral - wcUSDTv3 **************************/ + + const usdtOracleTimeout = '86400' // 24 hr + const usdtOracleError = getUsdtOracleError(hre.network.name) + + await verifyContract( + chainId, + deployments.collateral.cUSDTv3, + [ + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDT, + oracleError: usdtOracleError.toString(), + erc20: await collateral.erc20(), + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: usdtOracleTimeout, // 24h hr, + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(usdtOracleError).toString(), + delayUntilDefault: bn('86400').toString(), // 24h + }, + revenueHiding, + ], + 'contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol:CTokenV3Collateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verify_etherscan.ts b/scripts/verify_etherscan.ts index a448a9ee8f..b15b5dec1a 100644 --- a/scripts/verify_etherscan.ts +++ b/scripts/verify_etherscan.ts @@ -66,6 +66,7 @@ async function main() { 'collateral-plugins/verify_curve_stable_rtoken_metapool.ts', 'collateral-plugins/verify_stakedao_usdc_usdcplus.ts', 'collateral-plugins/verify_cusdcv3.ts', + 'collateral-plugins/verify_cusdtv3.ts', 'collateral-plugins/verify_reth.ts', 'collateral-plugins/verify_wsteth.ts', 'collateral-plugins/verify_cbeth.ts', @@ -96,6 +97,7 @@ async function main() { scripts.push( 'collateral-plugins/verify_aave_v3_usdc.ts', 'collateral-plugins/verify_cusdcv3.ts', + 'collateral-plugins/verify_cusdtv3.ts', 'collateral-plugins/verify_convex_crvusd_usdc.ts', 'collateral-plugins/verify_convex_crvusd_usdt.ts', 'collateral-plugins/verify_usdm.ts' diff --git a/tasks/validation/utils/trades.ts b/tasks/validation/utils/trades.ts index c37bf03284..af0a49aa69 100644 --- a/tasks/validation/utils/trades.ts +++ b/tasks/validation/utils/trades.ts @@ -387,7 +387,7 @@ const getERC20Tokens = async ( // Solutions for wrappers without whales if (tokAddress == wcUSDCv3Address || tokAddress == wcUSDCv3AddressOld) { - const wcUSDCv3 = await hre.ethers.getContractAt('CusdcV3Wrapper', tokAddress) + const wcUSDCv3 = await hre.ethers.getContractAt('CFiatV3Wrapper', tokAddress) await whileImpersonating( hre, whales[networkConfig['1'].tokens.cUSDCv3!.toLowerCase()], @@ -462,7 +462,7 @@ const getERC20Tokens = async ( // Solutions for wrappers without whales if (tokAddress == wcUSDCv3Address || tokAddress == wcUSDCv3AddressOld) { - const wcUSDCv3 = await hre.ethers.getContractAt('CusdcV3Wrapper', tokAddress) + const wcUSDCv3 = await hre.ethers.getContractAt('CFiatV3Wrapper', tokAddress) await whileImpersonating( hre, diff --git a/test/monitor/FacadeMonitor.test.ts b/test/monitor/FacadeMonitor.test.ts index a9dcaffdc2..34f0965589 100644 --- a/test/monitor/FacadeMonitor.test.ts +++ b/test/monitor/FacadeMonitor.test.ts @@ -36,7 +36,7 @@ import { TestIRToken, USDCMock, StaticATokenV3LM, - CusdcV3Wrapper, + CFiatV3Wrapper, CometInterface, StargateRewardableWrapper, StargatePoolFiatCollateral, @@ -822,16 +822,18 @@ describeFork(`FacadeMonitor - Integration - Mainnet Forking P${IMPLEMENTATION}`, describe('Compound V3', () => { const issueAmount: BigNumber = bn('1000000e18') - let wcusdcV3: CusdcV3Wrapper + let wcusdcV3: CFiatV3Wrapper beforeEach(async () => { - const CUsdcV3WrapperFactory = await hre.ethers.getContractFactory('CusdcV3Wrapper') + const CUsdcV3WrapperFactory = await hre.ethers.getContractFactory('CFiatV3Wrapper') - wcusdcV3 = ( + wcusdcV3 = ( await CUsdcV3WrapperFactory.deploy( cusdcV3.address, networkConfig[chainId].COMET_REWARDS || '', - networkConfig[chainId].tokens.COMP || '' + networkConfig[chainId].tokens.COMP || '', + 'Wrapped cUSDCv3', + 'wcUSDCv3' ) ) await wcusdcV3.deployed() diff --git a/test/plugins/individual-collateral/compoundv3/CFiatV3Wrapper.test.ts b/test/plugins/individual-collateral/compoundv3/CFiatV3Wrapper.test.ts new file mode 100644 index 0000000000..fac5fdd4fe --- /dev/null +++ b/test/plugins/individual-collateral/compoundv3/CFiatV3Wrapper.test.ts @@ -0,0 +1,865 @@ +import { expect } from 'chai' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import hre, { ethers, network } from 'hardhat' +import { useEnv } from '#/utils/env' +import { whileImpersonating } from '../../../utils/impersonation' +import { advanceTime, advanceBlocks } from '../../../utils/time' +import { allTests, allocateToken, enableRewardsAccrual, mintWcToken } from './helpers' +import { getForkBlock, COMP, REWARDS, getHolder } from './constants' +import { getResetFork } from '../helpers' +import { + ERC20Mock, + CometInterface, + ICFiatV3Wrapper, + CFiatV3Wrapper__factory, +} from '../../../../typechain' +import { bn } from '../../../../common/numbers' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { MAX_UINT256, ZERO_ADDRESS } from '../../../../common/constants' + +for (const curr of allTests) { + const describeFork = + useEnv('FORK') && useEnv('FORK_NETWORK') === curr.forkNetwork ? describe : describe.skip + + describeFork(curr.wrapperName, () => { + let bob: SignerWithAddress + let charles: SignerWithAddress + let don: SignerWithAddress + let token: ERC20Mock + let wcTokenV3: ICFiatV3Wrapper + let cTokenV3: CometInterface + + let chainId: number + + before(async () => { + await getResetFork(getForkBlock(curr.tokenName))() + + chainId = await getChainId(hre) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + }) + + beforeEach(async () => { + ;[, bob, charles, don] = await ethers.getSigners() + ;({ token, wcTokenV3, cTokenV3 } = await loadFixture(curr.fix)) + }) + + it('reverts if deployed with a 0 address', async () => { + const CTokenV3WrapperFactory = ( + await ethers.getContractFactory('CFiatV3Wrapper') + ) + + // TODO there is a chai limitation that cannot catch custom errors during deployment + await expect( + CTokenV3WrapperFactory.deploy( + ZERO_ADDRESS, + REWARDS, + COMP, + curr.wrapperName, + curr.wrapperSymbol + ) + ).to.be.reverted + }) + + it('configuration/state', async () => { + expect(await wcTokenV3.symbol()).to.equal(curr.wrapperSymbol) + expect(await wcTokenV3.name()).to.equal(curr.wrapperName) + expect(await wcTokenV3.totalSupply()).to.equal(bn(0)) + + expect(await wcTokenV3.underlyingComet()).to.equal(cTokenV3.address) + expect(await wcTokenV3.rewardERC20()).to.equal(COMP) + }) + + describe('deposit', () => { + const amount = bn('20000e6') + + beforeEach(async () => { + await allocateToken(bob.address, amount, getHolder(await token.symbol()), token.address) + await token.connect(bob).approve(cTokenV3.address, ethers.constants.MaxUint256) + await cTokenV3.connect(bob).supply(token.address, amount) + await cTokenV3.connect(bob).allow(wcTokenV3.address, true) + }) + + it('deposit', async () => { + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3.connect(bob).deposit(ethers.constants.MaxUint256) + expect(await cTokenV3.balanceOf(bob.address)).to.equal(0) + expect(await token.balanceOf(bob.address)).to.equal(0) + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(expectedAmount) + }) + + it('deposits to own account', async () => { + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3.connect(bob).depositTo(bob.address, ethers.constants.MaxUint256) + expect(await cTokenV3.balanceOf(bob.address)).to.equal(0) + expect(await token.balanceOf(bob.address)).to.equal(0) + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(expectedAmount) + }) + + it('deposits for someone else', async () => { + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3.connect(bob).depositTo(don.address, ethers.constants.MaxUint256) + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(0) + expect(await wcTokenV3.balanceOf(don.address)).to.eq(expectedAmount) + }) + + it('checks for correct approval on deposit - regression test', async () => { + await expect( + wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + ).revertedWithCustomError(wcTokenV3, 'Unauthorized') + + // Provide approval on the wrapper + await wcTokenV3.connect(bob).allow(don.address, true) + + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + + // This should fail even when bob approved wcTokenV3 to spend his tokens, + // because there is no explicit approval of cTokenV3 from bob to don, only + // approval on the wrapper + await expect( + wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(cTokenV3, 'Unauthorized') + + // Add explicit approval of cTokenV3 and retry + await cTokenV3.connect(bob).allow(don.address, true) + await wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(0) + expect(await wcTokenV3.balanceOf(charles.address)).to.eq(expectedAmount) + }) + + it('deposits from a different account', async () => { + expect(await wcTokenV3.balanceOf(charles.address)).to.eq(0) + await expect( + wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + ).revertedWithCustomError(wcTokenV3, 'Unauthorized') + + // Approval has to be on cTokenV3, not the wrapper + await cTokenV3.connect(bob).allow(don.address, true) + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(0) + expect(await wcTokenV3.balanceOf(charles.address)).to.eq(expectedAmount) + }) + + it('deposits less than available cToken', async () => { + const depositAmount = bn('10000e6') + const expectedAmount = await wcTokenV3.convertDynamicToStatic(depositAmount) + await wcTokenV3.connect(bob).depositTo(bob.address, depositAmount) + expect(await cTokenV3.balanceOf(bob.address)).to.be.closeTo(depositAmount, 100) + expect(await token.balanceOf(bob.address)).to.equal(0) + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(expectedAmount, 100) + }) + + it('user that deposits must have same baseTrackingIndex as this Token in Comet', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, amount, bob.address) + expect((await cTokenV3.callStatic.userBasic(wcTokenV3.address)).baseTrackingIndex).to.equal( + await wcTokenV3.baseTrackingIndex(bob.address) + ) + }) + + it('multiple deposits lead to accurate balances', async () => { + let expectedAmount = await wcTokenV3.convertDynamicToStatic(bn('10000e6')) + await wcTokenV3.connect(bob).depositTo(bob.address, bn('10000e6')) + await advanceTime(1000) + expectedAmount = expectedAmount.add(await wcTokenV3.convertDynamicToStatic(bn('10000e6'))) + await wcTokenV3.connect(bob).depositTo(bob.address, bn('10000e6')) + + // The more wcTokenV3 is minted, the higher its value is relative to cTokenV3. + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.be.gt(amount) + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(expectedAmount, 100) + + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.be.closeTo( + await cTokenV3.balanceOf(wcTokenV3.address), + 1 + ) + }) + + it('updates the totalSupply', async () => { + const totalSupplyBefore = await wcTokenV3.totalSupply() + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3.connect(bob).deposit(ethers.constants.MaxUint256) + expect(await wcTokenV3.totalSupply()).to.equal(totalSupplyBefore.add(expectedAmount)) + }) + + it('deposit 0 reverts', async () => { + await expect(wcTokenV3.connect(bob).deposit(0)).to.be.revertedWithCustomError( + wcTokenV3, + 'BadAmount' + ) + }) + + it('depositing 0 balance reverts', async () => { + await cTokenV3.connect(bob).transfer(charles.address, ethers.constants.MaxUint256) + await expect( + wcTokenV3.connect(bob).deposit(ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(wcTokenV3, 'BadAmount') + }) + + it('desposit to zero address reverts', async () => { + await expect( + wcTokenV3.connect(bob).depositTo(ZERO_ADDRESS, ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + }) + }) + + describe('withdraw', () => { + const initwtokenAmt = bn('20000e6') + + beforeEach(async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, initwtokenAmt, bob.address) + await mintWcToken(token, cTokenV3, wcTokenV3, charles, initwtokenAmt, charles.address) + }) + + it('withdraws to own account', async () => { + // bob withdraws ALL + const expectedAmountBob = await wcTokenV3.underlyingBalanceOf(bob.address) + await wcTokenV3.connect(bob).withdraw(ethers.constants.MaxUint256) + const bal = await wcTokenV3.balanceOf(bob.address) + expect(bal).to.closeTo(bn('0'), 10) + expect(await cTokenV3.balanceOf(bob.address)).to.closeTo(expectedAmountBob, 80) + }) + + it('withdraws to a different account', async () => { + const expectedAmount = await wcTokenV3.underlyingBalanceOf(bob.address) + await wcTokenV3.connect(bob).withdrawTo(don.address, ethers.constants.MaxUint256) + expect(await cTokenV3.balanceOf(don.address)).to.closeTo(expectedAmount, 100) + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(bn('0'), 10) + }) + + it('withdraws from a different account', async () => { + const withdrawAmount = await wcTokenV3.underlyingBalanceOf(bob.address) + await expect( + wcTokenV3.connect(charles).withdrawFrom(bob.address, don.address, withdrawAmount) + ).to.be.revertedWithCustomError(wcTokenV3, 'Unauthorized') + + await wcTokenV3.connect(bob).allow(charles.address, true) + await wcTokenV3.connect(charles).withdrawFrom(bob.address, don.address, withdrawAmount) + + expect(await cTokenV3.balanceOf(don.address)).to.closeTo(withdrawAmount, 100) + expect(await cTokenV3.balanceOf(charles.address)).to.closeTo(bn('0'), 50) + + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(bn(0), 150) + }) + + it('withdraws all underlying balance via multiple withdrawals', async () => { + await advanceTime(1000) + const initialBalance = await wcTokenV3.underlyingBalanceOf(bob.address) + const withdrawAmt = bn('10000e6') + await wcTokenV3.connect(bob).withdraw(withdrawAmt) + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.closeTo( + initialBalance.sub(withdrawAmt), + 50 + ) + await advanceTime(1000) + await wcTokenV3.connect(bob).withdraw(ethers.constants.MaxUint256) + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(bn('0'), 10) + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.closeTo(bn('0'), 10) + }) + + it('withdrawing 0 reverts', async () => { + const initialBalance = await wcTokenV3.balanceOf(bob.address) + await expect(wcTokenV3.connect(bob).withdraw(0)).to.be.revertedWithCustomError( + wcTokenV3, + 'BadAmount' + ) + expect(await wcTokenV3.balanceOf(bob.address)).to.equal(initialBalance) + }) + + it('withdrawing 0 balance reverts', async () => { + await expect( + wcTokenV3.connect(don).withdraw(ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(wcTokenV3, 'BadAmount') + }) + + it('handles complex withdrawal sequence', async () => { + let bobWithdrawn = bn('0') + let charlesWithdrawn = bn('0') + let donWithdrawn = bn('0') + + // charles withdraws SOME + const firstWithdrawAmt = bn('15000e6') + charlesWithdrawn = charlesWithdrawn.add(firstWithdrawAmt) + await wcTokenV3.connect(charles).withdraw(firstWithdrawAmt) + const newBalanceCharles = await cTokenV3.balanceOf(charles.address) + expect(newBalanceCharles).to.closeTo(firstWithdrawAmt, 50) + + // don deposits + await mintWcToken(token, cTokenV3, wcTokenV3, don, initwtokenAmt, don.address) + + // bob withdraws SOME + bobWithdrawn = bobWithdrawn.add(bn('12345e6')) + await wcTokenV3.connect(bob).withdraw(bn('12345e6')) + + // don withdraws SOME + donWithdrawn = donWithdrawn.add(bn('123e6')) + await wcTokenV3.connect(don).withdraw(bn('123e6')) + + // charles withdraws ALL + charlesWithdrawn = charlesWithdrawn.add( + await wcTokenV3.underlyingBalanceOf(charles.address) + ) + await wcTokenV3.connect(charles).withdraw(ethers.constants.MaxUint256) + + // don withdraws ALL + donWithdrawn = donWithdrawn.add(await wcTokenV3.underlyingBalanceOf(don.address)) + await wcTokenV3.connect(don).withdraw(ethers.constants.MaxUint256) + + // bob withdraws ALL + bobWithdrawn = bobWithdrawn.add(await wcTokenV3.underlyingBalanceOf(bob.address)) + await wcTokenV3.connect(bob).withdraw(ethers.constants.MaxUint256) + + const bal = await wcTokenV3.balanceOf(bob.address) + + expect(bal).to.closeTo(bn('0'), 10) + expect(await cTokenV3.balanceOf(bob.address)).to.closeTo(bobWithdrawn, 500) + expect(await cTokenV3.balanceOf(charles.address)).to.closeTo(charlesWithdrawn, 500) + expect(await cTokenV3.balanceOf(don.address)).to.closeTo(donWithdrawn, 500) + }) + + it('updates the totalSupply', async () => { + const totalSupplyBefore = await wcTokenV3.totalSupply() + const withdrawAmt = bn('15000e6') + const expectedDiff = await wcTokenV3.convertDynamicToStatic(withdrawAmt) + await wcTokenV3.connect(bob).withdraw(withdrawAmt) + // conservative rounding + expect(await wcTokenV3.totalSupply()).to.be.closeTo(totalSupplyBefore.sub(expectedDiff), 25) + }) + }) + + describe('transfer', () => { + beforeEach(async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + }) + + it('sets max allowance with approval', async () => { + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + + // set approve + await wcTokenV3.connect(bob).allow(don.address, true) + + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) + + // rollback approve + await wcTokenV3.connect(bob).allow(don.address, false) + + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + }) + + it('does not transfer without approval', async () => { + await expect( + wcTokenV3.connect(bob).transferFrom(don.address, bob.address, bn('10000e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'Unauthorized') + + // Perform approval + await wcTokenV3.connect(bob).allow(don.address, true) + + await expect( + wcTokenV3.connect(don).transferFrom(bob.address, don.address, bn('10000e6')) + ).to.emit(wcTokenV3, 'Transfer') + }) + + it('transfer from/to zero address revert', async () => { + await expect( + wcTokenV3.connect(bob).transfer(ZERO_ADDRESS, bn('100e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + + await whileImpersonating(ZERO_ADDRESS, async (signer) => { + await expect( + wcTokenV3.connect(signer).transfer(don.address, bn('100e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + }) + }) + + it('performs validation on transfer amount', async () => { + await expect( + wcTokenV3.connect(bob).transfer(don.address, bn('40000e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'ExceedsBalance') + }) + + it('supports IERC20.approve and performs validations', async () => { + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcTokenV3.hasPermission(bob.address, don.address)).to.equal(false) + + // Cannot set approve to the zero address + await expect( + wcTokenV3.connect(bob).approve(ZERO_ADDRESS, bn('10000e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + + // Can set full allowance with max uint256 + await expect(wcTokenV3.connect(bob).approve(don.address, MAX_UINT256)).to.emit( + wcTokenV3, + 'Approval' + ) + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) + expect(await wcTokenV3.hasPermission(bob.address, don.address)).to.equal(true) + + // Can revert allowance with zero amount + await expect(wcTokenV3.connect(bob).approve(don.address, bn(0))).to.emit( + wcTokenV3, + 'Approval' + ) + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcTokenV3.hasPermission(bob.address, don.address)).to.equal(false) + + // Any other amount reverts + await expect( + wcTokenV3.connect(bob).approve(don.address, bn('10000e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'BadAmount') + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcTokenV3.hasPermission(bob.address, don.address)).to.equal(false) + }) + + it('perform validations on allow', async () => { + await expect( + wcTokenV3.connect(bob).allow(ZERO_ADDRESS, true) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + + await whileImpersonating(ZERO_ADDRESS, async (signer) => { + await expect( + wcTokenV3.connect(signer).allow(don.address, true) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + }) + }) + + it('updates balances and rewards in sender and receiver', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await enableRewardsAccrual(cTokenV3) + await advanceTime(1000) + + await wcTokenV3.accrueAccount(don.address) + await wcTokenV3.accrueAccount(bob.address) + + // Don's rewards accrual should be less than Bob's because he deposited later + expect(await wcTokenV3.baseTrackingAccrued(don.address)).to.be.lt( + await wcTokenV3.baseTrackingAccrued(bob.address) + ) + const bobBal1 = await wcTokenV3.balanceOf(bob.address) + const donBal1 = await wcTokenV3.balanceOf(don.address) + await wcTokenV3.connect(bob).transfer(don.address, bn('10000e6')) + const bobBal2 = await wcTokenV3.balanceOf(bob.address) + const donBal2 = await wcTokenV3.balanceOf(don.address) + + expect(bobBal2).equal(bobBal1.sub(bn('10000e6'))) + expect(donBal2).equal(donBal1.add(bn('10000e6'))) + + await advanceTime(1000) + await wcTokenV3.accrueAccount(don.address) + await wcTokenV3.accrueAccount(bob.address) + + expect(await wcTokenV3.baseTrackingAccrued(don.address)).to.be.gt( + await wcTokenV3.baseTrackingAccrued(bob.address) + ) + + const donsBalance = (await wcTokenV3.underlyingBalanceOf(don.address)).toBigInt() + const bobsBalance = (await wcTokenV3.underlyingBalanceOf(bob.address)).toBigInt() + expect(donsBalance).to.be.gt(bobsBalance) + const totalBalances = donsBalance + bobsBalance + + // Rounding in favor of the Wrapped Token is happening here. Amount is negligible + expect(totalBalances).to.be.closeTo(await cTokenV3.balanceOf(wcTokenV3.address), 1) + }) + + it('does not update the total supply', async () => { + const totalSupplyBefore = await wcTokenV3.totalSupply() + await wcTokenV3.connect(bob).transfer(don.address, bn('10000e6')) + expect(totalSupplyBefore).to.equal(await wcTokenV3.totalSupply()) + }) + }) + + describe('accure / accrueAccount', () => { + it('accrues internally for the comet', async () => { + const initAccrueTime = (await cTokenV3.totalsBasic()).lastAccrualTime + await wcTokenV3.accrue() + const endAccrueTime = (await cTokenV3.totalsBasic()).lastAccrualTime + expect(endAccrueTime).gt(initAccrueTime) + }) + + it('accrues rewards over time', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + expect(await wcTokenV3.baseTrackingAccrued(bob.address)).to.eq(0) + await enableRewardsAccrual(cTokenV3) + await advanceTime(1000) + + await wcTokenV3.accrueAccount(bob.address) + expect(await wcTokenV3.baseTrackingAccrued(bob.address)).to.be.gt(0) + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.eq( + await cTokenV3.balanceOf(wcTokenV3.address) + ) + }) + + it('does not accrue when accruals are not enabled in Comet', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + expect(await wcTokenV3.baseTrackingAccrued(bob.address)).to.eq(0) + + await advanceTime(1000) + expect(await wcTokenV3.baseTrackingAccrued(bob.address)).to.eq(0) + }) + }) + + describe('underlying balance', () => { + it('returns the correct amount of decimals', async () => { + const decimals = await wcTokenV3.decimals() + expect(decimals).to.equal(6) + }) + + it('returns underlying balance of user which includes revenue', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + const wrappedBalance = await wcTokenV3.balanceOf(bob.address) + await advanceTime(1000) + expect(wrappedBalance).to.equal(await wcTokenV3.balanceOf(bob.address)) + // Underlying balance increases over time and is greater than the balance in the wrapped token + expect(wrappedBalance).to.be.lt(await wcTokenV3.underlyingBalanceOf(bob.address)) + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.eq( + await cTokenV3.balanceOf(wcTokenV3.address) + ) + + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + await advanceTime(1000) + const totalBalances = (await wcTokenV3.underlyingBalanceOf(don.address)).add( + await wcTokenV3.underlyingBalanceOf(bob.address) + ) + + const contractBalance = await cTokenV3.balanceOf(wcTokenV3.address) + expect(totalBalances).to.closeTo(contractBalance, 10) + expect(totalBalances).to.lte(contractBalance) + }) + + it('returns 0 when user has no balance', async () => { + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.equal(0) + }) + + it('also accrues account in Comet to ensure that global indices are updated', async () => { + await enableRewardsAccrual(cTokenV3) + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + const oldTrackingSupplyIndex = (await cTokenV3.totalsBasic()).trackingSupplyIndex + + await advanceTime(1000) + await wcTokenV3.accrueAccount(bob.address) + expect(oldTrackingSupplyIndex).to.be.lessThan( + (await cTokenV3.totalsBasic()).trackingSupplyIndex + ) + }) + + it('matches balance in cTokenV3', async () => { + // mint some ctoken to bob + const amount = bn('20000e6') + await allocateToken(bob.address, amount, getHolder(await token.symbol()), token.address) + await token.connect(bob).approve(cTokenV3.address, ethers.constants.MaxUint256) + await cTokenV3.connect(bob).supply(token.address, amount) + + // mint some wctoken to bob, charles, don + await mintWcToken(token, cTokenV3, wcTokenV3, bob, amount, bob.address) + await mintWcToken(token, cTokenV3, wcTokenV3, charles, amount, charles.address) + await mintWcToken(token, cTokenV3, wcTokenV3, don, amount, don.address) + await advanceTime(100000) + + let totalBalances = (await wcTokenV3.underlyingBalanceOf(don.address)) + .add(await wcTokenV3.underlyingBalanceOf(bob.address)) + .add(await wcTokenV3.underlyingBalanceOf(charles.address)) + let contractBalance = await cTokenV3.balanceOf(wcTokenV3.address) + expect(totalBalances).to.be.closeTo(contractBalance, 10) + expect(totalBalances).to.be.lte(contractBalance) + + const bobBal = await wcTokenV3.balanceOf(bob.address) + await wcTokenV3.connect(bob).withdraw(bobBal) + await wcTokenV3.connect(don).withdraw(bn('10000e6')) + + totalBalances = (await wcTokenV3.underlyingBalanceOf(don.address)) + .add(await wcTokenV3.underlyingBalanceOf(bob.address)) + .add(await wcTokenV3.underlyingBalanceOf(charles.address)) + contractBalance = await cTokenV3.balanceOf(wcTokenV3.address) + expect(totalBalances).to.be.closeTo(contractBalance, 10) + expect(totalBalances).to.be.lte(contractBalance) + }) + }) + + describe('exchange rate', () => { + it('returns the correct exchange rate with 0 balance', async () => { + const totalsBasic = await cTokenV3.totalsBasic() + const baseIndexScale = await cTokenV3.baseIndexScale() + const expectedExchangeRate = totalsBasic.baseSupplyIndex.mul(bn('1e6')).div(baseIndexScale) + expect(await cTokenV3.balanceOf(wcTokenV3.address)).to.equal(0) + expect(await wcTokenV3.exchangeRate()).to.be.closeTo(expectedExchangeRate, 10) + }) + + it('returns the correct exchange rate with a positive balance', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + const totalsBasic = await cTokenV3.totalsBasic() + const baseIndexScale = await cTokenV3.baseIndexScale() + const expectedExchangeRate = totalsBasic.baseSupplyIndex.mul(bn('1e6')).div(baseIndexScale) + expect(await wcTokenV3.exchangeRate()).to.equal(expectedExchangeRate) + }) + + it('current exchange rate is a ratio of total underlying balance and total supply', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + const totalSupply = await wcTokenV3.totalSupply() + const underlyingBalance = await cTokenV3.balanceOf(wcTokenV3.address) + expect(await wcTokenV3.exchangeRate()).to.equal( + underlyingBalance.mul(bn('1e6')).div(totalSupply) + ) + }) + }) + + describe('claiming rewards', () => { + beforeEach(async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + }) + + it('does not claim rewards when user has no permission', async () => { + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3) + await expect( + wcTokenV3.connect(don).claimTo(bob.address, bob.address) + ).to.be.revertedWithCustomError(wcTokenV3, 'Unauthorized') + + await wcTokenV3.connect(bob).allow(don.address, true) + expect(await wcTokenV3.isAllowed(bob.address, don.address)).to.eq(true) + await expect(wcTokenV3.connect(don).claimTo(bob.address, bob.address)).to.emit( + wcTokenV3, + 'RewardsClaimed' + ) + }) + + it('regression test: able to claim rewards even when they are big without overflow', async () => { + // Nov 28 2023: uint64 math in CFiatV3Wrapper contract results in overflow when COMP rewards are even moderately large + + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3, bn('2e18')) // enough to revert on uint64 implementation + + await expect(wcTokenV3.connect(bob).claimRewards()).to.emit(wcTokenV3, 'RewardsClaimed') + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) + }) + + it('claims rewards and sends to claimer (claimTo)', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3) + + await expect(wcTokenV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( + wcTokenV3, + 'RewardsClaimed' + ) + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) + }) + + it('caps at balance to avoid reverts when claiming rewards (claimTo)', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3) + + // Accrue multiple times + for (let i = 0; i < 10; i++) { + await advanceTime(1000) + await wcTokenV3.accrue() + } + + // Get rewards from Comet + const cometRewards = await ethers.getContractAt('ICometRewards', REWARDS) + await whileImpersonating(wcTokenV3.address, async (signer) => { + await cometRewards + .connect(signer) + .claimTo(cTokenV3.address, wcTokenV3.address, wcTokenV3.address, true) + }) + + // Accrue individual account + await wcTokenV3.accrueAccount(bob.address) + + // Due to rounding, balance is smaller that owed + const owed = await wcTokenV3.getRewardOwed(bob.address) + const bal = await compToken.balanceOf(wcTokenV3.address) + expect(owed).to.be.greaterThan(bal) + + // Should still be able to claimTo (caps at balance) + const balanceBobPrev = await compToken.balanceOf(bob.address) + await expect(wcTokenV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( + wcTokenV3, + 'RewardsClaimed' + ) + + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(balanceBobPrev) + }) + + it('claims rewards and sends to claimer (claimRewards)', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3) + + await expect(wcTokenV3.connect(bob).claimRewards()).to.emit(wcTokenV3, 'RewardsClaimed') + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) + }) + + it('claims rewards by participation', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await enableRewardsAccrual(cTokenV3) + await advanceTime(1000) + + expect(await compToken.balanceOf(bob.address)).to.equal(0) + expect(await compToken.balanceOf(don.address)).to.equal(0) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + + // claim at the same time + await network.provider.send('evm_setAutomine', [false]) + await wcTokenV3.connect(bob).claimTo(bob.address, bob.address) + await wcTokenV3.connect(don).claimTo(don.address, don.address) + await network.provider.send('evm_setAutomine', [true]) + await advanceBlocks(1) + + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) + const balanceBob = await compToken.balanceOf(bob.address) + const balanceDon = await compToken.balanceOf(don.address) + expect(balanceDon).lessThanOrEqual(balanceBob) + expect(balanceBob).to.be.closeTo(balanceDon, balanceBob.mul(5).div(1000)) // within 0.5% + }) + + // In this forked block, rewards accrual is not yet enabled in Comet + // Only applies to Mainnet forks (L1) + it('claims no rewards when rewards accrual is not enabled', async () => { + await enableRewardsAccrual(cTokenV3, bn(0)) + + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + await advanceTime(1000) + await wcTokenV3.connect(bob).claimTo(bob.address, bob.address) + expect(await compToken.balanceOf(bob.address)).to.equal(0) + }) + + it('returns reward owed after accrual and claims', async () => { + await enableRewardsAccrual(cTokenV3) + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await advanceTime(1000) + await advanceBlocks(1) + + const bobsReward = await wcTokenV3.getRewardOwed(bob.address) + const donsReward = await wcTokenV3.getRewardOwed(don.address) + + expect(bobsReward).to.be.greaterThan(donsReward) + + await wcTokenV3.connect(bob).claimTo(bob.address, bob.address) + expect(await wcTokenV3.getRewardOwed(bob.address)).to.equal(0) + + await advanceTime(1000) + expect(await wcTokenV3.getRewardOwed(bob.address)).to.be.greaterThan(0) + }) + + it('accrues the account on deposit and withdraw', async () => { + await enableRewardsAccrual(cTokenV3) + await advanceTime(1200) + await advanceBlocks(100) + const expectedReward = await wcTokenV3.getRewardOwed(bob.address) + await advanceTime(12) + await advanceBlocks(1) + const newExpectedReward = await wcTokenV3.getRewardOwed(bob.address) + // marginal increase in exepected reward due to time passed + expect(newExpectedReward).gt(expectedReward) + + await advanceTime(1200) + await wcTokenV3.connect(bob).withdraw(ethers.constants.MaxUint256) + const nextExpectedReward = await wcTokenV3.getRewardOwed(bob.address) + await advanceTime(1200) + const lastExpectedReward = await wcTokenV3.getRewardOwed(bob.address) + // expected reward stays the same because account is empty + expect(lastExpectedReward).to.eq(nextExpectedReward) + }) + }) + + describe('baseTrackingAccrued', () => { + it('matches baseTrackingAccrued in cTokenV3 over time', async () => { + await enableRewardsAccrual(cTokenV3) + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + let wrappedTokenAccrued = await cTokenV3.baseTrackingAccrued(wcTokenV3.address) + expect(wrappedTokenAccrued).to.equal(await wcTokenV3.baseTrackingAccrued(bob.address)) + + await wcTokenV3.accrueAccount(bob.address) + + wrappedTokenAccrued = await cTokenV3.baseTrackingAccrued(wcTokenV3.address) + expect(wrappedTokenAccrued).to.equal(await wcTokenV3.baseTrackingAccrued(bob.address)) + expect((await cTokenV3.callStatic.userBasic(wcTokenV3.address)).baseTrackingIndex).to.equal( + await wcTokenV3.baseTrackingIndex(bob.address) + ) + + await mintWcToken(token, cTokenV3, wcTokenV3, charles, bn('20000e6'), charles.address) + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await advanceTime(1000) + + await network.provider.send('evm_setAutomine', [false]) + await wcTokenV3.accrueAccount(bob.address) + await wcTokenV3.accrueAccount(charles.address) + await wcTokenV3.accrueAccount(don.address) + await advanceBlocks(1) + await network.provider.send('evm_setAutomine', [true]) + + // All users' total accrued rewards in Wrapped cToken should closely match Wrapped cToken's + // accrued rewards in cToken + const bobBTA = await wcTokenV3.baseTrackingAccrued(bob.address) + const charlesBTA = await wcTokenV3.baseTrackingAccrued(charles.address) + const donBTA = await wcTokenV3.baseTrackingAccrued(don.address) + const totalUsersAccrued = bobBTA.add(charlesBTA).add(donBTA) + wrappedTokenAccrued = await cTokenV3.baseTrackingAccrued(wcTokenV3.address) + expect(wrappedTokenAccrued).to.be.closeTo(totalUsersAccrued, 5) + }) + + it('matches baseTrackingAccrued in cTokenV3 after withdrawals', async () => { + await enableRewardsAccrual(cTokenV3) + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await advanceTime(1000) + await wcTokenV3.connect(bob).withdrawTo(bob.address, bn('10000e6')) + + await advanceTime(1000) + + await network.provider.send('evm_setAutomine', [false]) + await wcTokenV3.accrueAccount(bob.address) + await wcTokenV3.accrueAccount(don.address) + await advanceBlocks(1) + await network.provider.send('evm_setAutomine', [true]) + + // All users' total accrued rewards in Wrapped cToken should match Wrapped cToken's accrued rewards in cToken. + const totalUsersAccrued = (await wcTokenV3.baseTrackingAccrued(bob.address)).add( + await wcTokenV3.baseTrackingAccrued(don.address) + ) + const wrappedTokenAccrued = await cTokenV3.baseTrackingAccrued(wcTokenV3.address) + expect(wrappedTokenAccrued).to.closeTo(totalUsersAccrued, 10) + // expect(wrappedTokenAccrued).to.eq(totalUsersAccrued) + }) + }) + }) +} diff --git a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts index 78c334c90c..c5c37fea11 100644 --- a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts +++ b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts @@ -5,23 +5,23 @@ import { MintCollateralFunc, CollateralStatus, } from '../pluginTestTypes' -import { mintWcUSDC, makewCSUDC, resetFork, enableRewardsAccrual } from './helpers' +import { allTests, CTokenV3Enumeration, mintWcToken, enableRewardsAccrual } from './helpers' import { ethers, network } from 'hardhat' import { ContractFactory, BigNumberish, BigNumber } from 'ethers' import { ERC20Mock, MockV3Aggregator, CometInterface, - ICusdcV3Wrapper, - ICusdcV3WrapperMock, - CusdcV3WrapperMock, - CusdcV3Wrapper__factory, - CusdcV3WrapperMock__factory, + CFiatV3Wrapper__factory, + CFiatV3WrapperMock__factory, MockV3Aggregator__factory, CometMock, CometMock__factory, TestICollateral, + ICFiatV3Wrapper, + CFiatV3WrapperMock, } from '../../../../typechain' +import { getResetFork } from '../helpers' import { pushOracleForward } from '../../../utils/oracles' import { bn, fp } from '../../../../common/numbers' import { MAX_UINT48 } from '../../../../common/constants' @@ -29,19 +29,16 @@ import { expect } from 'chai' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { advanceBlocks, getLatestBlockTimestamp, setNextBlockTimestamp } from '../../../utils/time' import { - forkNetwork, ORACLE_ERROR, ORACLE_TIMEOUT, PRICE_TIMEOUT, COMP, - CUSDC_V3, - USDC_USD_PRICE_FEED, MAX_TRADE_VOL, DEFAULT_THRESHOLD, DELAY_UNTIL_DEFAULT, REWARDS, - USDC, COMET_EXT, + getForkBlock, } from './constants' import { setCode } from '@nomicfoundation/hardhat-network-helpers' @@ -50,16 +47,16 @@ import { setCode } from '@nomicfoundation/hardhat-network-helpers' */ interface CometCollateralFixtureContext extends CollateralFixtureContext { - cusdcV3: CometInterface - wcusdcV3: ICusdcV3Wrapper - usdc: ERC20Mock + cTokenV3: CometInterface + wcTokenV3: ICFiatV3Wrapper + token: ERC20Mock } interface CometCollateralFixtureContextMockComet extends CollateralFixtureContext { - cusdcV3: CometMock - wcusdcV3: ICusdcV3Wrapper - usdc: ERC20Mock - wcusdcV3Mock: CusdcV3WrapperMock + cTokenV3: CometMock + wcTokenV3: ICFiatV3Wrapper + token: ERC20Mock + wcTokenV3Mock: CFiatV3WrapperMock } interface CometCollateralOpts extends CollateralOpts { @@ -72,324 +69,327 @@ interface CometCollateralOpts extends CollateralOpts { const chainlinkDefaultAnswer = bn('1e8') -export const defaultCometCollateralOpts: CometCollateralOpts = { - erc20: CUSDC_V3, - targetName: ethers.utils.formatBytes32String('USD'), - rewardERC20: COMP, - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: USDC_USD_PRICE_FEED, - oracleTimeout: ORACLE_TIMEOUT, - oracleError: ORACLE_ERROR, - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - revenueHiding: fp('0'), -} +allTests.forEach((curr: CTokenV3Enumeration) => { + const defaultCometCollateralOpts: CometCollateralOpts = { + erc20: curr.cTokenV3, + targetName: ethers.utils.formatBytes32String('USD'), + rewardERC20: COMP, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: curr.chainlinkFeed, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), + } -export const deployCollateral = async ( - opts: CometCollateralOpts = {} -): Promise => { - opts = { ...defaultCometCollateralOpts, ...opts } - - const CTokenV3CollateralFactory: ContractFactory = await ethers.getContractFactory( - 'CTokenV3Collateral' - ) - - const collateral = await CTokenV3CollateralFactory.deploy( - { - erc20: opts.erc20, - targetName: opts.targetName, - priceTimeout: opts.priceTimeout, - chainlinkFeed: opts.chainlinkFeed, - oracleError: opts.oracleError, - oracleTimeout: opts.oracleTimeout, - maxTradeVolume: opts.maxTradeVolume, - defaultThreshold: opts.defaultThreshold, - delayUntilDefault: opts.delayUntilDefault, - }, - opts.revenueHiding, - { gasLimit: 2000000000 } - ) - await collateral.deployed() - - // Push forward chainlink feed - await pushOracleForward(opts.chainlinkFeed!) - - // sometimes we are trying to test a negative test case and we want this to fail silently - // fortunately this syntax fails silently because our tools are terrible - await expect(collateral.refresh()) - - return collateral -} + const deployCollateral = async (opts: CometCollateralOpts = {}): Promise => { + opts = { ...defaultCometCollateralOpts, ...opts } + + const CTokenV3CollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CTokenV3Collateral' + ) + + const collateral = await CTokenV3CollateralFactory.deploy( + { + erc20: opts.erc20, + targetName: opts.targetName, + priceTimeout: opts.priceTimeout, + chainlinkFeed: opts.chainlinkFeed, + oracleError: opts.oracleError, + oracleTimeout: opts.oracleTimeout, + maxTradeVolume: opts.maxTradeVolume, + defaultThreshold: opts.defaultThreshold, + delayUntilDefault: opts.delayUntilDefault, + }, + opts.revenueHiding, + { gasLimit: 2000000000 } + ) + + await collateral.deployed() -type Fixture = () => Promise + // Push forward chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return collateral + } + + type Fixture = () => Promise + + const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CometCollateralOpts = {} + ): Fixture => { + const collateralOpts = { ...defaultCometCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + ) + collateralOpts.chainlinkFeed = chainlinkFeed.address + + const fix = await curr.fix() + const cTokenV3 = fix.cTokenV3 + const { wcTokenV3, token } = fix + + collateralOpts.erc20 = wcTokenV3.address + const collateral = await deployCollateral(collateralOpts) + const rewardToken = await ethers.getContractAt('ERC20Mock', COMP) + + return { + alice, + collateral, + chainlinkFeed, + cTokenV3, + wcTokenV3, + token, + tok: wcTokenV3, + rewardToken, + } + } + + return makeCollateralFixtureContext + } -const makeCollateralFixtureContext = ( - alice: SignerWithAddress, - opts: CometCollateralOpts = {} -): Fixture => { - const collateralOpts = { ...defaultCometCollateralOpts, ...opts } + const deployCollateralCometMockContext = async ( + opts: CometCollateralOpts = {} + ): Promise => { + const collateralOpts = { ...defaultCometCollateralOpts, ...opts } - const makeCollateralFixtureContext = async () => { const MockV3AggregatorFactory = ( await ethers.getContractFactory('MockV3Aggregator') ) + const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + collateralOpts.chainlinkFeed = chainlinkFeed.address + + const CometFactory = await ethers.getContractFactory('CometMock') + const cTokenV3 = await CometFactory.deploy(curr.cTokenV3) - const chainlinkFeed = ( - await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + const CTokenV3WrapperFactory = ( + await ethers.getContractFactory('CFiatV3Wrapper') ) - collateralOpts.chainlinkFeed = chainlinkFeed.address - const fix = await makewCSUDC() - const cusdcV3 = fix.cusdcV3 - const { wcusdcV3, usdc } = fix + const wcTokenV3 = ( + ((await CTokenV3WrapperFactory.deploy( + cTokenV3.address, + REWARDS, + COMP, + curr.wrapperName, + curr.wrapperSymbol + )) as unknown as ICFiatV3Wrapper) + ) + const CTokenV3WrapperMockFactory = ( + await ethers.getContractFactory('CFiatV3WrapperMock') + ) + const wcTokenV3Mock = ( + ((await CTokenV3WrapperMockFactory.deploy(wcTokenV3.address)) as unknown) + ) - collateralOpts.erc20 = wcusdcV3.address + collateralOpts.erc20 = wcTokenV3Mock.address + const token = await ethers.getContractAt('ERC20Mock', curr.token) const collateral = await deployCollateral(collateralOpts) const rewardToken = await ethers.getContractAt('ERC20Mock', COMP) return { - alice, collateral, chainlinkFeed, - cusdcV3, - wcusdcV3, - usdc, - tok: wcusdcV3, + cTokenV3, + wcTokenV3: wcTokenV3Mock, + wcTokenV3Mock: wcTokenV3Mock as unknown as CFiatV3WrapperMock, + token, + tok: wcTokenV3, rewardToken, } } - return makeCollateralFixtureContext -} - -const deployCollateralCometMockContext = async ( - opts: CometCollateralOpts = {} -): Promise => { - const collateralOpts = { ...defaultCometCollateralOpts, ...opts } - - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - collateralOpts.chainlinkFeed = chainlinkFeed.address - - const CometFactory = await ethers.getContractFactory('CometMock') - const cusdcV3 = await CometFactory.deploy(CUSDC_V3) - - const CusdcV3WrapperFactory = ( - await ethers.getContractFactory('CusdcV3Wrapper') - ) - - const wcusdcV3 = ( - ((await CusdcV3WrapperFactory.deploy( - cusdcV3.address, - REWARDS, - COMP - )) as unknown as ICusdcV3Wrapper) - ) - const CusdcV3WrapperMockFactory = ( - await ethers.getContractFactory('CusdcV3WrapperMock') - ) - const wcusdcV3Mock = ( - ((await CusdcV3WrapperMockFactory.deploy(wcusdcV3.address)) as unknown) - ) - - collateralOpts.erc20 = wcusdcV3Mock.address - const usdc = await ethers.getContractAt('ERC20Mock', USDC) - const collateral = await deployCollateral(collateralOpts) - const rewardToken = await ethers.getContractAt('ERC20Mock', COMP) - - return { - collateral, - chainlinkFeed, - cusdcV3, - wcusdcV3: wcusdcV3Mock, - wcusdcV3Mock: wcusdcV3Mock as unknown as CusdcV3WrapperMock, - usdc, - tok: wcusdcV3, - rewardToken, + /* + Define helper functions + */ + + const mintCollateralTo: MintCollateralFunc = async ( + ctx: CometCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string + ) => { + await mintWcToken( + ctx.token, + ctx.cTokenV3, + ctx.tok as unknown as ICFiatV3Wrapper, + user, + amount, + recipient + ) } -} -/* - Define helper functions -*/ - -const mintCollateralTo: MintCollateralFunc = async ( - ctx: CometCollateralFixtureContext, - amount: BigNumberish, - user: SignerWithAddress, - recipient: string -) => { - await mintWcUSDC( - ctx.usdc, - ctx.cusdcV3, - ctx.tok as unknown as ICusdcV3Wrapper, - user, - amount, - recipient - ) -} - -const reduceTargetPerRef = async ( - ctx: CometCollateralFixtureContext, - pctDecrease: BigNumberish -) => { - const lastRound = await ctx.chainlinkFeed.latestRoundData() - const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100)) - await ctx.chainlinkFeed.updateAnswer(nextAnswer) -} + const reduceTargetPerRef = async ( + ctx: CometCollateralFixtureContext, + pctDecrease: BigNumberish + ) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) + } -const increaseTargetPerRef = async ( - ctx: CometCollateralFixtureContext, - pctIncrease: BigNumberish -) => { - const lastRound = await ctx.chainlinkFeed.latestRoundData() - const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100)) - await ctx.chainlinkFeed.updateAnswer(nextAnswer) -} + const increaseTargetPerRef = async ( + ctx: CometCollateralFixtureContext, + pctIncrease: BigNumberish + ) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) + } -const reduceRefPerTok = async (ctx: CometCollateralFixtureContext, pctDecrease: BigNumberish) => { - const totalsBasic = await ctx.cusdcV3.totalsBasic() - const bsi = totalsBasic.baseSupplyIndex + const reduceRefPerTok = async (ctx: CometCollateralFixtureContext, pctDecrease: BigNumberish) => { + const totalsBasic = await ctx.cTokenV3.totalsBasic() + const bsi = totalsBasic.baseSupplyIndex - // save old bytecode - const oldBytecode = await network.provider.send('eth_getCode', [COMET_EXT]) + // save old bytecode + const oldBytecode = await network.provider.send('eth_getCode', [COMET_EXT]) - const mockFactory = await ethers.getContractFactory('CometExtMock') - const mock = await mockFactory.deploy() - const bytecode = await network.provider.send('eth_getCode', [mock.address]) - await setCode(COMET_EXT, bytecode) + const mockFactory = await ethers.getContractFactory('CometExtMock') + const mock = await mockFactory.deploy() + const bytecode = await network.provider.send('eth_getCode', [mock.address]) + await setCode(COMET_EXT, bytecode) - const cometAsMock = await ethers.getContractAt('CometExtMock', ctx.cusdcV3.address) - await cometAsMock.setBaseSupplyIndex(bsi.sub(bsi.mul(pctDecrease).div(100))) + const cometAsMock = await ethers.getContractAt('CometExtMock', ctx.cTokenV3.address) + await cometAsMock.setBaseSupplyIndex(bsi.sub(bsi.mul(pctDecrease).div(100))) - await setCode(COMET_EXT, oldBytecode) -} + await setCode(COMET_EXT, oldBytecode) + } -const increaseRefPerTok = async () => { - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) -} + const increaseRefPerTok = async () => { + await advanceBlocks(1000) + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) + } -const getExpectedPrice = async (ctx: CometCollateralFixtureContext): Promise => { - const initRefPerTok = await ctx.collateral.refPerTok() + const getExpectedPrice = async (ctx: CometCollateralFixtureContext): Promise => { + const initRefPerTok = await ctx.collateral.refPerTok() - const decimals = await ctx.chainlinkFeed.decimals() + const decimals = await ctx.chainlinkFeed.decimals() - const initData = await ctx.chainlinkFeed.latestRoundData() - return initData.answer - .mul(bn(10).pow(18 - decimals)) - .mul(initRefPerTok) - .div(fp('1')) -} + const initData = await ctx.chainlinkFeed.latestRoundData() + return initData.answer + .mul(bn(10).pow(18 - decimals)) + .mul(initRefPerTok) + .div(fp('1')) + } -/* - Define collateral-specific tests -*/ + /* + Define collateral-specific tests + */ -const collateralSpecificConstructorTests = () => { - return -} + const collateralSpecificConstructorTests = () => { + return + } -const collateralSpecificStatusTests = () => { - it('does revenue hiding correctly', async () => { - const { collateral, wcusdcV3Mock } = await deployCollateralCometMockContext({ - revenueHiding: fp('0.01'), + const collateralSpecificStatusTests = () => { + it('does revenue hiding correctly', async () => { + const { collateral, wcTokenV3Mock } = await deployCollateralCometMockContext({ + revenueHiding: fp('0.01'), + }) + + // Should remain SOUND after a 1% decrease + let refPerTok = await collateral.refPerTok() + let currentExchangeRate = await wcTokenV3Mock.exchangeRate() + await wcTokenV3Mock.setMockExchangeRate( + true, + currentExchangeRate.sub(currentExchangeRate.mul(1).div(100)) + ) + await collateral.refresh() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + // refPerTok should be unchanged + expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand + + // Should become DISABLED if drops more than that + refPerTok = await collateral.refPerTok() + currentExchangeRate = await wcTokenV3Mock.exchangeRate() + await wcTokenV3Mock.setMockExchangeRate( + true, + currentExchangeRate.sub(currentExchangeRate.mul(1).div(100)) + ) + await collateral.refresh() + expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + + // refPerTok should have fallen 1% + refPerTok = refPerTok.sub(refPerTok.div(100)) + expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand }) - // Should remain SOUND after a 1% decrease - let refPerTok = await collateral.refPerTok() - let currentExchangeRate = await wcusdcV3Mock.exchangeRate() - await wcusdcV3Mock.setMockExchangeRate( - true, - currentExchangeRate.sub(currentExchangeRate.mul(1).div(100)) - ) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - - // refPerTok should be unchanged - expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand - - // Should become DISABLED if drops more than that - refPerTok = await collateral.refPerTok() - currentExchangeRate = await wcusdcV3Mock.exchangeRate() - await wcusdcV3Mock.setMockExchangeRate( - true, - currentExchangeRate.sub(currentExchangeRate.mul(1).div(100)) - ) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - - // refPerTok should have fallen 1% - refPerTok = refPerTok.sub(refPerTok.div(100)) - expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand - }) - - it('enters DISABLED state when refPerTok() decreases', async () => { - // Context: Usually this is left to generic suite, but we were having issues with the comet extensions - // on arbitrum as compared to ethereum mainnet, and this was the easiest way around it. - - const { collateral, wcusdcV3Mock } = await deployCollateralCometMockContext({}) - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // Should default instantly after 5% drop - const currentExchangeRate = await wcusdcV3Mock.exchangeRate() - await wcusdcV3Mock.setMockExchangeRate( - true, - currentExchangeRate.sub(currentExchangeRate.mul(5).div(100)) - ) - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('should not brick refPerTok() even if _underlyingRefPerTok() reverts', async () => { - const { collateral, wcusdcV3Mock } = await deployCollateralCometMockContext({}) - await wcusdcV3Mock.setRevertExchangeRate(true) - await expect(collateral.refresh()).not.to.be.reverted - await expect(collateral.refPerTok()).not.to.be.reverted - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - }) -} + it('enters DISABLED state when refPerTok() decreases', async () => { + // Context: Usually this is left to generic suite, but we were having issues with the comet extensions + // on arbitrum as compared to ethereum mainnet, and this was the easiest way around it. + + const { collateral, wcTokenV3Mock } = await deployCollateralCometMockContext({}) + + // Check initial state + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await collateral.whenDefault()).to.equal(MAX_UINT48) + await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') + + // Should default instantly after 5% drop + const currentExchangeRate = await wcTokenV3Mock.exchangeRate() + await wcTokenV3Mock.setMockExchangeRate( + true, + currentExchangeRate.sub(currentExchangeRate.mul(5).div(100)) + ) + await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') + expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) + }) -const beforeEachRewardsTest = async (ctx: CometCollateralFixtureContext) => { - await enableRewardsAccrual(ctx.cusdcV3) -} + it('should not brick refPerTok() even if _underlyingRefPerTok() reverts', async () => { + const { collateral, wcTokenV3Mock } = await deployCollateralCometMockContext({}) + await wcTokenV3Mock.setRevertExchangeRate(true) + await expect(collateral.refresh()).not.to.be.reverted + await expect(collateral.refPerTok()).not.to.be.reverted + expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + }) + } -/* - Run the test suite -*/ + const beforeEachRewardsTest = async (ctx: CometCollateralFixtureContext) => { + await enableRewardsAccrual(ctx.cTokenV3) + } -const opts = { - deployCollateral, - collateralSpecificConstructorTests, - collateralSpecificStatusTests, - beforeEachRewardsTest, - makeCollateralFixtureContext, - mintCollateralTo, - reduceTargetPerRef, - increaseTargetPerRef, - reduceRefPerTok, - increaseRefPerTok, - getExpectedPrice, - itClaimsRewards: it, - itChecksTargetPerRefDefault: it, - itChecksTargetPerRefDefaultUp: it, - itChecksRefPerTokDefault: it.skip, // implemented in this file - itChecksPriceChanges: it, - itChecksNonZeroDefaultThreshold: it, - itHasRevenueHiding: it.skip, // implemented in this file - itIsPricedByPeg: true, - resetFork, - collateralName: 'CompoundV3USDC', - chainlinkDefaultAnswer, - targetNetwork: forkNetwork, -} + /* + Run the test suite + */ + + const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + beforeEachRewardsTest, + makeCollateralFixtureContext, + mintCollateralTo, + reduceTargetPerRef, + increaseTargetPerRef, + reduceRefPerTok, + increaseRefPerTok, + getExpectedPrice, + itClaimsRewards: it, + itChecksTargetPerRefDefault: it, + itChecksTargetPerRefDefaultUp: it, + itChecksRefPerTokDefault: it.skip, // implemented in this file + itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, + itHasRevenueHiding: it.skip, // implemented in this file + itIsPricedByPeg: true, + resetFork: getResetFork(getForkBlock(curr.tokenName)), + collateralName: curr.testName, + chainlinkDefaultAnswer, + targetNetwork: curr.forkNetwork, + } -collateralTests(opts) + collateralTests(opts) +}) diff --git a/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts b/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts deleted file mode 100644 index d8749820d9..0000000000 --- a/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts +++ /dev/null @@ -1,842 +0,0 @@ -import { expect } from 'chai' -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import hre, { ethers, network } from 'hardhat' -import { useEnv } from '#/utils/env' -import { whileImpersonating } from '../../../utils/impersonation' -import { advanceTime, advanceBlocks } from '../../../utils/time' -import { allocateUSDC, enableRewardsAccrual, mintWcUSDC, makewCSUDC, resetFork } from './helpers' -import { forkNetwork, COMP, REWARDS } from './constants' -import { - ERC20Mock, - CometInterface, - ICusdcV3Wrapper, - CusdcV3Wrapper__factory, -} from '../../../../typechain' -import { bn } from '../../../../common/numbers' -import { getChainId } from '../../../../common/blockchain-utils' -import { networkConfig } from '../../../../common/configuration' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { MAX_UINT256, ZERO_ADDRESS } from '../../../../common/constants' - -const describeFork = useEnv('FORK') ? describe : describe.skip - -const itL1 = forkNetwork != 'base' && forkNetwork != 'arbitrum' ? it : it.skip - -describeFork('Wrapped CUSDCv3', () => { - let bob: SignerWithAddress - let charles: SignerWithAddress - let don: SignerWithAddress - let usdc: ERC20Mock - let wcusdcV3: ICusdcV3Wrapper - let cusdcV3: CometInterface - - let chainId: number - - before(async () => { - await resetFork() - - chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - }) - - beforeEach(async () => { - ;[, bob, charles, don] = await ethers.getSigners() - ;({ usdc, wcusdcV3, cusdcV3 } = await loadFixture(makewCSUDC)) - }) - - it('reverts if deployed with a 0 address', async () => { - const CusdcV3WrapperFactory = ( - await ethers.getContractFactory('CusdcV3Wrapper') - ) - - // TODO there is a chai limitation that cannot catch custom errors during deployment - await expect(CusdcV3WrapperFactory.deploy(ZERO_ADDRESS, REWARDS, COMP)).to.be.reverted - }) - - it('configuration/state', async () => { - expect(await wcusdcV3.symbol()).to.equal('wcUSDCv3') - expect(await wcusdcV3.name()).to.equal('Wrapped cUSDCv3') - expect(await wcusdcV3.totalSupply()).to.equal(bn(0)) - - expect(await wcusdcV3.underlyingComet()).to.equal(cusdcV3.address) - expect(await wcusdcV3.rewardERC20()).to.equal(COMP) - }) - - describe('deposit', () => { - const amount = bn('20000e6') - - beforeEach(async () => { - await allocateUSDC(bob.address, amount) - await usdc.connect(bob).approve(cusdcV3.address, ethers.constants.MaxUint256) - await cusdcV3.connect(bob).supply(usdc.address, amount) - await cusdcV3.connect(bob).allow(wcusdcV3.address, true) - }) - - it('deposit', async () => { - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3.connect(bob).deposit(ethers.constants.MaxUint256) - expect(await cusdcV3.balanceOf(bob.address)).to.equal(0) - expect(await usdc.balanceOf(bob.address)).to.equal(0) - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(expectedAmount) - }) - - it('deposits to own account', async () => { - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3.connect(bob).depositTo(bob.address, ethers.constants.MaxUint256) - expect(await cusdcV3.balanceOf(bob.address)).to.equal(0) - expect(await usdc.balanceOf(bob.address)).to.equal(0) - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(expectedAmount) - }) - - it('deposits for someone else', async () => { - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3.connect(bob).depositTo(don.address, ethers.constants.MaxUint256) - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(0) - expect(await wcusdcV3.balanceOf(don.address)).to.eq(expectedAmount) - }) - - it('checks for correct approval on deposit - regression test', async () => { - await expect( - wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - ).revertedWithCustomError(wcusdcV3, 'Unauthorized') - - // Provide approval on the wrapper - await wcusdcV3.connect(bob).allow(don.address, true) - - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - - // This should fail even when bob approved wcusdcv3 to spend his tokens, - // because there is no explicit approval of cUSDCv3 from bob to don, only - // approval on the wrapper - await expect( - wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - ).to.be.revertedWithCustomError(cusdcV3, 'Unauthorized') - - // Add explicit approval of cUSDCv3 and retry - await cusdcV3.connect(bob).allow(don.address, true) - await wcusdcV3 - .connect(don) - .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(0) - expect(await wcusdcV3.balanceOf(charles.address)).to.eq(expectedAmount) - }) - - it('deposits from a different account', async () => { - expect(await wcusdcV3.balanceOf(charles.address)).to.eq(0) - await expect( - wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - ).revertedWithCustomError(wcusdcV3, 'Unauthorized') - - // Approval has to be on cUsdcV3, not the wrapper - await cusdcV3.connect(bob).allow(don.address, true) - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3 - .connect(don) - .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(0) - expect(await wcusdcV3.balanceOf(charles.address)).to.eq(expectedAmount) - }) - - it('deposits less than available cusdc', async () => { - const depositAmount = bn('10000e6') - const expectedAmount = await wcusdcV3.convertDynamicToStatic(depositAmount) - await wcusdcV3.connect(bob).depositTo(bob.address, depositAmount) - expect(await cusdcV3.balanceOf(bob.address)).to.be.closeTo(depositAmount, 100) - expect(await usdc.balanceOf(bob.address)).to.equal(0) - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(expectedAmount, 100) - }) - - it('user that deposits must have same baseTrackingIndex as this Token in Comet', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, amount, bob.address) - expect((await cusdcV3.callStatic.userBasic(wcusdcV3.address)).baseTrackingIndex).to.equal( - await wcusdcV3.baseTrackingIndex(bob.address) - ) - }) - - it('multiple deposits lead to accurate balances', async () => { - let expectedAmount = await wcusdcV3.convertDynamicToStatic(bn('10000e6')) - await wcusdcV3.connect(bob).depositTo(bob.address, bn('10000e6')) - await advanceTime(1000) - expectedAmount = expectedAmount.add(await wcusdcV3.convertDynamicToStatic(bn('10000e6'))) - await wcusdcV3.connect(bob).depositTo(bob.address, bn('10000e6')) - - // The more wcUSDCv3 is minted, the higher its value is relative to cUSDCv3. - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.be.gt(amount) - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(expectedAmount, 100) - - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.be.closeTo( - await cusdcV3.balanceOf(wcusdcV3.address), - 1 - ) - }) - - it('updates the totalSupply', async () => { - const totalSupplyBefore = await wcusdcV3.totalSupply() - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3.connect(bob).deposit(ethers.constants.MaxUint256) - expect(await wcusdcV3.totalSupply()).to.equal(totalSupplyBefore.add(expectedAmount)) - }) - - it('deposit 0 reverts', async () => { - await expect(wcusdcV3.connect(bob).deposit(0)).to.be.revertedWithCustomError( - wcusdcV3, - 'BadAmount' - ) - }) - - it('depositing 0 balance reverts', async () => { - await cusdcV3.connect(bob).transfer(charles.address, ethers.constants.MaxUint256) - await expect( - wcusdcV3.connect(bob).deposit(ethers.constants.MaxUint256) - ).to.be.revertedWithCustomError(wcusdcV3, 'BadAmount') - }) - - it('desposit to zero address reverts', async () => { - await expect( - wcusdcV3.connect(bob).depositTo(ZERO_ADDRESS, ethers.constants.MaxUint256) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - }) - }) - - describe('withdraw', () => { - const initwusdcAmt = bn('20000e6') - - beforeEach(async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, initwusdcAmt, bob.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, charles, initwusdcAmt, charles.address) - }) - - it('withdraws to own account', async () => { - // bob withdraws ALL - const expectedAmountBob = await wcusdcV3.underlyingBalanceOf(bob.address) - await wcusdcV3.connect(bob).withdraw(ethers.constants.MaxUint256) - const bal = await wcusdcV3.balanceOf(bob.address) - expect(bal).to.closeTo(bn('0'), 10) - expect(await cusdcV3.balanceOf(bob.address)).to.closeTo(expectedAmountBob, 50) - }) - - it('withdraws to a different account', async () => { - const expectedAmount = await wcusdcV3.underlyingBalanceOf(bob.address) - await wcusdcV3.connect(bob).withdrawTo(don.address, ethers.constants.MaxUint256) - expect(await cusdcV3.balanceOf(don.address)).to.closeTo(expectedAmount, 100) - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(bn('0'), 10) - }) - - it('withdraws from a different account', async () => { - const withdrawAmount = await wcusdcV3.underlyingBalanceOf(bob.address) - await expect( - wcusdcV3.connect(charles).withdrawFrom(bob.address, don.address, withdrawAmount) - ).to.be.revertedWithCustomError(wcusdcV3, 'Unauthorized') - - await wcusdcV3.connect(bob).allow(charles.address, true) - await wcusdcV3.connect(charles).withdrawFrom(bob.address, don.address, withdrawAmount) - - expect(await cusdcV3.balanceOf(don.address)).to.closeTo(withdrawAmount, 100) - expect(await cusdcV3.balanceOf(charles.address)).to.closeTo(bn('0'), 30) - - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(bn(0), 100) - }) - - it('withdraws all underlying balance via multiple withdrawals', async () => { - await advanceTime(1000) - const initialBalance = await wcusdcV3.underlyingBalanceOf(bob.address) - const withdrawAmt = bn('10000e6') - await wcusdcV3.connect(bob).withdraw(withdrawAmt) - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.closeTo( - initialBalance.sub(withdrawAmt), - 50 - ) - await advanceTime(1000) - await wcusdcV3.connect(bob).withdraw(ethers.constants.MaxUint256) - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(bn('0'), 10) - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.closeTo(bn('0'), 10) - }) - - it('withdrawing 0 reverts', async () => { - const initialBalance = await wcusdcV3.balanceOf(bob.address) - await expect(wcusdcV3.connect(bob).withdraw(0)).to.be.revertedWithCustomError( - wcusdcV3, - 'BadAmount' - ) - expect(await wcusdcV3.balanceOf(bob.address)).to.equal(initialBalance) - }) - - it('withdrawing 0 balance reverts', async () => { - await expect( - wcusdcV3.connect(don).withdraw(ethers.constants.MaxUint256) - ).to.be.revertedWithCustomError(wcusdcV3, 'BadAmount') - }) - - it('handles complex withdrawal sequence', async () => { - let bobWithdrawn = bn('0') - let charlesWithdrawn = bn('0') - let donWithdrawn = bn('0') - - // charles withdraws SOME - const firstWithdrawAmt = bn('15000e6') - charlesWithdrawn = charlesWithdrawn.add(firstWithdrawAmt) - await wcusdcV3.connect(charles).withdraw(firstWithdrawAmt) - const newBalanceCharles = await cusdcV3.balanceOf(charles.address) - expect(newBalanceCharles).to.closeTo(firstWithdrawAmt, 50) - - // don deposits - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, initwusdcAmt, don.address) - - // bob withdraws SOME - bobWithdrawn = bobWithdrawn.add(bn('12345e6')) - await wcusdcV3.connect(bob).withdraw(bn('12345e6')) - - // don withdraws SOME - donWithdrawn = donWithdrawn.add(bn('123e6')) - await wcusdcV3.connect(don).withdraw(bn('123e6')) - - // charles withdraws ALL - charlesWithdrawn = charlesWithdrawn.add(await wcusdcV3.underlyingBalanceOf(charles.address)) - await wcusdcV3.connect(charles).withdraw(ethers.constants.MaxUint256) - - // don withdraws ALL - donWithdrawn = donWithdrawn.add(await wcusdcV3.underlyingBalanceOf(don.address)) - await wcusdcV3.connect(don).withdraw(ethers.constants.MaxUint256) - - // bob withdraws ALL - bobWithdrawn = bobWithdrawn.add(await wcusdcV3.underlyingBalanceOf(bob.address)) - await wcusdcV3.connect(bob).withdraw(ethers.constants.MaxUint256) - - const bal = await wcusdcV3.balanceOf(bob.address) - - expect(bal).to.closeTo(bn('0'), 10) - expect(await cusdcV3.balanceOf(bob.address)).to.closeTo(bobWithdrawn, 200) - expect(await cusdcV3.balanceOf(charles.address)).to.closeTo(charlesWithdrawn, 200) - expect(await cusdcV3.balanceOf(don.address)).to.closeTo(donWithdrawn, 200) - }) - - it('updates the totalSupply', async () => { - const totalSupplyBefore = await wcusdcV3.totalSupply() - const withdrawAmt = bn('15000e6') - const expectedDiff = await wcusdcV3.convertDynamicToStatic(withdrawAmt) - await wcusdcV3.connect(bob).withdraw(withdrawAmt) - // conservative rounding - expect(await wcusdcV3.totalSupply()).to.be.closeTo(totalSupplyBefore.sub(expectedDiff), 25) - }) - }) - - describe('transfer', () => { - beforeEach(async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - }) - - it('sets max allowance with approval', async () => { - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - - // set approve - await wcusdcV3.connect(bob).allow(don.address, true) - - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) - - // rollback approve - await wcusdcV3.connect(bob).allow(don.address, false) - - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - }) - - it('does not transfer without approval', async () => { - await expect( - wcusdcV3.connect(bob).transferFrom(don.address, bob.address, bn('10000e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'Unauthorized') - - // Perform approval - await wcusdcV3.connect(bob).allow(don.address, true) - - await expect( - wcusdcV3.connect(don).transferFrom(bob.address, don.address, bn('10000e6')) - ).to.emit(wcusdcV3, 'Transfer') - }) - - it('transfer from/to zero address revert', async () => { - await expect( - wcusdcV3.connect(bob).transfer(ZERO_ADDRESS, bn('100e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - - await whileImpersonating(ZERO_ADDRESS, async (signer) => { - await expect( - wcusdcV3.connect(signer).transfer(don.address, bn('100e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - }) - }) - - it('performs validation on transfer amount', async () => { - await expect( - wcusdcV3.connect(bob).transfer(don.address, bn('40000e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'ExceedsBalance') - }) - - it('supports IERC20.approve and performs validations', async () => { - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) - - // Cannot set approve to the zero address - await expect( - wcusdcV3.connect(bob).approve(ZERO_ADDRESS, bn('10000e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - - // Can set full allowance with max uint256 - await expect(wcusdcV3.connect(bob).approve(don.address, MAX_UINT256)).to.emit( - wcusdcV3, - 'Approval' - ) - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) - expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(true) - - // Can revert allowance with zero amount - await expect(wcusdcV3.connect(bob).approve(don.address, bn(0))).to.emit(wcusdcV3, 'Approval') - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) - - // Any other amount reverts - await expect( - wcusdcV3.connect(bob).approve(don.address, bn('10000e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'BadAmount') - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) - }) - - it('perform validations on allow', async () => { - await expect(wcusdcV3.connect(bob).allow(ZERO_ADDRESS, true)).to.be.revertedWithCustomError( - wcusdcV3, - 'ZeroAddress' - ) - - await whileImpersonating(ZERO_ADDRESS, async (signer) => { - await expect( - wcusdcV3.connect(signer).allow(don.address, true) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - }) - }) - - it('updates balances and rewards in sender and receiver', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await enableRewardsAccrual(cusdcV3) - await advanceTime(1000) - - await wcusdcV3.accrueAccount(don.address) - await wcusdcV3.accrueAccount(bob.address) - - // Don's rewards accrual should be less than Bob's because he deposited later - expect(await wcusdcV3.baseTrackingAccrued(don.address)).to.be.lt( - await wcusdcV3.baseTrackingAccrued(bob.address) - ) - const bobBal1 = await wcusdcV3.balanceOf(bob.address) - const donBal1 = await wcusdcV3.balanceOf(don.address) - await wcusdcV3.connect(bob).transfer(don.address, bn('10000e6')) - const bobBal2 = await wcusdcV3.balanceOf(bob.address) - const donBal2 = await wcusdcV3.balanceOf(don.address) - - expect(bobBal2).equal(bobBal1.sub(bn('10000e6'))) - expect(donBal2).equal(donBal1.add(bn('10000e6'))) - - await advanceTime(1000) - await wcusdcV3.accrueAccount(don.address) - await wcusdcV3.accrueAccount(bob.address) - - expect(await wcusdcV3.baseTrackingAccrued(don.address)).to.be.gt( - await wcusdcV3.baseTrackingAccrued(bob.address) - ) - - const donsBalance = (await wcusdcV3.underlyingBalanceOf(don.address)).toBigInt() - const bobsBalance = (await wcusdcV3.underlyingBalanceOf(bob.address)).toBigInt() - expect(donsBalance).to.be.gt(bobsBalance) - const totalBalances = donsBalance + bobsBalance - - // Rounding in favor of the Wrapped Token is happening here. Amount is negligible - expect(totalBalances).to.be.closeTo(await cusdcV3.balanceOf(wcusdcV3.address), 1) - }) - - it('does not update the total supply', async () => { - const totalSupplyBefore = await wcusdcV3.totalSupply() - await wcusdcV3.connect(bob).transfer(don.address, bn('10000e6')) - expect(totalSupplyBefore).to.equal(await wcusdcV3.totalSupply()) - }) - }) - - describe('accure / accrueAccount', () => { - it('accrues internally for the comet', async () => { - const initAccrueTime = (await cusdcV3.totalsBasic()).lastAccrualTime - await wcusdcV3.accrue() - const endAccrueTime = (await cusdcV3.totalsBasic()).lastAccrualTime - expect(endAccrueTime).gt(initAccrueTime) - }) - - it('accrues rewards over time', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - expect(await wcusdcV3.baseTrackingAccrued(bob.address)).to.eq(0) - await enableRewardsAccrual(cusdcV3) - await advanceTime(1000) - - await wcusdcV3.accrueAccount(bob.address) - expect(await wcusdcV3.baseTrackingAccrued(bob.address)).to.be.gt(0) - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.eq( - await cusdcV3.balanceOf(wcusdcV3.address) - ) - }) - - it('does not accrue when accruals are not enabled in Comet', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - expect(await wcusdcV3.baseTrackingAccrued(bob.address)).to.eq(0) - - await advanceTime(1000) - expect(await wcusdcV3.baseTrackingAccrued(bob.address)).to.eq(0) - }) - }) - - describe('underlying balance', () => { - it('returns the correct amount of decimals', async () => { - const decimals = await wcusdcV3.decimals() - expect(decimals).to.equal(6) - }) - - it('returns underlying balance of user which includes revenue', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - const wrappedBalance = await wcusdcV3.balanceOf(bob.address) - await advanceTime(1000) - expect(wrappedBalance).to.equal(await wcusdcV3.balanceOf(bob.address)) - // Underlying balance increases over time and is greater than the balance in the wrapped token - expect(wrappedBalance).to.be.lt(await wcusdcV3.underlyingBalanceOf(bob.address)) - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.eq( - await cusdcV3.balanceOf(wcusdcV3.address) - ) - - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - await advanceTime(1000) - const totalBalances = (await wcusdcV3.underlyingBalanceOf(don.address)).add( - await wcusdcV3.underlyingBalanceOf(bob.address) - ) - - const contractBalance = await cusdcV3.balanceOf(wcusdcV3.address) - expect(totalBalances).to.closeTo(contractBalance, 10) - expect(totalBalances).to.lte(contractBalance) - }) - - it('returns 0 when user has no balance', async () => { - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.equal(0) - }) - - it('also accrues account in Comet to ensure that global indices are updated', async () => { - await enableRewardsAccrual(cusdcV3) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - const oldTrackingSupplyIndex = (await cusdcV3.totalsBasic()).trackingSupplyIndex - - await advanceTime(1000) - await wcusdcV3.accrueAccount(bob.address) - expect(oldTrackingSupplyIndex).to.be.lessThan( - (await cusdcV3.totalsBasic()).trackingSupplyIndex - ) - }) - - it('matches balance in cUSDCv3', async () => { - // mint some cusdc to bob - const amount = bn('20000e6') - await allocateUSDC(bob.address, amount) - await usdc.connect(bob).approve(cusdcV3.address, ethers.constants.MaxUint256) - await cusdcV3.connect(bob).supply(usdc.address, amount) - - // mint some wcusdc to bob, charles, don - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, amount, bob.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, charles, amount, charles.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, amount, don.address) - await advanceTime(100000) - - let totalBalances = (await wcusdcV3.underlyingBalanceOf(don.address)) - .add(await wcusdcV3.underlyingBalanceOf(bob.address)) - .add(await wcusdcV3.underlyingBalanceOf(charles.address)) - let contractBalance = await cusdcV3.balanceOf(wcusdcV3.address) - expect(totalBalances).to.be.closeTo(contractBalance, 10) - expect(totalBalances).to.be.lte(contractBalance) - - const bobBal = await wcusdcV3.balanceOf(bob.address) - await wcusdcV3.connect(bob).withdraw(bobBal) - await wcusdcV3.connect(don).withdraw(bn('10000e6')) - - totalBalances = (await wcusdcV3.underlyingBalanceOf(don.address)) - .add(await wcusdcV3.underlyingBalanceOf(bob.address)) - .add(await wcusdcV3.underlyingBalanceOf(charles.address)) - contractBalance = await cusdcV3.balanceOf(wcusdcV3.address) - expect(totalBalances).to.be.closeTo(contractBalance, 10) - expect(totalBalances).to.be.lte(contractBalance) - }) - }) - - describe('exchange rate', () => { - it('returns the correct exchange rate with 0 balance', async () => { - const totalsBasic = await cusdcV3.totalsBasic() - const baseIndexScale = await cusdcV3.baseIndexScale() - const expectedExchangeRate = totalsBasic.baseSupplyIndex.mul(bn('1e6')).div(baseIndexScale) - expect(await cusdcV3.balanceOf(wcusdcV3.address)).to.equal(0) - expect(await wcusdcV3.exchangeRate()).to.be.closeTo(expectedExchangeRate, 5) - }) - - it('returns the correct exchange rate with a positive balance', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - const totalsBasic = await cusdcV3.totalsBasic() - const baseIndexScale = await cusdcV3.baseIndexScale() - const expectedExchangeRate = totalsBasic.baseSupplyIndex.mul(bn('1e6')).div(baseIndexScale) - expect(await wcusdcV3.exchangeRate()).to.equal(expectedExchangeRate) - }) - - it('current exchange rate is a ratio of total underlying balance and total supply', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - const totalSupply = await wcusdcV3.totalSupply() - const underlyingBalance = await cusdcV3.balanceOf(wcusdcV3.address) - expect(await wcusdcV3.exchangeRate()).to.equal( - underlyingBalance.mul(bn('1e6')).div(totalSupply) - ) - }) - }) - - describe('claiming rewards', () => { - beforeEach(async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - }) - - it('does not claim rewards when user has no permission', async () => { - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3) - await expect( - wcusdcV3.connect(don).claimTo(bob.address, bob.address) - ).to.be.revertedWithCustomError(wcusdcV3, 'Unauthorized') - - await wcusdcV3.connect(bob).allow(don.address, true) - expect(await wcusdcV3.isAllowed(bob.address, don.address)).to.eq(true) - await expect(wcusdcV3.connect(don).claimTo(bob.address, bob.address)).to.emit( - wcusdcV3, - 'RewardsClaimed' - ) - }) - - it('regression test: able to claim rewards even when they are big without overflow', async () => { - // Nov 28 2023: uint64 math in CusdcV3Wrapper contract results in overflow when COMP rewards are even moderately large - - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3, bn('2e18')) // enough to revert on uint64 implementation - - await expect(wcusdcV3.connect(bob).claimRewards()).to.emit(wcusdcV3, 'RewardsClaimed') - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) - }) - - it('claims rewards and sends to claimer (claimTo)', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3) - - await expect(wcusdcV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( - wcusdcV3, - 'RewardsClaimed' - ) - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) - }) - - it('caps at balance to avoid reverts when claiming rewards (claimTo)', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3) - - // Accrue multiple times - for (let i = 0; i < 10; i++) { - await advanceTime(1000) - await wcusdcV3.accrue() - } - - // Get rewards from Comet - const cometRewards = await ethers.getContractAt('ICometRewards', REWARDS) - await whileImpersonating(wcusdcV3.address, async (signer) => { - await cometRewards - .connect(signer) - .claimTo(cusdcV3.address, wcusdcV3.address, wcusdcV3.address, true) - }) - - // Accrue individual account - await wcusdcV3.accrueAccount(bob.address) - - // Due to rounding, balance is smaller that owed - const owed = await wcusdcV3.getRewardOwed(bob.address) - const bal = await compToken.balanceOf(wcusdcV3.address) - expect(owed).to.be.greaterThan(bal) - - // Should still be able to claimTo (caps at balance) - const balanceBobPrev = await compToken.balanceOf(bob.address) - await expect(wcusdcV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( - wcusdcV3, - 'RewardsClaimed' - ) - - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(balanceBobPrev) - }) - - it('claims rewards and sends to claimer (claimRewards)', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3) - - await expect(wcusdcV3.connect(bob).claimRewards()).to.emit(wcusdcV3, 'RewardsClaimed') - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) - }) - - it('claims rewards by participation', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await enableRewardsAccrual(cusdcV3) - await advanceTime(1000) - - expect(await compToken.balanceOf(bob.address)).to.equal(0) - expect(await compToken.balanceOf(don.address)).to.equal(0) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - - // claim at the same time - await network.provider.send('evm_setAutomine', [false]) - await wcusdcV3.connect(bob).claimTo(bob.address, bob.address) - await wcusdcV3.connect(don).claimTo(don.address, don.address) - await network.provider.send('evm_setAutomine', [true]) - await advanceBlocks(1) - - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) - const balanceBob = await compToken.balanceOf(bob.address) - const balanceDon = await compToken.balanceOf(don.address) - expect(balanceDon).lessThanOrEqual(balanceBob) - expect(balanceBob).to.be.closeTo(balanceDon, balanceBob.mul(5).div(1000)) // within 0.5% - }) - - // In this forked block, rewards accrual is not yet enabled in Comet - // Only applies to Mainnet forks (L1) - itL1('claims no rewards when rewards accrual is not enabled', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - await advanceTime(1000) - await wcusdcV3.connect(bob).claimTo(bob.address, bob.address) - expect(await compToken.balanceOf(bob.address)).to.equal(0) - }) - - it('returns reward owed after accrual and claims', async () => { - await enableRewardsAccrual(cusdcV3) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await advanceTime(1000) - await advanceBlocks(1) - - const bobsReward = await wcusdcV3.getRewardOwed(bob.address) - const donsReward = await wcusdcV3.getRewardOwed(don.address) - - expect(bobsReward).to.be.greaterThan(donsReward) - - await wcusdcV3.connect(bob).claimTo(bob.address, bob.address) - expect(await wcusdcV3.getRewardOwed(bob.address)).to.equal(0) - - await advanceTime(1000) - expect(await wcusdcV3.getRewardOwed(bob.address)).to.be.greaterThan(0) - }) - - it('accrues the account on deposit and withdraw', async () => { - await enableRewardsAccrual(cusdcV3) - await advanceTime(1200) - await advanceBlocks(100) - const expectedReward = await wcusdcV3.getRewardOwed(bob.address) - await advanceTime(12) - await advanceBlocks(1) - const newExpectedReward = await wcusdcV3.getRewardOwed(bob.address) - // marginal increase in exepected reward due to time passed - expect(newExpectedReward).gt(expectedReward) - - await advanceTime(1200) - await wcusdcV3.connect(bob).withdraw(ethers.constants.MaxUint256) - const nextExpectedReward = await wcusdcV3.getRewardOwed(bob.address) - await advanceTime(1200) - const lastExpectedReward = await wcusdcV3.getRewardOwed(bob.address) - // expected reward stays the same because account is empty - expect(lastExpectedReward).to.eq(nextExpectedReward) - }) - }) - - describe('baseTrackingAccrued', () => { - it('matches baseTrackingAccrued in cUSDCv3 over time', async () => { - await enableRewardsAccrual(cusdcV3) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - let wrappedTokenAccrued = await cusdcV3.baseTrackingAccrued(wcusdcV3.address) - expect(wrappedTokenAccrued).to.equal(await wcusdcV3.baseTrackingAccrued(bob.address)) - - await wcusdcV3.accrueAccount(bob.address) - - wrappedTokenAccrued = await cusdcV3.baseTrackingAccrued(wcusdcV3.address) - expect(wrappedTokenAccrued).to.equal(await wcusdcV3.baseTrackingAccrued(bob.address)) - expect((await cusdcV3.callStatic.userBasic(wcusdcV3.address)).baseTrackingIndex).to.equal( - await wcusdcV3.baseTrackingIndex(bob.address) - ) - - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, charles, bn('20000e6'), charles.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await advanceTime(1000) - - await network.provider.send('evm_setAutomine', [false]) - await wcusdcV3.accrueAccount(bob.address) - await wcusdcV3.accrueAccount(charles.address) - await wcusdcV3.accrueAccount(don.address) - await advanceBlocks(1) - await network.provider.send('evm_setAutomine', [true]) - - // All users' total accrued rewards in Wrapped cUSDC should closely match Wrapped cUSDC's - // accrued rewards in cUSDC. - const bobBTA = await wcusdcV3.baseTrackingAccrued(bob.address) - const charlesBTA = await wcusdcV3.baseTrackingAccrued(charles.address) - const donBTA = await wcusdcV3.baseTrackingAccrued(don.address) - const totalUsersAccrued = bobBTA.add(charlesBTA).add(donBTA) - wrappedTokenAccrued = await cusdcV3.baseTrackingAccrued(wcusdcV3.address) - expect(wrappedTokenAccrued).to.be.closeTo(totalUsersAccrued, 5) - }) - - it('matches baseTrackingAccrued in cUSDCv3 after withdrawals', async () => { - await enableRewardsAccrual(cusdcV3) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await advanceTime(1000) - await wcusdcV3.connect(bob).withdrawTo(bob.address, bn('10000e6')) - - await advanceTime(1000) - - await network.provider.send('evm_setAutomine', [false]) - await wcusdcV3.accrueAccount(bob.address) - await wcusdcV3.accrueAccount(don.address) - await advanceBlocks(1) - await network.provider.send('evm_setAutomine', [true]) - - // All users' total accrued rewards in Wrapped cUSDC should match Wrapped cUSDC's accrued rewards in cUSDC. - const totalUsersAccrued = (await wcusdcV3.baseTrackingAccrued(bob.address)).add( - await wcusdcV3.baseTrackingAccrued(don.address) - ) - const wrappedTokenAccrued = await cusdcV3.baseTrackingAccrued(wcusdcV3.address) - expect(wrappedTokenAccrued).to.closeTo(totalUsersAccrued, 10) - // expect(wrappedTokenAccrued).to.eq(totalUsersAccrued) - }) - }) -}) diff --git a/test/plugins/individual-collateral/compoundv3/constants.ts b/test/plugins/individual-collateral/compoundv3/constants.ts index 1b884a5473..6d9af98324 100644 --- a/test/plugins/individual-collateral/compoundv3/constants.ts +++ b/test/plugins/individual-collateral/compoundv3/constants.ts @@ -3,7 +3,7 @@ import { networkConfig } from '../../../../common/configuration' import { useEnv } from '#/utils/env' export const forkNetwork = useEnv('FORK_NETWORK') ?? 'mainnet' -let chainId +let chainId: string switch (forkNetwork) { case 'mainnet': @@ -22,25 +22,62 @@ switch (forkNetwork) { const USDC_NAME = 'USDC' const CUSDC_NAME = 'cUSDCv3' + const USDC_HOLDERS: { [key: string]: string } = { '1': '0x0a59649758aa4d66e25f08dd01271e891fe52199', '8453': '0xcdac0d6c6c59727a65f871236188350531885c43', '42161': '0x2df1c51e09aecf9cacb7bc98cb1742757f163df7', } -const FORK_BLOCKS: { [key: string]: number } = { +const USDT_HOLDERS: { [key: string]: string } = { + '1': '0xF977814e90dA44bFA03b6295A0616a897441aceC', + '8453': '0x0000000000000000000000000000000000000000', + '42161': '0xF977814e90dA44bFA03b6295A0616a897441aceC', +} + +export const HOLDERS: { [key: string]: { [chainId: string]: string } } = { + USDC: USDC_HOLDERS, + USDT: USDT_HOLDERS, +} + +export const getHolder = (tokenName: string): string => { + return HOLDERS[tokenName][chainId] +} + +const USDC_FORK_BLOCKS: { [key: string]: number } = { '1': 15850930, '8453': 12292893, '42161': 193157126, } +const USDT_FORK_BLOCKS: { [key: string]: number } = { + '1': 20814000, + '8453': 12292893, // not used + '42161': 237293528, +} + +export const FORK_BLOCKS: { [key: string]: { [chainId: string]: number } } = { + USDC: USDC_FORK_BLOCKS, + USDT: USDT_FORK_BLOCKS, +} + +export const getForkBlock = (tokenName: string): number => { + return FORK_BLOCKS[tokenName][chainId] +} + // Mainnet Addresses export const RSR = networkConfig[chainId].tokens.RSR as string export const USDC_USD_PRICE_FEED = networkConfig[chainId].chainlinkFeeds.USDC as string export const CUSDC_V3 = networkConfig[chainId].tokens[CUSDC_NAME]! +export const USDC = networkConfig[chainId].tokens[USDC_NAME]! +export const USDC_DECIMALS = bn(6) + +export const USDT_USD_PRICE_FEED = networkConfig[chainId].chainlinkFeeds.USDT as string +export const CUSDT_V3 = networkConfig[chainId].tokens.cUSDTv3 as string +export const USDT = networkConfig[chainId].tokens.USDT as string +export const USDT_DECIMALS = bn(6) + export const COMP = networkConfig[chainId].tokens.COMP as string export const REWARDS = networkConfig[chainId].COMET_REWARDS! -export const USDC = networkConfig[chainId].tokens[USDC_NAME]! -export const USDC_HOLDER = USDC_HOLDERS[chainId] export const COMET_CONFIGURATOR = networkConfig[chainId].COMET_CONFIGURATOR! export const COMET_PROXY_ADMIN = networkConfig[chainId].COMET_PROXY_ADMIN! export const COMET_EXT = networkConfig[chainId].COMET_EXT! @@ -51,6 +88,3 @@ export const ORACLE_ERROR = fp('0.005') export const DEFAULT_THRESHOLD = bn(5).mul(bn(10).pow(16)) // 0.05 export const DELAY_UNTIL_DEFAULT = bn(86400) export const MAX_TRADE_VOL = bn(1000000) -export const USDC_DECIMALS = bn(6) - -export const FORK_BLOCK = FORK_BLOCKS[chainId] diff --git a/test/plugins/individual-collateral/compoundv3/helpers.ts b/test/plugins/individual-collateral/compoundv3/helpers.ts index f08566f823..dc682311b7 100644 --- a/test/plugins/individual-collateral/compoundv3/helpers.ts +++ b/test/plugins/individual-collateral/compoundv3/helpers.ts @@ -5,23 +5,25 @@ import { CometInterface, ICometConfigurator, ICometProxyAdmin, - ICusdcV3Wrapper, - CusdcV3Wrapper__factory, + ICFiatV3Wrapper, + CFiatV3Wrapper__factory, } from '../../../../typechain' import { whileImpersonating } from '../../../utils/impersonation' import { bn } from '../../../../common/numbers' import { BigNumberish } from 'ethers' import { - USDC_HOLDER, USDC, + USDT, + USDC_USD_PRICE_FEED, + USDT_USD_PRICE_FEED, COMET_CONFIGURATOR, COMET_PROXY_ADMIN, CUSDC_V3, + CUSDT_V3, REWARDS, COMP, - FORK_BLOCK, + getHolder, } from './constants' -import { getResetFork } from '../helpers' export const enableRewardsAccrual = async ( cusdcV3: CometInterface, @@ -49,60 +51,126 @@ const allocateERC20 = async (token: ERC20Mock, from: string, to: string, balance }) } -export const allocateUSDC = async ( +export const allocateToken = async ( to: string, balance: BigNumberish, - from: string = USDC_HOLDER, - token: string = USDC + from: string, + token: string ) => { - const usdc = await ethers.getContractAt('ERC20Mock', token) - await allocateERC20(usdc, from, to, balance) + const erc20 = await ethers.getContractAt('ERC20Mock', token) + await allocateERC20(erc20, from, to, balance) } -interface WrappedcUSDCFixture { - cusdcV3: CometInterface - wcusdcV3: ICusdcV3Wrapper - usdc: ERC20Mock +export interface WrappedCTokenFixture { + cTokenV3: CometInterface + wcTokenV3: ICFiatV3Wrapper + token: ERC20Mock } -export const mintWcUSDC = async ( - usdc: ERC20Mock, - cusdc: CometInterface, - wcusdc: ICusdcV3Wrapper, +export const mintWcToken = async ( + token: ERC20Mock, + cTokenV3: CometInterface, + wcTokenV3: ICFiatV3Wrapper, account: SignerWithAddress, amount: BigNumberish, recipient: string ) => { - const initBal = await cusdc.balanceOf(account.address) + const initBal = await cTokenV3.balanceOf(account.address) // do these actions together to move rate as little as possible await hre.network.provider.send('evm_setAutomine', [false]) - const usdcAmount = await wcusdc.convertStaticToDynamic(amount) - await allocateUSDC(account.address, usdcAmount) - await usdc.connect(account).approve(cusdc.address, ethers.constants.MaxUint256) - await cusdc.connect(account).allow(wcusdc.address, true) + const tokenAmount = await wcTokenV3.convertStaticToDynamic(amount) + await allocateToken(account.address, tokenAmount, getHolder(await token.symbol()), token.address) + await token.connect(account).approve(cTokenV3.address, ethers.constants.MaxUint256) + await cTokenV3.connect(account).allow(wcTokenV3.address, true) await hre.network.provider.send('evm_setAutomine', [true]) - await cusdc.connect(account).supply(usdc.address, usdcAmount) - const nowBal = await cusdc.balanceOf(account.address) + await cTokenV3.connect(account).supply(token.address, tokenAmount) + const nowBal = await cTokenV3.balanceOf(account.address) if (account.address == recipient) { - await wcusdc.connect(account).deposit(nowBal.sub(initBal)) + await wcTokenV3.connect(account).deposit(nowBal.sub(initBal)) } else { - await wcusdc.connect(account).depositTo(recipient, nowBal.sub(initBal)) + await wcTokenV3.connect(account).depositTo(recipient, nowBal.sub(initBal)) } } -export const makewCSUDC = async (): Promise => { +export const makewCSUDC = async (): Promise => { const cusdcV3 = await ethers.getContractAt('CometInterface', CUSDC_V3) - const CusdcV3WrapperFactory = ( - await ethers.getContractFactory('CusdcV3Wrapper') + const CTokenV3WrapperFactory = ( + await ethers.getContractFactory('CFiatV3Wrapper') ) - const wcusdcV3 = ( - await CusdcV3WrapperFactory.deploy(cusdcV3.address, REWARDS, COMP) + const wcusdcV3 = ( + await CTokenV3WrapperFactory.deploy( + cusdcV3.address, + REWARDS, + COMP, + 'Wrapped cUSDCv3', + 'wcUSDCv3' + ) ) const usdc = await ethers.getContractAt('ERC20Mock', USDC) - return { cusdcV3, wcusdcV3, usdc } + return { cTokenV3: cusdcV3, wcTokenV3: wcusdcV3, token: usdc } +} + +export const makewCSUDT = async (): Promise => { + const cusdtV3 = await ethers.getContractAt('CometInterface', CUSDT_V3) + const CTokenV3WrapperFactory = ( + await ethers.getContractFactory('CFiatV3Wrapper') + ) + const wcusdtV3 = ( + await CTokenV3WrapperFactory.deploy( + cusdtV3.address, + REWARDS, + COMP, + 'Wrapped cUSDTv3', + 'wcUSDTv3' + ) + ) + const usdt = await ethers.getContractAt('ERC20Mock', USDT) + + return { cTokenV3: cusdtV3, wcTokenV3: wcusdtV3, token: usdt } +} + +// Test configuration +export interface CTokenV3Enumeration { + testName: string + forkNetwork: string + wrapperName: string + wrapperSymbol: string + cTokenV3: string + token: string + tokenName: string + chainlinkFeed: string + fix: typeof makewCSUDC +} + +const cUSDCv3 = { + testName: 'CompoundV3USDC', + wrapperName: 'Wrapped cUSDCv3', + wrapperSymbol: 'wcUSDCv3', + cTokenV3: CUSDC_V3, + token: USDC, + tokenName: 'USDC', + chainlinkFeed: USDC_USD_PRICE_FEED, + fix: makewCSUDC, +} + +const cUSDTv3 = { + testName: 'CompoundV3USDT', + wrapperName: 'Wrapped cUSDTv3', + wrapperSymbol: 'wcUSDTv3', + cTokenV3: CUSDT_V3, + token: USDT, + tokenName: 'USDT', + chainlinkFeed: USDT_USD_PRICE_FEED, + fix: makewCSUDT, } -export const resetFork = getResetFork(FORK_BLOCK) +export const allTests = [ + { ...cUSDCv3, forkNetwork: 'mainnet' }, + { ...cUSDCv3, forkNetwork: 'base' }, + { ...cUSDCv3, forkNetwork: 'arbitrum' }, + { ...cUSDTv3, forkNetwork: 'mainnet' }, + { ...cUSDTv3, forkNetwork: 'arbitrum' }, +]