diff --git a/contracts/interfaces/external/morpho/IMorphoOracle.sol b/contracts/interfaces/external/morpho/IMorphoOracle.sol new file mode 100644 index 00000000..e0cfdc19 --- /dev/null +++ b/contracts/interfaces/external/morpho/IMorphoOracle.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +/// @title IMorphoOracle +/// @notice Interface for the oracle contracts used within Morpho +interface IMorphoOracle { + function price() external view returns (uint256); +} diff --git a/contracts/transmuter/Storage.sol b/contracts/transmuter/Storage.sol index c9dadfd7..1d0d04f0 100644 --- a/contracts/transmuter/Storage.sol +++ b/contracts/transmuter/Storage.sol @@ -48,7 +48,8 @@ enum OracleReadType { RETH, SFRXETH, PYTH, - MAX + MAX, + MORPHO_ORACLE } enum OracleQuoteType { @@ -65,70 +66,70 @@ enum WhitelistType { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ struct Permit2Details { - address to; // Address that will receive the funds - uint256 nonce; // Nonce of the transaction - bytes signature; // Permit signature of the user + address to; // Address that will receive the funds + uint256 nonce; // Nonce of the transaction + bytes signature; // Permit signature of the user } struct FacetCut { - address facetAddress; // Facet contract address - FacetCutAction action; // Can be add, remove or replace - bytes4[] functionSelectors; // Ex. bytes4(keccak256("transfer(address,uint256)")) + address facetAddress; // Facet contract address + FacetCutAction action; // Can be add, remove or replace + bytes4[] functionSelectors; // Ex. bytes4(keccak256("transfer(address,uint256)")) } struct Facet { - address facetAddress; // Facet contract address - bytes4[] functionSelectors; // Ex. bytes4(keccak256("transfer(address,uint256)")) + address facetAddress; // Facet contract address + bytes4[] functionSelectors; // Ex. bytes4(keccak256("transfer(address,uint256)")) } struct FacetInfo { - address facetAddress; // Facet contract address - uint16 selectorPosition; // Position in the list of all selectors + address facetAddress; // Facet contract address + uint16 selectorPosition; // Position in the list of all selectors } struct DiamondStorage { - bytes4[] selectors; // List of all available selectors - mapping(bytes4 => FacetInfo) selectorInfo; // Selector to (address, position in list) - IAccessControlManager accessControlManager; // Contract handling access control + bytes4[] selectors; // List of all available selectors + mapping(bytes4 => FacetInfo) selectorInfo; // Selector to (address, position in list) + IAccessControlManager accessControlManager; // Contract handling access control } struct ImplementationStorage { - address implementation; // Dummy implementation address for Etherscan usability + address implementation; // Dummy implementation address for Etherscan usability } struct ManagerStorage { - IERC20[] subCollaterals; // Subtokens handled by the manager or strategies - bytes config; // Additional configuration data + IERC20[] subCollaterals; // Subtokens handled by the manager or strategies + bytes config; // Additional configuration data } struct Collateral { - uint8 isManaged; // If the collateral is managed through external strategies - uint8 isMintLive; // If minting from this asset is unpaused - uint8 isBurnLive; // If burning to this asset is unpaused - uint8 decimals; // IERC20Metadata(collateral).decimals() - uint8 onlyWhitelisted; // If only whitelisted addresses can burn or redeem for this token - uint216 normalizedStables; // Normalized amount of stablecoins issued from this collateral - uint64[] xFeeMint; // Increasing exposures in [0,BASE_9[ - int64[] yFeeMint; // Mint fees at the exposures specified in `xFeeMint` - uint64[] xFeeBurn; // Decreasing exposures in ]0,BASE_9] - int64[] yFeeBurn; // Burn fees at the exposures specified in `xFeeBurn` - bytes oracleConfig; // Data about the oracle used for the collateral - bytes whitelistData; // For whitelisted collateral, data used to verify whitelists - ManagerStorage managerData; // For managed collateral, data used to handle the strategies + uint8 isManaged; // If the collateral is managed through external strategies + uint8 isMintLive; // If minting from this asset is unpaused + uint8 isBurnLive; // If burning to this asset is unpaused + uint8 decimals; // IERC20Metadata(collateral).decimals() + uint8 onlyWhitelisted; // If only whitelisted addresses can burn or redeem for this token + uint216 normalizedStables; // Normalized amount of stablecoins issued from this collateral + uint64[] xFeeMint; // Increasing exposures in [0,BASE_9[ + int64[] yFeeMint; // Mint fees at the exposures specified in `xFeeMint` + uint64[] xFeeBurn; // Decreasing exposures in ]0,BASE_9] + int64[] yFeeBurn; // Burn fees at the exposures specified in `xFeeBurn` + bytes oracleConfig; // Data about the oracle used for the collateral + bytes whitelistData; // For whitelisted collateral, data used to verify whitelists + ManagerStorage managerData; // For managed collateral, data used to handle the strategies } struct TransmuterStorage { - IAgToken agToken; // agToken handled by the system - uint8 isRedemptionLive; // If redemption is unpaused - uint8 statusReentrant; // If call is reentrant or not - uint128 normalizedStables; // Normalized amount of stablecoins issued by the system - uint128 normalizer; // To reconcile `normalizedStables` values with the actual amount - address[] collateralList; // List of collateral assets supported by the system - uint64[] xRedemptionCurve; // Increasing collateral ratios > 0 - int64[] yRedemptionCurve; // Value of the redemption fees at `xRedemptionCurve` - mapping(address => Collateral) collaterals; // Maps a collateral asset to its parameters - mapping(address => uint256) isTrusted; // If an address is trusted to update the normalizer value + IAgToken agToken; // agToken handled by the system + uint8 isRedemptionLive; // If redemption is unpaused + uint8 statusReentrant; // If call is reentrant or not + uint128 normalizedStables; // Normalized amount of stablecoins issued by the system + uint128 normalizer; // To reconcile `normalizedStables` values with the actual amount + address[] collateralList; // List of collateral assets supported by the system + uint64[] xRedemptionCurve; // Increasing collateral ratios > 0 + int64[] yRedemptionCurve; // Value of the redemption fees at `xRedemptionCurve` + mapping(address => Collateral) collaterals; // Maps a collateral asset to its parameters + mapping(address => uint256) isTrusted; // If an address is trusted to update the normalizer value mapping(address => uint256) isSellerTrusted; // If an address is trusted to sell accruing reward tokens mapping(WhitelistType => mapping(address => uint256)) isWhitelistedForType; - // Whether an address is whitelisted for a specific whitelist type + // Whether an address is whitelisted for a specific whitelist type } diff --git a/contracts/transmuter/configs/FakeGnosis.sol b/contracts/transmuter/configs/FakeGnosis.sol index 6244403b..c392f3bd 100644 --- a/contracts/transmuter/configs/FakeGnosis.sol +++ b/contracts/transmuter/configs/FakeGnosis.sol @@ -28,7 +28,6 @@ contract FakeGnosis { address[] memory _collateralAddresses, address[] memory _oracleAddresses ) external { - uint256 BPS = 1e14; // Fee structure uint64[] memory xMintFee = new uint64[](4); diff --git a/contracts/transmuter/libraries/LibOracle.sol b/contracts/transmuter/libraries/LibOracle.sol index bcfca114..486c8038 100644 --- a/contracts/transmuter/libraries/LibOracle.sol +++ b/contracts/transmuter/libraries/LibOracle.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; import { ITransmuterOracle } from "interfaces/ITransmuterOracle.sol"; import { AggregatorV3Interface } from "interfaces/external/chainlink/AggregatorV3Interface.sol"; +import { IMorphoOracle } from "interfaces/external/morpho/IMorphoOracle.sol"; import { IPyth, PythStructs } from "interfaces/external/pyth/IPyth.sol"; import { LibStorage as s } from "./LibStorage.sol"; @@ -33,25 +34,14 @@ library LibOracle { ITransmuterOracle externalOracle = abi.decode(oracleData, (ITransmuterOracle)); return externalOracle.readRedemption(); } else { - // We consider the actual oracle value and not the processed one - // as it doesn't impact directly the redemption process (oracleValue, ) = readSpotAndTarget(oracleType, targetType, oracleData, targetData, 0); - // We don't consider the mint firewall as `readRedemption` is only used to compute the collateral ratio - // `getCollateralRatio` is only used in `_quoteRedemptionCurve` and `accrue` on the savingsVest - // `_quoteRedemptionCurve` use the collateral ratio to compute the penalty factor. Artificially increase the - // oracle rate will just allow you to navigate through the penalty factor curve and when - // the collateral ratio > 100% the penalty factor curve is decreasing such that there is no incentives - // for upward manipulation - // `accrue` would be impacted by an inflated oracle value, but only governors can call this function - // We don't consider the burn firewall is less relevant for redemptions - // as there is already a surplus buffer to circumvent small deviations return oracleValue; } } /// @notice Reads the oracle value used during mint operations for an asset with `oracleConfig` - /// @dev For assets which do not rely on external oracles, this value is the minimum between the asset oracle - /// value and its target price + /// @dev For assets which do not rely on external oracles, this value is the minimum between the processed oracle + /// value for the asset and its target price function readMint(bytes memory oracleConfig) internal view returns (uint256 oracleValue) { ( OracleReadType oracleType, @@ -70,7 +60,7 @@ library LibOracle { oracleValue = _firewallMint(targetPrice, oracleValue, mintDeviation); } - /// @notice Reads the oracle value that will be used for a burn operation for an asset with `oracleConfig` + /// @notice Reads the oracle value used for a burn operation for an asset with `oracleConfig` /// @return oracleValue The actual oracle value obtained /// @return ratio If `oracle value < target price`, the ratio between the oracle value and the target /// price, otherwise `BASE_18` @@ -142,7 +132,7 @@ library LibOracle { ) internal view returns (uint256 oracleValue, uint256 targetPrice) { targetPrice = read(targetType, BASE_18, targetData); oracleValue = read(oracleType, targetPrice, oracleData); - // Post process of the oracle value, it tolerates small deviation from target + // Post process of the oracle value: system may tolerate small deviations from target oracleValue = _userOracleProtection(targetPrice, oracleValue, deviation); } @@ -197,6 +187,9 @@ library LibOracle { } else if (readType == OracleReadType.MAX) { (uint256 maxValue, , , ) = abi.decode(data, (uint256, uint96, uint96, uint32)); return maxValue; + } else if (readType == OracleReadType.MORPHO_ORACLE) { + (address contractAddress, uint256 normalizationFactor) = abi.decode(data, (address, uint256)); + return IMorphoOracle(contractAddress).price() / normalizationFactor; } // If the `OracleReadType` is `EXTERNAL`, it means that this function is called to compute a // `targetPrice` in which case the `baseValue` is returned here @@ -256,15 +249,18 @@ library LibOracle { } /// @notice Firewall in case the oracle value reported is too high compared to the target - /// --> disregard the oracle value and return the target price - /// TODO we may want something continuous ans therefore set - /// `oracleValue = targetPrice * (BASE_18 + deviation) / BASE_18` + /// @dev This mint firewall is useful in the case of assets for which the `targetPrice` is theorically defined as + /// the maximum value ever observed for the oracle, but this maximum value has simply not been updated. + /// Typically, imagine a case where target is 100 and oracle is 101, in this setup, during a mint, because + /// target should be put at 101 but hasn't been modified, the system uses for a price the oracle value function _firewallMint(uint256 targetPrice, uint256 oracleValue, uint256 deviation) private pure returns (uint256) { if (targetPrice * (BASE_18 + deviation) < oracleValue * BASE_18) oracleValue = targetPrice; return oracleValue; } /// @notice Firewall in case the oracle value reported is low compared to the target + /// @dev If the oracle value is slightly below its target, then no deviation is reported for the oracle and + /// the price of burning the stablecoin for other assets is not impacted function _burnRatio( uint256 targetPrice, uint256 oracleValue, @@ -275,7 +271,7 @@ library LibOracle { return ratio; } - /// @notice Firewall in case the oracle value reported is under a reasonable threshold to the target + /// @notice Firewall in case the oracle value reported is reasonably close to the target /// --> disregard the oracle value and return the target price function _userOracleProtection( uint256 targetPrice, diff --git a/package.json b/package.json index cf8fe347..01240d76 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,6 @@ "solhint-plugin-prettier": "^0.0.5" }, "dependencies": { - "@angleprotocol/sdk": "^0.34.5" + "@angleprotocol/sdk": "^0.36.13" } -} \ No newline at end of file +} diff --git a/scripts/Constants.s.sol b/scripts/Constants.s.sol index ee44f9e7..cbc35044 100644 --- a/scripts/Constants.s.sol +++ b/scripts/Constants.s.sol @@ -21,6 +21,8 @@ address constant EURE = 0x3231Cb76718CDeF2155FC47b5286d82e6eDA273f; address constant BC3M = 0x2F123cF3F37CE3328CC9B5b8415f9EC5109b45e7; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant BERNX = 0x3f95AA88dDbB7D9D484aa3D482bf0a80009c52c9; +address constant STEAK_USDC = 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB; +address constant BIB01 = 0xCA30c93B02514f86d5C86a6e375E3A330B435Fb5; // EUROC uint80 constant FIREWALL_MINT_EUROC = uint80(0); @@ -39,7 +41,21 @@ uint80 constant FIREWALL_BURN_RATIO_ERNX = uint80(100 * BPS); uint80 constant USER_PROTECTION_ERNX = uint80(0); uint96 constant DEVIATION_THRESHOLD_ERNX = uint96(100 * BPS); -uint32 constant HEARTBEAT = uint32(1 days); +uint32 constant HEARTBEAT = uint32(7 days); + +uint80 constant FIREWALL_MINT_USDC = uint80(0); +uint80 constant FIREWALL_BURN_RATIO_USDC = uint80(0); +uint80 constant USER_PROTECTION_USDC = uint80(5 * BPS); + +uint80 constant FIREWALL_MINT_STEAK_USDC = uint80(50 * BPS); +uint80 constant FIREWALL_BURN_RATIO_STEAK_USDC = uint80(50 * BPS); +uint80 constant USER_PROTECTION_STEAK_USDC = uint80(0); +uint256 constant DEVIATION_THRESHOLD_STEAKUSDC = 50 * BPS; + +uint80 constant FIREWALL_MINT_IB01 = uint80(70 * BPS); +uint80 constant FIREWALL_BURN_RATIO_IB01 = uint80(50 * BPS); +uint80 constant USER_PROTECTION_IB01 = uint80(0); +uint256 constant DEVIATION_THRESHOLD_IB01 = 50 * BPS; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FACET ADDRESSES diff --git a/scripts/SetupDeployedTransmuter.s.sol b/scripts/SetupDeployedTransmuter.s.sol new file mode 100644 index 00000000..c9accb42 --- /dev/null +++ b/scripts/SetupDeployedTransmuter.s.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { Utils } from "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { stdJson } from "forge-std/StdJson.sol"; +import "stringutils/strings.sol"; +import "./Constants.s.sol"; + +import "contracts/transmuter/Storage.sol" as Storage; +import { ITransmuter } from "interfaces/ITransmuter.sol"; +import "interfaces/external/chainlink/AggregatorV3Interface.sol"; +import "interfaces/external/IERC4626.sol"; + +import { CollateralSetupProd } from "contracts/transmuter/configs/ProductionTypes.sol"; + +contract SetupDeployedTransmuter is Utils { + function run() external { + uint256 deployerPrivateKey = vm.deriveKey(vm.envString("MNEMONIC_MAINNET"), "m/44'/60'/0'/0/", 0); + address deployer = vm.addr(deployerPrivateKey); + console.log("Address: %s", deployer); + console.log(deployer.balance); + vm.startBroadcast(deployerPrivateKey); + + ITransmuter usdaTransmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); + console.log(address(usdaTransmuter)); + + // TODO Run this script after facet upgrade script otherwise it won't work due to oracles calibrated + // in a different manner + + // For USDC, we just need to update the oracle as the fees have already been properly set for this use case + + { + bytes memory oracleConfig; + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + + // Chainlink USDC/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); + stalePeriods[0] = ((1 days) * 3) / 2; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + bytes memory targetData; + oracleConfig = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint80(5 * BPS), uint80(0), uint80(0)) + ); + usdaTransmuter.setOracle(USDC, oracleConfig); + } + + // Set Collaterals + CollateralSetupProd[] memory collaterals = new CollateralSetupProd[](2); + + // IB01 + { + uint64[] memory xMintFeeIB01 = new uint64[](3); + xMintFeeIB01[0] = uint64(0); + xMintFeeIB01[1] = uint64((49 * BASE_9) / 100); + xMintFeeIB01[2] = uint64((50 * BASE_9) / 100); + + int64[] memory yMintFeeIB01 = new int64[](3); + yMintFeeIB01[0] = int64(0); + yMintFeeIB01[1] = int64(0); + yMintFeeIB01[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeIB01 = new uint64[](3); + xBurnFeeIB01[0] = uint64(BASE_9); + xBurnFeeIB01[1] = uint64((16 * BASE_9) / 100); + xBurnFeeIB01[2] = uint64((15 * BASE_9) / 100); + + int64[] memory yBurnFeeIB01 = new int64[](3); + yBurnFeeIB01[0] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeIB01[1] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeIB01[2] = int64(uint64(MAX_BURN_FEE)); + + bytes memory oracleConfig; + { + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + // Chainlink IB01/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x32d1463EB53b73C095625719Afa544D5426354cB); + stalePeriods[0] = ((1 days) * 3) / 2; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + + // Current value is 109.43, but we need to update it now otherwise we'll have to wait for a week + // before we can update it + (, int256 answer, , , ) = AggregatorV3Interface(0x32d1463EB53b73C095625719Afa544D5426354cB) + .latestRoundData(); + uint256 initTarget = uint256(answer) * 1e10; + bytes memory targetData = abi.encode( + initTarget, + uint96(DEVIATION_THRESHOLD_IB01), + uint96(block.timestamp), + HEARTBEAT + ); + + oracleConfig = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_IB01, FIREWALL_MINT_IB01, FIREWALL_BURN_RATIO_IB01) + ); + } + collaterals[0] = CollateralSetupProd( + BIB01, + oracleConfig, + xMintFeeIB01, + yMintFeeIB01, + xBurnFeeIB01, + yBurnFeeIB01 + ); + } + + // steakUSDC -> max oracle or target oracle + { + uint64[] memory xMintFeeSteak = new uint64[](3); + xMintFeeSteak[0] = uint64(0); + xMintFeeSteak[1] = uint64((79 * BASE_9) / 100); + xMintFeeSteak[2] = uint64((80 * BASE_9) / 100); + + int64[] memory yMintFeeSteak = new int64[](3); + yMintFeeSteak[0] = int64(uint64((5 * BASE_9) / 10000)); + yMintFeeSteak[1] = int64(uint64((5 * BASE_9) / 10000)); + yMintFeeSteak[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeSteak = new uint64[](3); + xBurnFeeSteak[0] = uint64(BASE_9); + xBurnFeeSteak[1] = uint64((31 * BASE_9) / 100); + xBurnFeeSteak[2] = uint64((30 * BASE_9) / 100); + + int64[] memory yBurnFeeSteak = new int64[](3); + yBurnFeeSteak[0] = int64(uint64((5 * BASE_9) / 10000)); + yBurnFeeSteak[1] = int64(uint64((5 * BASE_9) / 10000)); + yBurnFeeSteak[2] = int64(uint64(MAX_BURN_FEE)); + + bytes memory oracleConfig; + { + bytes memory readData = abi.encode(0x025106374196586E8BC91eE8818dD7B0Efd2B78B, BASE_18); + // Current price is 1.012534 -> we take a small margin + uint256 startPrice = IERC4626(STEAK_USDC).previewRedeem(1e30); + bytes memory targetData = abi.encode( + startPrice, + uint96(DEVIATION_THRESHOLD_STEAKUSDC), + uint96(block.timestamp), + HEARTBEAT + ); + oracleConfig = abi.encode( + Storage.OracleReadType.MORPHO_ORACLE, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_STEAK_USDC, FIREWALL_MINT_STEAK_USDC, FIREWALL_BURN_RATIO_STEAK_USDC) + ); + } + collaterals[1] = CollateralSetupProd( + STEAK_USDC, + oracleConfig, + xMintFeeSteak, + yMintFeeSteak, + xBurnFeeSteak, + yBurnFeeSteak + ); + } + + // Setup each collateral + uint256 collateralsLength = collaterals.length; + for (uint256 i; i < collateralsLength; i++) { + CollateralSetupProd memory collateral = collaterals[i]; + usdaTransmuter.addCollateral(collateral.token); + usdaTransmuter.setOracle(collateral.token, collateral.oracleConfig); + // Mint fees + usdaTransmuter.setFees(collateral.token, collateral.xMintFee, collateral.yMintFee, true); + // Burn fees + usdaTransmuter.setFees(collateral.token, collateral.xBurnFee, collateral.yBurnFee, false); + usdaTransmuter.togglePause(collateral.token, Storage.ActionType.Mint); + usdaTransmuter.togglePause(collateral.token, Storage.ActionType.Burn); + } + + // Set whitelist status for bIB01 + bytes memory whitelistData = abi.encode( + Storage.WhitelistType.BACKED, + abi.encode(address(0x9391B14dB2d43687Ea1f6E546390ED4b20766c46)) + ); + usdaTransmuter.setWhitelistStatus(BIB01, 1, whitelistData); + + usdaTransmuter.toggleTrusted(NEW_DEPLOYER, Storage.TrustedType.Seller); + usdaTransmuter.toggleTrusted(NEW_KEEPER, Storage.TrustedType.Seller); + + console.log("Transmuter setup"); + vm.stopBroadcast(); + } +} diff --git a/test/fuzz/OracleTest.t.sol b/test/fuzz/OracleTest.t.sol index bf47f580..40370d20 100644 --- a/test/fuzz/OracleTest.t.sol +++ b/test/fuzz/OracleTest.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; import { ITransmuterOracle, MockExternalOracle } from "../mock/MockExternalOracle.sol"; +import { MockMorphoOracle } from "../mock/MockMorphoOracle.sol"; import { MockPyth } from "../mock/MockPyth.sol"; import { IERC20Metadata } from "../mock/MockTokenPermit.sol"; import "../Fixture.sol"; @@ -530,6 +531,51 @@ contract OracleTest is Fixture, FunctionUtils { vm.stopPrank(); } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MORPHO + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_ReadMorphoFeed(uint256[3] memory baseValues, uint256[3] memory normalizers) public { + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + baseValues[i] = uint256(bound(baseValues[i], 100, 1e5)); + normalizers[i] = uint256(bound(normalizers[i], 1, 18)); + MockMorphoOracle morphoOracle = new MockMorphoOracle(baseValues[i] * 1e36); + { + Storage.OracleReadType readType = Storage.OracleReadType.MORPHO_ORACLE; + Storage.OracleReadType targetType = Storage.OracleReadType.MAX; + bytes memory readData = abi.encode(address(morphoOracle), 10 ** normalizers[i]); + bytes memory targetData = abi.encode( + (baseValues[i] * 1e36) / 10 ** normalizers[i], + uint96(block.timestamp), + 0, + 1 days + ); + transmuter.setOracle( + _collaterals[i], + abi.encode(readType, targetType, readData, targetData, abi.encode(uint80(0), uint80(0), uint80(0))) + ); + } + (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) = transmuter + .getOracleValues(address(_collaterals[i])); + assertEq(mint, (baseValues[i] * 1e36) / 10 ** normalizers[i]); + assertEq(burn, (baseValues[i] * 1e36) / 10 ** normalizers[i]); + assertEq(ratio, BASE_18); + assertEq(minRatio, BASE_18); + assertEq(redemption, (baseValues[i] * 1e36) / 10 ** normalizers[i]); + if (i == 2) { + morphoOracle.setValue((baseValues[i] * 1e36 * 9) / 10); + (mint, burn, ratio, minRatio, redemption) = transmuter.getOracleValues(address(_collaterals[i])); + assertEq(mint, ((baseValues[i] * 1e36) * 9) / 10 ** normalizers[i] / 10); + assertEq(burn, ((baseValues[i] * 1e36) * 9) / 10 ** normalizers[i] / 10); + assertEq(ratio, (BASE_18 * 9) / 10); + assertEq(minRatio, (BASE_18 * 9) / 10); + assertEq(redemption, ((baseValues[i] * 1e36) * 9) / 10 ** normalizers[i] / 10); + } + } + vm.stopPrank(); + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FIREWALL //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ diff --git a/test/mock/MockMorphoOracle.sol b/test/mock/MockMorphoOracle.sol new file mode 100644 index 00000000..0c3f1d84 --- /dev/null +++ b/test/mock/MockMorphoOracle.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.19; + +import { IMorphoOracle } from "contracts/interfaces/external/morpho/IMorphoOracle.sol"; + +contract MockMorphoOracle is IMorphoOracle { + uint256 value; + + constructor(uint256 _value) { + value = _value; + } + + function price() external view returns (uint256) { + return value; + } + + function setValue(uint256 _value) external { + value = _value; + } +} diff --git a/test/scripts/UpdateTransmuterFacetsUSDATest.t.sol b/test/scripts/UpdateTransmuterFacetsUSDATest.t.sol new file mode 100644 index 00000000..93a288db --- /dev/null +++ b/test/scripts/UpdateTransmuterFacetsUSDATest.t.sol @@ -0,0 +1,711 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { stdJson } from "forge-std/StdJson.sol"; +import { console } from "forge-std/console.sol"; +import { Test } from "forge-std/Test.sol"; + +import "../../scripts/Constants.s.sol"; + +import { Helpers } from "../../scripts/Helpers.s.sol"; +import "contracts/utils/Errors.sol" as Errors; +import "contracts/transmuter/Storage.sol" as Storage; +import { Getters } from "contracts/transmuter/facets/Getters.sol"; +import { Redeemer } from "contracts/transmuter/facets/Redeemer.sol"; +import { SettersGovernor } from "contracts/transmuter/facets/SettersGovernor.sol"; +import { SettersGuardian } from "contracts/transmuter/facets/SettersGuardian.sol"; +import { Swapper } from "contracts/transmuter/facets/Swapper.sol"; +import "contracts/transmuter/libraries/LibHelpers.sol"; +import "interfaces/external/chainlink/AggregatorV3Interface.sol"; +import { CollateralSetupProd } from "contracts/transmuter/configs/ProductionTypes.sol"; +import { ITransmuter } from "interfaces/ITransmuter.sol"; +import "utils/src/Constants.sol"; +import { IERC20 } from "oz/interfaces/IERC20.sol"; +import { MockMorphoOracle } from "../mock/MockMorphoOracle.sol"; + +interface OldTransmuter { + function getOracle( + address + ) external view returns (Storage.OracleReadType, Storage.OracleReadType, bytes memory, bytes memory); +} + +contract UpdateTransmuterFacetsUSDATest is Helpers, Test { + using stdJson for string; + + uint256 public CHAIN_SOURCE; + + address constant WHALE_USDA = 0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701; + + string[] replaceFacetNames; + string[] addFacetNames; + address[] facetAddressList; + address[] addFacetAddressList; + + ITransmuter transmuter; + IERC20 USDA; + address governor; + bytes public oracleConfigUSDC; + bytes public oracleConfigIB01; + bytes public oracleConfigSTEAK; + + function setUp() public override { + super.setUp(); + + CHAIN_SOURCE = CHAIN_ETHEREUM; + + ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19483530); + vm.selectFork(forkIdentifier[CHAIN_SOURCE]); + + governor = DEPLOYER; + transmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); + USDA = IERC20(0x0000206329b97DB379d5E1Bf586BbDB969C63274); + + Storage.FacetCut[] memory replaceCut; + Storage.FacetCut[] memory addCut; + + replaceFacetNames.push("Getters"); + facetAddressList.push(address(new Getters())); + console.log("Getters deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("Redeemer"); + facetAddressList.push(address(new Redeemer())); + console.log("Redeemer deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("SettersGovernor"); + address settersGovernor = address(new SettersGovernor()); + facetAddressList.push(settersGovernor); + console.log("SettersGovernor deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("Swapper"); + facetAddressList.push(address(new Swapper())); + console.log("Swapper deployed at: ", facetAddressList[facetAddressList.length - 1]); + + addFacetNames.push("SettersGovernor"); + addFacetAddressList.push(settersGovernor); + + string memory jsonReplace = vm.readFile(JSON_SELECTOR_PATH_REPLACE); + { + // Build appropriate payload + uint256 n = replaceFacetNames.length; + replaceCut = new Storage.FacetCut[](n); + for (uint256 i = 0; i < n; ++i) { + // Get Selectors from json + bytes4[] memory selectors = _arrayBytes32ToBytes4( + jsonReplace.readBytes32Array(string.concat("$.", replaceFacetNames[i])) + ); + + replaceCut[i] = Storage.FacetCut({ + facetAddress: facetAddressList[i], + action: Storage.FacetCutAction.Replace, + functionSelectors: selectors + }); + } + } + + string memory jsonAdd = vm.readFile(JSON_SELECTOR_PATH_ADD); + { + // Build appropriate payload + uint256 n = addFacetNames.length; + addCut = new Storage.FacetCut[](n); + for (uint256 i = 0; i < n; ++i) { + // Get Selectors from json + bytes4[] memory selectors = _arrayBytes32ToBytes4( + jsonAdd.readBytes32Array(string.concat("$.", addFacetNames[i])) + ); + addCut[i] = Storage.FacetCut({ + facetAddress: addFacetAddressList[i], + action: Storage.FacetCutAction.Add, + functionSelectors: selectors + }); + } + } + + vm.startPrank(governor); + + bytes memory callData; + // set the right implementations + transmuter.diamondCut(replaceCut, address(0), callData); + transmuter.diamondCut(addCut, address(0), callData); + + { + bytes memory oracleConfig; + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + + // Chainlink USDC/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); + stalePeriods[0] = ((1 days) * 3) / 2; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + bytes memory targetData; + oracleConfig = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint80(5 * BPS), uint80(0), uint80(0)) + ); + oracleConfigUSDC = oracleConfig; + transmuter.setOracle(USDC, oracleConfig); + (, , , , uint256 redemptionPrice) = transmuter.getOracleValues(address(USDC)); + } + + // Set Collaterals + CollateralSetupProd[] memory collaterals = new CollateralSetupProd[](2); + + // IB01 + { + uint64[] memory xMintFeeIB01 = new uint64[](3); + xMintFeeIB01[0] = uint64(0); + xMintFeeIB01[1] = uint64((49 * BASE_9) / 100); + xMintFeeIB01[2] = uint64((50 * BASE_9) / 100); + + int64[] memory yMintFeeIB01 = new int64[](3); + yMintFeeIB01[0] = int64(0); + yMintFeeIB01[1] = int64(0); + yMintFeeIB01[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeIB01 = new uint64[](3); + xBurnFeeIB01[0] = uint64(BASE_9); + xBurnFeeIB01[1] = uint64((16 * BASE_9) / 100); + xBurnFeeIB01[2] = uint64((15 * BASE_9) / 100); + + int64[] memory yBurnFeeIB01 = new int64[](3); + yBurnFeeIB01[0] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeIB01[1] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeIB01[2] = int64(uint64(MAX_BURN_FEE)); + + bytes memory oracleConfig; + { + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + // Chainlink IB01/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x32d1463EB53b73C095625719Afa544D5426354cB); + stalePeriods[0] = ((1 days) * 3) / 2; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + + // Current value is 109.43, but we need to update it now otherwise we'll have to wait for a week + // before we can update it + (, int256 answer, , , ) = AggregatorV3Interface(0x32d1463EB53b73C095625719Afa544D5426354cB) + .latestRoundData(); + uint256 initTarget = uint256(answer) * 1e10; + bytes memory targetData = abi.encode( + initTarget, + uint96(DEVIATION_THRESHOLD_IB01), + uint96(block.timestamp), + HEARTBEAT + ); + + oracleConfig = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_IB01, FIREWALL_MINT_IB01, FIREWALL_BURN_RATIO_IB01) + ); + oracleConfigIB01 = oracleConfig; + } + collaterals[0] = CollateralSetupProd( + BIB01, + oracleConfig, + xMintFeeIB01, + yMintFeeIB01, + xBurnFeeIB01, + yBurnFeeIB01 + ); + } + + // steakUSDC -> max oracle or target oracle + { + uint64[] memory xMintFeeSteak = new uint64[](3); + xMintFeeSteak[0] = uint64(0); + xMintFeeSteak[1] = uint64((79 * BASE_9) / 100); + xMintFeeSteak[2] = uint64((80 * BASE_9) / 100); + + int64[] memory yMintFeeSteak = new int64[](3); + yMintFeeSteak[0] = int64(0); + yMintFeeSteak[1] = int64(0); + yMintFeeSteak[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeSteak = new uint64[](3); + xBurnFeeSteak[0] = uint64(BASE_9); + xBurnFeeSteak[1] = uint64((31 * BASE_9) / 100); + xBurnFeeSteak[2] = uint64((30 * BASE_9) / 100); + + int64[] memory yBurnFeeSteak = new int64[](3); + yBurnFeeSteak[0] = int64(0); + yBurnFeeSteak[1] = int64(0); + yBurnFeeSteak[2] = int64(uint64(MAX_BURN_FEE)); + + bytes memory oracleConfig; + { + bytes memory readData = abi.encode(0x025106374196586E8BC91eE8818dD7B0Efd2B78B, BASE_18); + // Current price is 1.012534 -> we take a small margin + bytes memory targetData = abi.encode( + 1013000000000000000, + uint96(DEVIATION_THRESHOLD_STEAKUSDC), + uint96(block.timestamp), + HEARTBEAT + ); + oracleConfig = abi.encode( + Storage.OracleReadType.MORPHO_ORACLE, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_STEAK_USDC, FIREWALL_MINT_STEAK_USDC, FIREWALL_BURN_RATIO_STEAK_USDC) + ); + oracleConfigSTEAK = oracleConfig; + } + collaterals[1] = CollateralSetupProd( + STEAK_USDC, + oracleConfig, + xMintFeeSteak, + yMintFeeSteak, + xBurnFeeSteak, + yBurnFeeSteak + ); + } + + // Setup each collateral + uint256 collateralsLength = collaterals.length; + for (uint256 i; i < collateralsLength; i++) { + CollateralSetupProd memory collateral = collaterals[i]; + transmuter.addCollateral(collateral.token); + transmuter.setOracle(collateral.token, collateral.oracleConfig); + // Mint fees + transmuter.setFees(collateral.token, collateral.xMintFee, collateral.yMintFee, true); + // Burn fees + transmuter.setFees(collateral.token, collateral.xBurnFee, collateral.yBurnFee, false); + transmuter.togglePause(collateral.token, Storage.ActionType.Mint); + transmuter.togglePause(collateral.token, Storage.ActionType.Burn); + } + + // Set whitelist status for bIB01 + bytes memory whitelistData = abi.encode( + WhitelistType.BACKED, + // Keyring whitelist check + abi.encode(address(0x9391B14dB2d43687Ea1f6E546390ED4b20766c46)) + ); + transmuter.setWhitelistStatus(BIB01, 1, whitelistData); + transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, WHALE_USDA); + + transmuter.toggleTrusted(NEW_DEPLOYER, Storage.TrustedType.Seller); + transmuter.toggleTrusted(NEW_KEEPER, Storage.TrustedType.Seller); + + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GETTERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testUnit_UpgradeUSDA_AgToken() external { + assertEq(address(transmuter.agToken()), 0x0000206329b97DB379d5E1Bf586BbDB969C63274); + } + + function testUnit_UpgradeUSDA_GetCollateralList() external { + address[] memory collateralList = transmuter.getCollateralList(); + assertEq(collateralList.length, 3); + assertEq(collateralList[0], address(USDC)); + assertEq(collateralList[1], address(BIB01)); + assertEq(collateralList[2], address(STEAK_USDC)); + } + + function testUnit_UpgradeUSDA_GetCollateralInfo() external { + { + Storage.Collateral memory collatInfoUSDC = transmuter.getCollateralInfo(address(USDC)); + assertEq(collatInfoUSDC.isManaged, 0); + assertEq(collatInfoUSDC.isMintLive, 1); + assertEq(collatInfoUSDC.isBurnLive, 1); + assertEq(collatInfoUSDC.decimals, 6); + assertEq(collatInfoUSDC.onlyWhitelisted, 0); + assertEq(collatInfoUSDC.oracleConfig, oracleConfigUSDC); + assertEq(collatInfoUSDC.whitelistData.length, 0); + assertEq(collatInfoUSDC.managerData.subCollaterals.length, 0); + assertEq(collatInfoUSDC.managerData.config.length, 0); + + { + assertEq(collatInfoUSDC.xFeeMint.length, 1); + assertEq(collatInfoUSDC.yFeeMint.length, 1); + assertEq(collatInfoUSDC.xFeeMint[0], 0); + assertEq(collatInfoUSDC.yFeeMint[0], 0); + } + { + assertEq(collatInfoUSDC.xFeeBurn.length, 1); + assertEq(collatInfoUSDC.yFeeBurn.length, 1); + assertEq(collatInfoUSDC.xFeeBurn[0], 1000000000); + assertEq(collatInfoUSDC.yFeeBurn[0], 0); + } + } + + { + Storage.Collateral memory collatInfoIB01 = transmuter.getCollateralInfo(address(BIB01)); + assertEq(collatInfoIB01.isManaged, 0); + assertEq(collatInfoIB01.isMintLive, 1); + assertEq(collatInfoIB01.isBurnLive, 1); + assertEq(collatInfoIB01.decimals, 18); + assertEq(collatInfoIB01.onlyWhitelisted, 1); + assertEq(collatInfoIB01.oracleConfig, oracleConfigIB01); + { + (Storage.WhitelistType whitelist, bytes memory data) = abi.decode( + collatInfoIB01.whitelistData, + (Storage.WhitelistType, bytes) + ); + address keyringGuard = abi.decode(data, (address)); + assertEq(uint8(whitelist), uint8(Storage.WhitelistType.BACKED)); + assertEq(keyringGuard, 0x4954c61984180868495D1a7Fb193b05a2cbd9dE3); + } + assertEq(collatInfoIB01.managerData.subCollaterals.length, 0); + assertEq(collatInfoIB01.managerData.config.length, 0); + + { + assertEq(collatInfoIB01.xFeeMint.length, 3); + assertEq(collatInfoIB01.yFeeMint.length, 3); + assertEq(collatInfoIB01.xFeeMint[0], 0); + assertEq(collatInfoIB01.xFeeMint[1], 490000000); + assertEq(collatInfoIB01.xFeeMint[2], 500000000); + assertEq(collatInfoIB01.yFeeMint[0], 0); + assertEq(collatInfoIB01.yFeeMint[1], 0); + assertEq(collatInfoIB01.yFeeMint[2], 999999999999); + } + { + assertEq(collatInfoIB01.xFeeBurn.length, 3); + assertEq(collatInfoIB01.yFeeBurn.length, 3); + assertEq(collatInfoIB01.xFeeBurn[0], 1000000000); + assertEq(collatInfoIB01.xFeeBurn[1], 160000000); + assertEq(collatInfoIB01.xFeeBurn[2], 150000000); + assertEq(collatInfoIB01.yFeeBurn[0], 5000000); + assertEq(collatInfoIB01.yFeeBurn[1], 5000000); + assertEq(collatInfoIB01.yFeeBurn[2], 999000000); + } + } + + { + Storage.Collateral memory collatInfoSTEAK = transmuter.getCollateralInfo(address(STEAK_USDC)); + assertEq(collatInfoSTEAK.isManaged, 0); + assertEq(collatInfoSTEAK.isMintLive, 1); + assertEq(collatInfoSTEAK.isBurnLive, 1); + assertEq(collatInfoSTEAK.decimals, 18); + assertEq(collatInfoSTEAK.onlyWhitelisted, 0); + assertEq(collatInfoSTEAK.oracleConfig, oracleConfigSTEAK); + assertEq(collatInfoSTEAK.managerData.subCollaterals.length, 0); + assertEq(collatInfoSTEAK.managerData.config.length, 0); + + { + assertEq(collatInfoSTEAK.xFeeMint.length, 3); + assertEq(collatInfoSTEAK.yFeeMint.length, 3); + assertEq(collatInfoSTEAK.xFeeMint[0], 0); + assertEq(collatInfoSTEAK.xFeeMint[1], 790000000); + assertEq(collatInfoSTEAK.xFeeMint[2], 800000000); + assertEq(collatInfoSTEAK.yFeeMint[0], 0); + assertEq(collatInfoSTEAK.yFeeMint[1], 0); + assertEq(collatInfoSTEAK.yFeeMint[2], 999999999999); + } + { + assertEq(collatInfoSTEAK.xFeeBurn.length, 3); + assertEq(collatInfoSTEAK.yFeeBurn.length, 3); + assertEq(collatInfoSTEAK.xFeeBurn[0], 1000000000); + assertEq(collatInfoSTEAK.yFeeBurn[0], 0); + assertEq(collatInfoSTEAK.xFeeBurn[1], 310000000); + assertEq(collatInfoSTEAK.yFeeBurn[1], 0); + assertEq(collatInfoSTEAK.xFeeBurn[2], 300000000); + assertEq(collatInfoSTEAK.yFeeBurn[2], 999000000); + } + } + } + + function testUnit_UpgradeUSDA_GetCollateralDecimals() external { + assertEq(transmuter.getCollateralDecimals(address(USDC)), 6); + assertEq(transmuter.getCollateralDecimals(address(STEAK_USDC)), 18); + assertEq(transmuter.getCollateralDecimals(address(BIB01)), 18); + } + + function testUnit_UpgradeUSDA_GetCollateralRatio() external { + (uint64 collatRatio, uint256 stablecoinIssued) = transmuter.getCollateralRatio(); + + assertApproxEqRel(collatRatio, 1000173196, BPS * 100); + assertApproxEqRel(stablecoinIssued, 1199993347000000000000, 100 * BPS); + } + + function testUnit_UpgradeUSDA_isTrusted() external { + assertEq(transmuter.isTrusted(address(governor)), false); + assertEq(transmuter.isTrustedSeller(address(governor)), false); + assertEq(transmuter.isTrusted(DEPLOYER), false); + assertEq(transmuter.isTrustedSeller(DEPLOYER), false); + assertEq(transmuter.isTrusted(NEW_DEPLOYER), false); + assertEq(transmuter.isTrustedSeller(NEW_DEPLOYER), true); + assertEq(transmuter.isTrusted(KEEPER), false); + assertEq(transmuter.isTrustedSeller(KEEPER), false); + assertEq(transmuter.isTrusted(NEW_KEEPER), false); + assertEq(transmuter.isTrustedSeller(NEW_KEEPER), true); + } + + function testUnit_UpgradeUSDA_IsWhitelistedForCollateral() external { + assertEq(transmuter.isWhitelistedForCollateral(address(USDC), alice), true); + assertEq(transmuter.isWhitelistedForCollateral(address(BIB01), alice), false); + assertEq(transmuter.isWhitelistedForCollateral(address(STEAK_USDC), alice), true); + vm.startPrank(governor); + transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, alice); + vm.stopPrank(); + assertEq(transmuter.isWhitelistedForCollateral(address(BIB01), alice), true); + assertEq(transmuter.isWhitelistedForCollateral(address(BIB01), WHALE_USDA), true); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ORACLE + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testUnit_UpgradeUSDA_getOracleValues_Success() external { + _checkOracleValues(address(USDC), BASE_18, USER_PROTECTION_USDC, FIREWALL_MINT_USDC, FIREWALL_BURN_RATIO_USDC); + _checkOracleValues( + address(BIB01), + 109480000000000000000, + USER_PROTECTION_IB01, + FIREWALL_MINT_IB01, + FIREWALL_BURN_RATIO_IB01 + ); + _checkOracleValues( + address(STEAK_USDC), + 1013000000000000000, + USER_PROTECTION_STEAK_USDC, + FIREWALL_MINT_STEAK_USDC, + FIREWALL_BURN_RATIO_STEAK_USDC + ); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MINT + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_UpgradeUSDA_QuoteMintExactInput_Reflexivity(uint256 amountIn, uint256 fromToken) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + amountIn = bound(amountIn, BASE_6, collateral != USDC ? 1000 * BASE_18 : BASE_6 * 1e6); + + uint256 amountStable = transmuter.quoteIn(amountIn, collateral, address(USDA)); + uint256 amountInReflexive = transmuter.quoteOut(amountStable, collateral, address(USDA)); + assertApproxEqRel(amountIn, amountInReflexive, BPS * 10); + } + + function testFuzz_UpgradeUSDA_QuoteMintExactInput_Independent( + uint256 amountIn, + uint256 splitProportion, + uint256 fromToken + ) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + amountIn = bound(amountIn, BASE_6, collateral != USDC ? 1000 * BASE_18 : BASE_6 * 1e6); + splitProportion = bound(splitProportion, 0, BASE_9); + + uint256 amountStable = transmuter.quoteIn(amountIn, collateral, address(USDA)); + uint256 amountInSplit1 = (amountIn * splitProportion) / BASE_9; + amountInSplit1 = amountInSplit1 == 0 ? 1 : amountInSplit1; + uint256 amountStableSplit1 = transmuter.quoteIn(amountInSplit1, collateral, address(USDA)); + // do the swap to update the system + _mintExactInput(alice, collateral, amountInSplit1, amountStableSplit1); + uint256 amountStableSplit2 = transmuter.quoteIn(amountIn - amountInSplit1, collateral, address(USDA)); + assertApproxEqRel(amountStableSplit1 + amountStableSplit2, amountStable, BPS * 10); + } + + function testFuzz_UpgradeUSDA_MintExactOutput(uint256 stableAmount, uint256 fromToken) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + stableAmount = bound(stableAmount, BASE_18, BASE_6 * 1e18); + + uint256 prevBalanceStable = USDA.balanceOf(alice); + uint256 prevTransmuterCollat = IERC20(collateral).balanceOf(address(transmuter)); + uint256 prevAgTokenSupply = IERC20(USDA).totalSupply(); + (uint256 prevStableAmountCollat, uint256 prevStableAmount) = transmuter.getIssuedByCollateral(collateral); + + uint256 amountIn = transmuter.quoteOut(stableAmount, collateral, address(USDA)); + if (amountIn == 0 || stableAmount == 0) return; + _mintExactOutput(alice, collateral, stableAmount, amountIn); + + uint256 balanceStable = USDA.balanceOf(alice); + + assertEq(balanceStable, prevBalanceStable + stableAmount); + assertEq(USDA.totalSupply(), prevAgTokenSupply + stableAmount); + assertEq(IERC20(collateral).balanceOf(alice), 0); + assertEq(IERC20(collateral).balanceOf(address(transmuter)), prevTransmuterCollat + amountIn); + + (uint256 newStableAmountCollat, uint256 newStableAmount) = transmuter.getIssuedByCollateral(collateral); + + assertApproxEqAbs(newStableAmountCollat, prevStableAmountCollat + stableAmount, 1 wei); + assertApproxEqAbs(newStableAmount, prevStableAmount + stableAmount, 1 wei); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + BURN + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_UpgradeUSDA_QuoteBurnExactInput_Independent( + uint256 amountStable, + uint256 splitProportion + ) public { + amountStable = bound(amountStable, 1, BASE_6); + splitProportion = bound(splitProportion, 0, BASE_9); + + uint256 amountOut = transmuter.quoteIn(amountStable, address(USDA), USDC); + uint256 amountStableSplit1 = (amountStable * splitProportion) / BASE_9; + amountStableSplit1 = amountStableSplit1 == 0 ? 1 : amountStableSplit1; + uint256 amountOutSplit1 = transmuter.quoteIn(amountStableSplit1, address(USDA), USDC); + // do the swap to update the system + _burnExactInput(WHALE_USDA, USDC, amountStableSplit1, amountOutSplit1); + uint256 amountOutSplit2 = transmuter.quoteIn(amountStable - amountStableSplit1, address(USDA), USDC); + assertApproxEqRel(amountOutSplit1 + amountOutSplit2, amountOut, BPS * 10); + } + + function testFuzz_UpgradeUSDA_BurnExactOutput(uint256 amountOut) public { + amountOut = bound(amountOut, 1, BASE_6); + + uint256 prevBalanceStable = USDA.balanceOf(WHALE_USDA); + uint256 prevBalanceUSDC = IERC20(USDC).balanceOf(WHALE_USDA); + uint256 prevTransmuterCollat = IERC20(USDC).balanceOf(address(transmuter)); + uint256 prevAgTokenSupply = IERC20(USDA).totalSupply(); + (uint256 prevStableAmountCollat, uint256 prevStableAmount) = transmuter.getIssuedByCollateral(USDC); + + uint256 stableAmount = transmuter.quoteOut(amountOut, address(USDA), USDC); + if (amountOut == 0 || stableAmount == 0) return; + _burnExactOutput(WHALE_USDA, USDC, amountOut, stableAmount); + + uint256 balanceStable = USDA.balanceOf(WHALE_USDA); + + assertEq(balanceStable, prevBalanceStable - stableAmount); + assertEq(USDA.totalSupply(), prevAgTokenSupply - stableAmount); + assertEq(IERC20(USDC).balanceOf(WHALE_USDA), amountOut + prevBalanceUSDC); + assertEq(IERC20(USDC).balanceOf(address(transmuter)), prevTransmuterCollat - amountOut); + + (uint256 newStableAmountCollat, uint256 newStableAmount) = transmuter.getIssuedByCollateral(USDC); + + assertApproxEqAbs(newStableAmountCollat, prevStableAmountCollat - stableAmount, 1 wei); + assertApproxEqAbs(newStableAmount, prevStableAmount - stableAmount, 1 wei); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + UTILS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _mintExactOutput( + address owner, + address tokenIn, + uint256 amountStable, + uint256 estimatedAmountIn + ) internal { + vm.startPrank(owner); + deal(tokenIn, owner, estimatedAmountIn); + IERC20(tokenIn).approve(address(transmuter), type(uint256).max); + transmuter.swapExactOutput(amountStable, estimatedAmountIn, tokenIn, address(USDA), owner, block.timestamp * 2); + vm.stopPrank(); + } + + function _mintExactInput(address owner, address tokenIn, uint256 amountIn, uint256 estimatedStable) internal { + vm.startPrank(owner); + deal(tokenIn, owner, amountIn); + IERC20(tokenIn).approve(address(transmuter), type(uint256).max); + transmuter.swapExactInput(amountIn, estimatedStable, tokenIn, address(USDA), owner, block.timestamp * 2); + vm.stopPrank(); + } + + function _burnExactInput( + address owner, + address tokenOut, + uint256 amountStable, + uint256 estimatedAmountOut + ) internal returns (bool burnMoreThanHad) { + // we need to increase the balance because fees are negative and we need to transfer + // more than what we received with the mint + if (IERC20(tokenOut).balanceOf(address(transmuter)) < estimatedAmountOut) { + deal(tokenOut, address(transmuter), estimatedAmountOut); + burnMoreThanHad = true; + } + + vm.startPrank(owner); + transmuter.swapExactInput(amountStable, estimatedAmountOut, address(USDA), tokenOut, owner, 0); + vm.stopPrank(); + } + + function _burnExactOutput( + address owner, + address tokenOut, + uint256 amountOut, + uint256 estimatedStable + ) internal returns (bool) { + // _logIssuedCollateral(); + vm.startPrank(owner); + (uint256 maxAmount, ) = transmuter.getIssuedByCollateral(tokenOut); + uint256 balanceStableOwner = USDA.balanceOf(owner); + if (estimatedStable > maxAmount) vm.expectRevert(); + else if (estimatedStable > balanceStableOwner) vm.expectRevert("ERC20: burn amount exceeds balance"); + transmuter.swapExactOutput(amountOut, estimatedStable, address(USDA), tokenOut, owner, block.timestamp * 2); + if (amountOut > maxAmount) return false; + vm.stopPrank(); + return true; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CHECKS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _checkOracleValues( + address collateral, + uint256 targetValue, + uint80 userProtection, + uint80 firewallMint, + uint80 firewallBurn + ) internal { + (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) = transmuter.getOracleValues( + collateral + ); + assertApproxEqRel(targetValue, redemption, 200 * BPS); + + if ( + targetValue * (BASE_18 - userProtection) < redemption * BASE_18 && + redemption * BASE_18 < targetValue * (BASE_18 + userProtection) + ) assertEq(burn, targetValue); + else assertEq(burn, redemption); + + if ( + targetValue * (BASE_18 - userProtection) < redemption * BASE_18 && + redemption * BASE_18 < targetValue * (BASE_18 + userProtection) + ) { + assertEq(mint, targetValue); + assertEq(ratio, BASE_18); + } else if (redemption * BASE_18 > targetValue * (BASE_18 + firewallMint)) { + assertEq(mint, targetValue); + assertEq(ratio, BASE_18); + } else if (redemption * BASE_18 < targetValue * (BASE_18 - firewallBurn)) { + assertEq(mint, redemption); + assertEq(ratio, (redemption * BASE_18) / targetValue); + } else { + assertEq(mint, redemption); + assertEq(ratio, BASE_18); + } + } +} diff --git a/yarn.lock b/yarn.lock index 68cb5faf..09b588ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@angleprotocol/sdk@^0.34.5": - version "0.34.5" - resolved "https://npm.pkg.github.com/download/@angleprotocol/sdk/0.34.5/37f08e2e540cb8b5e3fa6810b3a11654bfa97d19#37f08e2e540cb8b5e3fa6810b3a11654bfa97d19" - integrity sha512-jQFeHCwQYF9E0eA3JfdUAni/QRWsd3pUDaHGR6y2qdbfu/h3wuTxL6CTpTq/KwVahiSGcgATOe86NaiW2+VJqw== +"@angleprotocol/sdk@^0.36.13": + version "0.36.13" + resolved "https://npm.pkg.github.com/download/@angleprotocol/sdk/0.36.13/f72f660a2c8c16d0899336ffe2b31b849f6031d8#f72f660a2c8c16d0899336ffe2b31b849f6031d8" + integrity sha512-PUIrwgmi74J9NgGnrE74BrhYJSI5ytTU6zX3Umc9+4fRr/wTsELKnOrIi1Eyqzfg6wRgoZXRUp649I8Tj6CjgA== dependencies: "@apollo/client" "^3.7.17" "@typechain/ethers-v5" "^10.0.0" @@ -18,7 +18,7 @@ lodash "^4.17.21" merkletreejs "^0.3.10" tiny-invariant "^1.1.0" - typechain "^8.0.0" + typechain "^8.3.2" "@apollo/client@^3.7.17": version "3.9.5" @@ -1563,7 +1563,7 @@ tslib@^2.1.0, tslib@^2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -typechain@^8.0.0: +typechain@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/typechain/-/typechain-8.3.2.tgz#1090dd8d9c57b6ef2aed3640a516bdbf01b00d73" integrity sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==