diff --git a/contracts/interfaces/IGetters.sol b/contracts/interfaces/IGetters.sol index 858612da..52f5e3cd 100644 --- a/contracts/interfaces/IGetters.sol +++ b/contracts/interfaces/IGetters.sol @@ -114,4 +114,7 @@ interface IGetters { /// @notice Gets the data needed to deal with whitelists for `collateral` function getCollateralWhitelistData(address collateral) external view returns (bytes memory); + + /// @notice Returns the stablecoin cap for `collateral` + function getStablecoinCap(address collateral) external view returns (uint256); } diff --git a/contracts/interfaces/ISetters.sol b/contracts/interfaces/ISetters.sol index 953f6355..27d2b2eb 100644 --- a/contracts/interfaces/ISetters.sol +++ b/contracts/interfaces/ISetters.sol @@ -59,4 +59,7 @@ interface ISettersGuardian { /// @notice Changes the whitelist status for a collateral with `whitelistType` for an address `who` function toggleWhitelist(WhitelistType whitelistType, address who) external; + + /// @notice Sets the stablecoin cap that can be issued from a `collateral` + function setStablecoinCap(address collateral, uint256 stablecoinCap) external; } diff --git a/contracts/transmuter/Storage.sol b/contracts/transmuter/Storage.sol index 1854eafe..0a86fe0d 100644 --- a/contracts/transmuter/Storage.sol +++ b/contracts/transmuter/Storage.sol @@ -116,6 +116,7 @@ struct Collateral { 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 + uint256 stablecoinCap; // Cap on the amount of stablecoins that can be issued from this collateral } struct TransmuterStorage { diff --git a/contracts/transmuter/configs/Test.sol b/contracts/transmuter/configs/Test.sol index 1ee5f11f..009b1f03 100644 --- a/contracts/transmuter/configs/Test.sol +++ b/contracts/transmuter/configs/Test.sol @@ -207,6 +207,11 @@ contract Test { LibSetters.setFees(eurY.collateral, xBurnFee, yBurnFee, false); LibSetters.togglePause(eurY.collateral, ActionType.Burn); + // Set no hard limits on stablecoin minting per collateral + LibSetters.setStablecoinCap(eurA.collateral, type(uint256).max); + LibSetters.setStablecoinCap(eurB.collateral, type(uint256).max); + LibSetters.setStablecoinCap(eurY.collateral, type(uint256).max); + // Redeem LibSetters.togglePause(eurA.collateral, ActionType.Redeem); } diff --git a/contracts/transmuter/facets/Getters.sol b/contracts/transmuter/facets/Getters.sol index cdacb445..479aa2b0 100644 --- a/contracts/transmuter/facets/Getters.sol +++ b/contracts/transmuter/facets/Getters.sol @@ -189,4 +189,9 @@ contract Getters is IGetters { function getCollateralWhitelistData(address collateral) external view returns (bytes memory) { return s.transmuterStorage().collaterals[collateral].whitelistData; } + + /// @inheritdoc IGetters + function getStablecoinCap(address collateral) external view returns (uint256) { + return s.transmuterStorage().collaterals[collateral].stablecoinCap; + } } diff --git a/contracts/transmuter/facets/SettersGuardian.sol b/contracts/transmuter/facets/SettersGuardian.sol index 7075108b..b1dad476 100644 --- a/contracts/transmuter/facets/SettersGuardian.sol +++ b/contracts/transmuter/facets/SettersGuardian.sol @@ -31,4 +31,9 @@ contract SettersGuardian is AccessControlModifiers, ISettersGuardian { function toggleWhitelist(WhitelistType whitelistType, address who) external onlyGuardian { LibSetters.toggleWhitelist(whitelistType, who); } + + /// @inheritdoc ISettersGuardian + function setStablecoinCap(address collateral, uint256 stablecoinCap) external onlyGuardian { + LibSetters.setStablecoinCap(collateral, stablecoinCap); + } } diff --git a/contracts/transmuter/facets/Swapper.sol b/contracts/transmuter/facets/Swapper.sol index a66a0c1f..d5709a3c 100644 --- a/contracts/transmuter/facets/Swapper.sol +++ b/contracts/transmuter/facets/Swapper.sol @@ -151,9 +151,12 @@ contract Swapper is ISwapper, AccessControlModifiers { /// @inheritdoc ISwapper function quoteIn(uint256 amountIn, address tokenIn, address tokenOut) external view returns (uint256 amountOut) { + TransmuterStorage storage ts = s.transmuterStorage(); (bool mint, Collateral storage collatInfo) = _getMintBurn(tokenIn, tokenOut, 0); - if (mint) return _quoteMintExactInput(collatInfo, amountIn); - else { + if (mint) { + amountOut = _quoteMintExactInput(collatInfo, amountIn); + _checkHardCaps(collatInfo, amountOut, ts.normalizer); + } else { amountOut = _quoteBurnExactInput(tokenOut, collatInfo, amountIn); _checkAmounts(tokenOut, collatInfo, amountOut); } @@ -161,9 +164,12 @@ contract Swapper is ISwapper, AccessControlModifiers { /// @inheritdoc ISwapper function quoteOut(uint256 amountOut, address tokenIn, address tokenOut) external view returns (uint256 amountIn) { + TransmuterStorage storage ts = s.transmuterStorage(); (bool mint, Collateral storage collatInfo) = _getMintBurn(tokenIn, tokenOut, 0); - if (mint) return _quoteMintExactOutput(collatInfo, amountOut); - else { + if (mint) { + _checkHardCaps(collatInfo, amountOut, ts.normalizer); + return _quoteMintExactOutput(collatInfo, amountOut); + } else { _checkAmounts(tokenOut, collatInfo, amountOut); return _quoteBurnExactOutput(tokenOut, collatInfo, amountOut); } @@ -187,6 +193,7 @@ contract Swapper is ISwapper, AccessControlModifiers { if (amountIn > 0 && amountOut > 0) { TransmuterStorage storage ts = s.transmuterStorage(); if (mint) { + _checkHardCaps(collatInfo, amountOut, ts.normalizer); uint128 changeAmount = (amountOut.mulDiv(BASE_27, ts.normalizer, Math.Rounding.Up)).toUint128(); // The amount of stablecoins issued from a collateral are not stored as absolute variables, but // as variables normalized by a `normalizer` @@ -354,9 +361,11 @@ contract Swapper is ISwapper, AccessControlModifiers { } { // In the mint case, when `!v.isExact`: = `b_{i+1} * (1+(g_i(0)+f_{i+1})/2)` - uint256 amountToNextBreakPointNormalizer = v.isExact ? v.amountToNextBreakPoint : v.isMint - ? _invertFeeMint(v.amountToNextBreakPoint, int64(v.upperFees + currentFees) / 2) - : _applyFeeBurn(v.amountToNextBreakPoint, int64(v.upperFees + currentFees) / 2); + uint256 amountToNextBreakPointNormalizer = v.isExact + ? v.amountToNextBreakPoint + : v.isMint + ? _invertFeeMint(v.amountToNextBreakPoint, int64(v.upperFees + currentFees) / 2) + : _applyFeeBurn(v.amountToNextBreakPoint, int64(v.upperFees + currentFees) / 2); if (amountToNextBreakPointNormalizer >= amountStable) { int64 midFee; @@ -426,9 +435,11 @@ contract Swapper is ISwapper, AccessControlModifiers { return amount + _computeFee(quoteType, amountStable, midFee); } else { amountStable -= amountToNextBreakPointNormalizer; - amount += !v.isExact ? v.amountToNextBreakPoint : v.isMint - ? _invertFeeMint(v.amountToNextBreakPoint, int64(v.upperFees + currentFees) / 2) - : _applyFeeBurn(v.amountToNextBreakPoint, int64(v.upperFees + currentFees) / 2); + amount += !v.isExact + ? v.amountToNextBreakPoint + : v.isMint + ? _invertFeeMint(v.amountToNextBreakPoint, int64(v.upperFees + currentFees) / 2) + : _applyFeeBurn(v.amountToNextBreakPoint, int64(v.upperFees + currentFees) / 2); currentExposure = v.upperExposure * BASE_9; ++i; // Update for the rest of the swaps the stablecoins issued from the asset @@ -452,6 +463,12 @@ contract Swapper is ISwapper, AccessControlModifiers { ) revert InvalidSwap(); } + /// @notice Checks whether there is enough space left to mint from this collateral + function _checkHardCaps(Collateral storage collatInfo, uint256 amount, uint256 normalizer) internal view { + if (amount + (collatInfo.normalizedStables * normalizer) / BASE_27 > collatInfo.stablecoinCap) + revert InvalidSwap(); + } + /// @notice Checks whether a swap from `tokenIn` to `tokenOut` is a mint or a burn, whether the /// collateral provided is paused or not and in case of whether the swap is not occuring too late /// @dev The function reverts if the `tokenIn` and `tokenOut` given do not correspond to the stablecoin @@ -521,11 +538,13 @@ contract Swapper is ISwapper, AccessControlModifiers { /// @notice Applies or inverts `fees` to an `amount` based on the type of operation function _computeFee(QuoteType quoteType, uint256 amount, int64 fees) internal pure returns (uint256) { return - quoteType == QuoteType.MintExactInput ? _applyFeeMint(amount, fees) : quoteType == QuoteType.MintExactOutput - ? _invertFeeMint(amount, fees) - : quoteType == QuoteType.BurnExactInput - ? _applyFeeBurn(amount, fees) - : _invertFeeBurn(amount, fees); + quoteType == QuoteType.MintExactInput + ? _applyFeeMint(amount, fees) + : quoteType == QuoteType.MintExactOutput + ? _invertFeeMint(amount, fees) + : quoteType == QuoteType.BurnExactInput + ? _applyFeeBurn(amount, fees) + : _invertFeeBurn(amount, fees); } /// @notice Checks whether an operation is a mint operation or not diff --git a/contracts/transmuter/libraries/LibSetters.sol b/contracts/transmuter/libraries/LibSetters.sol index 7aad55d8..3934ace4 100644 --- a/contracts/transmuter/libraries/LibSetters.sol +++ b/contracts/transmuter/libraries/LibSetters.sol @@ -30,6 +30,7 @@ library LibSetters { event PauseToggled(address indexed collateral, uint256 pausedType, bool isPaused); event RedemptionCurveParamsSet(uint64[] xFee, int64[] yFee); event ReservesAdjusted(address indexed collateral, uint256 amount, bool increase); + event StablecoinCapSet(address indexed collateral, uint256 stablecoinCap); event TrustedToggled(address indexed sender, bool isTrusted, TrustedType trustedType); event WhitelistStatusToggled(WhitelistType whitelistType, address indexed who, uint256 whitelistStatus); @@ -209,6 +210,14 @@ library LibSetters { emit WhitelistStatusToggled(whitelistType, who, whitelistStatus); } + /// @notice Sets the stablecoin cap that can be issued from a collateral + function setStablecoinCap(address collateral, uint256 stablecoinCap) internal { + Collateral storage collatInfo = s.transmuterStorage().collaterals[collateral]; + if (collatInfo.decimals == 0) revert NotCollateral(); + collatInfo.stablecoinCap = stablecoinCap; + emit StablecoinCapSet(collateral, stablecoinCap); + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ diff --git a/foundry.toml b/foundry.toml index d4075eaa..18a7be4d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,7 +10,7 @@ via_ir = true sizes = true optimizer = true optimizer_runs=1000 -solc_version = '0.8.22' +solc_version = '0.8.23' ffi = true fs_permissions = [ { access = "read-write", path = "./scripts/selectors.json"}, diff --git a/package.json b/package.json index 690660c2..6831fb2e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:invariant": "forge test -vvv --gas-report --match-path \"test/invariants/**/*.sol\"", "test:fuzz": "forge test -vvv --gas-report --match-path \"test/fuzz/**/*.sol\"", "slither": "chmod +x ./slither.sh && ./slither.sh", - "test": "forge test -vvvv", + "test": "forge test -vvv", "lcov:clean": "lcov --remove lcov.info -o lcov.info 'test/**' 'scripts/**' 'contracts/transmuter/configs/**' 'contracts/utils/**'", "lcov:generate-html": "genhtml lcov.info --output=coverage", "size": "forge build --skip test --sizes", diff --git a/scripts/selectors_add.json b/scripts/selectors_add.json index ffec3deb..c4e0abfc 100644 --- a/scripts/selectors_add.json +++ b/scripts/selectors_add.json @@ -2,5 +2,8 @@ "SettersGovernor": [ "0x1cb44dfc00000000000000000000000000000000000000000000000000000000" ], + "SettersGuardian": [ + "0x603b432700000000000000000000000000000000000000000000000000000000" + ], "useless": "" -} \ No newline at end of file +} diff --git a/scripts/test/UpdateTransmuterFacets.s.sol b/scripts/test/UpdateTransmuterFacets.s.sol index 8ae6e5a2..cf1cdb2e 100644 --- a/scripts/test/UpdateTransmuterFacets.s.sol +++ b/scripts/test/UpdateTransmuterFacets.s.sol @@ -84,6 +84,10 @@ contract UpdateTransmuterFacetsTest is Helpers, Test { addFacetNames.push("SettersGovernor"); addFacetAddressList.push(settersGovernor); + addFacetNames.push("SettersGuardian"); + address settersGuardian = address(new SettersGuardian()); + addFacetAddressList.push(settersGuardian); + string memory jsonReplace = vm.readFile(JSON_SELECTOR_PATH_REPLACE); { // Build appropriate payload @@ -310,6 +314,11 @@ contract UpdateTransmuterFacetsTest is Helpers, Test { transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, WHALE_AGEUR); + // Set no hard limits on stablecoin minting per collateral + transmuter.setStablecoinCap(EUROC, type(uint256).max); + transmuter.setStablecoinCap(BC3M, type(uint256).max); + transmuter.setStablecoinCap(BERNX, type(uint256).max); + vm.stopPrank(); } @@ -341,7 +350,7 @@ contract UpdateTransmuterFacetsTest is Helpers, Test { assertEq(collatInfoEUROC.isBurnLive, 1); assertEq(collatInfoEUROC.decimals, 6); assertEq(collatInfoEUROC.onlyWhitelisted, 0); - assertApproxEqRel(collatInfoEUROC.normalizedStables, 9580108 * BASE_18, 100 * BPS); + assertApproxEqRel(collatInfoEUROC.normalizedStables, 10593543 * BASE_18, 100 * BPS); assertEq(collatInfoEUROC.oracleConfig, oracleConfigEUROC); assertEq(collatInfoEUROC.whitelistData.length, 0); assertEq(collatInfoEUROC.managerData.subCollaterals.length, 0); @@ -537,7 +546,7 @@ contract UpdateTransmuterFacetsTest is Helpers, Test { function testUnit_Upgrade_GetCollateralRatio() external { (uint64 collatRatio, uint256 stablecoinIssued) = transmuter.getCollateralRatio(); assertApproxEqRel(collatRatio, 1065 * 10 ** 6, BPS * 100); - assertApproxEqRel(stablecoinIssued, 15816758 * BASE_18, 100 * BPS); + assertApproxEqRel(stablecoinIssued, 16816758 * BASE_18, 100 * BPS); } function testUnit_Upgrade_isTrusted() external { @@ -580,8 +589,8 @@ contract UpdateTransmuterFacetsTest is Helpers, Test { function testUnit_Upgrade_getOracleValues_Success() external { _checkOracleValues(address(EUROC), BASE_18, USER_PROTECTION_EUROC, FIREWALL_BURN_RATIO_EUROC); - _checkOracleValues(address(BC3M), (11974 * BASE_18) / 100, USER_PROTECTION_BC3M, FIREWALL_BURN_RATIO_BC3M); - _checkOracleValues(address(BERNX), (52274 * BASE_18) / 10000, USER_PROTECTION_ERNX, FIREWALL_BURN_RATIO_ERNX); + _checkOracleValues(address(BC3M), (11951 * BASE_18) / 100, USER_PROTECTION_BC3M, FIREWALL_BURN_RATIO_BC3M); + _checkOracleValues(address(BERNX), (52164 * BASE_18) / 10000, USER_PROTECTION_ERNX, FIREWALL_BURN_RATIO_ERNX); } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol b/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol index 7497eb7f..5dc640da 100644 --- a/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol +++ b/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol @@ -83,6 +83,10 @@ contract UpdateTransmuterFacetsUSDATest is Helpers, Test { addFacetNames.push("SettersGovernor"); addFacetAddressList.push(settersGovernor); + addFacetNames.push("SettersGuardian"); + address settersGuardian = address(new SettersGuardian()); + addFacetAddressList.push(settersGuardian); + string memory jsonReplace = vm.readFile(JSON_SELECTOR_PATH_REPLACE); { // Build appropriate payload @@ -308,6 +312,11 @@ contract UpdateTransmuterFacetsUSDATest is Helpers, Test { transmuter.toggleTrusted(NEW_DEPLOYER, Storage.TrustedType.Seller); transmuter.toggleTrusted(NEW_KEEPER, Storage.TrustedType.Seller); + // Set no hard limits on stablecoin minting per collateral + transmuter.setStablecoinCap(USDC, type(uint256).max); + transmuter.setStablecoinCap(BIB01, type(uint256).max); + transmuter.setStablecoinCap(STEAK_USDC, type(uint256).max); + vm.stopPrank(); } diff --git a/test/fuzz/MintTest.t.sol b/test/fuzz/MintTest.t.sol index 23dc67ad..13f28255 100644 --- a/test/fuzz/MintTest.t.sol +++ b/test/fuzz/MintTest.t.sol @@ -26,6 +26,7 @@ contract MintTest is Fixture, FunctionUtils { address[] internal _collaterals; AggregatorV3Interface[] internal _oracles; uint256[] internal _maxTokenAmount; + uint256 internal _maxAgTokenAmount; function setUp() public override { super.setUp(); @@ -62,6 +63,7 @@ contract MintTest is Fixture, FunctionUtils { _maxTokenAmount.push(_maxAmountWithoutDecimals * 10 ** IERC20Metadata(_collaterals[0]).decimals()); _maxTokenAmount.push(_maxAmountWithoutDecimals * 10 ** IERC20Metadata(_collaterals[1]).decimals()); _maxTokenAmount.push(_maxAmountWithoutDecimals * 10 ** IERC20Metadata(_collaterals[2]).decimals()); + _maxAgTokenAmount = _maxAmountWithoutDecimals * 10 ** 18; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -116,6 +118,55 @@ contract MintTest is Fixture, FunctionUtils { assertEq(computedTotalStable, totalStablecoins); } + function testFuzz_MintCapReached( + uint256[3] memory initialAmounts, + uint256[2] memory transferProportions, + uint256[3] memory amounts, + uint256[3] memory latestOracleValue, + uint256[3] memory stablecoinCaps + ) public { + // let's first load the reserves of the protocol + (uint256 mintedStables, uint256[] memory collateralMintedStables) = _loadReserves( + charlie, + sweeper, + initialAmounts, + transferProportions[0] + ); + + uint256 computedTotalStable; + for (uint256 i; i < collateralMintedStables.length; i++) { + computedTotalStable += collateralMintedStables[i]; + (uint256 stablecoinsFromCollateral, ) = transmuter.getIssuedByCollateral(address(_collaterals[i])); + assertEq(collateralMintedStables[i], stablecoinsFromCollateral); + } + + assertEq(computedTotalStable, agToken.totalSupply()); + assertEq(mintedStables, agToken.totalSupply()); + (, uint256 totalStablecoins) = transmuter.getIssuedByCollateral(address(_collaterals[0])); + assertEq(computedTotalStable, totalStablecoins); + + _updateOracles(latestOracleValue); + _setStablecoinCaps(stablecoinCaps); + // let's first load the reserves of the protocol + (uint256 mintedStables2, uint256[] memory collateralMintedStables2) = _loadReservesWithCap( + charlie, + sweeper, + amounts, + collateralMintedStables + ); + for (uint256 i; i < collateralMintedStables2.length; i++) { + computedTotalStable += collateralMintedStables2[i]; + (uint256 stablecoinsFromCollateral, ) = transmuter.getIssuedByCollateral(address(_collaterals[i])); + assertEq(collateralMintedStables[i] + collateralMintedStables2[i], stablecoinsFromCollateral); + } + + assertEq(computedTotalStable, agToken.totalSupply()); + assertEq(mintedStables + mintedStables2, agToken.totalSupply()); + + (, totalStablecoins) = transmuter.getIssuedByCollateral(address(_collaterals[0])); + assertEq(computedTotalStable, totalStablecoins); + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// MINT //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -805,6 +856,52 @@ contract MintTest is Fixture, FunctionUtils { vm.stopPrank(); } + function _loadReservesWithCap( + address owner, + address receiver, + uint256[3] memory initialAmounts, + uint256[] memory collateralPreviouslyMintedStables + ) internal returns (uint256 mintedStables, uint256[] memory collateralMintedStables) { + collateralMintedStables = new uint256[](_collaterals.length); + + vm.startPrank(owner); + for (uint256 i; i < _collaterals.length; i++) { + initialAmounts[i] = bound(initialAmounts[i], 1, _maxAgTokenAmount); + uint256 capStablecoin = transmuter.getStablecoinCap(_collaterals[i]); + // (uint256 stablecoinsFromCollateral, ) = transmuter.getIssuedByCollateral(_collaterals[i]); + // There may be some rounding issues here as collateralPreviouslyMintedStables is not computed exactly the same as capStablecoin + if ( + capStablecoin < type(uint256).max && + capStablecoin != 0 && + collateralPreviouslyMintedStables[i] + initialAmounts[i] < capStablecoin + 1 && + collateralPreviouslyMintedStables[i] + initialAmounts[i] > capStablecoin - 1 + ) { + // do nothing as there may be rounding errors + } else if (collateralPreviouslyMintedStables[i] + initialAmounts[i] > capStablecoin) { + vm.expectRevert(Errors.InvalidSwap.selector); + uint256 collateralNeeded = transmuter.quoteOut(initialAmounts[i], _collaterals[i], address(agToken)); + } else { + uint256 collateralNeeded = transmuter.quoteOut(initialAmounts[i], _collaterals[i], address(agToken)); + deal(_collaterals[i], owner, collateralNeeded); + IERC20(_collaterals[i]).approve(address(transmuter), type(uint256).max); + + collateralNeeded = transmuter.swapExactOutput( + initialAmounts[i], + type(uint256).max, + _collaterals[i], + address(agToken), + owner, + block.timestamp * 2 + ); + if (collateralNeeded > 0) { + collateralMintedStables[i] = initialAmounts[i]; + mintedStables += initialAmounts[i]; + } + } + } + vm.stopPrank(); + } + function _getExposures( uint256 mintedStables, uint256[] memory collateralMintedStables @@ -842,6 +939,14 @@ contract MintTest is Fixture, FunctionUtils { } } + function _setStablecoinCaps(uint256[3] memory stablecoinCaps) internal { + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + transmuter.setStablecoinCap(_collaterals[i], stablecoinCaps[i]); + } + vm.stopPrank(); + } + function _randomMintFees( address collateral, uint64[10] memory xFeeMintUnbounded, diff --git a/test/fuzz/SavingsVestTest.t.sol b/test/fuzz/SavingsVestTest.t.sol index cd615506..5dd13602 100644 --- a/test/fuzz/SavingsVestTest.t.sol +++ b/test/fuzz/SavingsVestTest.t.sol @@ -397,7 +397,8 @@ contract SavingsVestTest is Fixture, FunctionUtils { uint256 elapseTimestamps ) public { protocolSafetyFee = uint64(bound(protocolSafetyFee, 0, BASE_9)); - elapseTimestamps = bound(elapseTimestamps, 0, _maxElapseTime); + vm.warp(block.timestamp + 10); + vm.roll(1); bytes32 what = "PF"; vm.prank(governor); diff --git a/test/units/Layout.t.sol b/test/units/Layout.t.sol index 1a002cde..262e06dd 100644 --- a/test/units/Layout.t.sol +++ b/test/units/Layout.t.sol @@ -63,6 +63,7 @@ contract Test_Layout is Fixture { uint216 normalizedStables, bytes memory oracleConfig, bytes memory whitelistData, + , ) = layout.collaterals(collateralList[0]); diff --git a/test/units/Setters.t.sol b/test/units/Setters.t.sol index 2b22eb94..5ab7c6bf 100644 --- a/test/units/Setters.t.sol +++ b/test/units/Setters.t.sol @@ -1352,3 +1352,49 @@ contract Test_Setters_DiamondEtherscan is Fixture { assertEq(transmuter.implementation(), address(alice)); } } + +contract Test_Setters_SetStalecoinCap is Fixture { + event StablecoinCapSet(address indexed collateral, uint256 stablecoinCap); + + function test_RevertWhen_NotGuardian() public { + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + transmuter.setStablecoinCap(address(eurA), 1 ether); + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + transmuter.setStablecoinCap(address(eurB), 1 ether); + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + transmuter.setStablecoinCap(address(eurY), 1 ether); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + hoax(alice); + transmuter.setStablecoinCap(address(eurA), 1 ether); + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + hoax(alice); + transmuter.setStablecoinCap(address(eurB), 1 ether); + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + hoax(alice); + transmuter.setStablecoinCap(address(eurY), 1 ether); + } + + function test_RevertWhen_NotCollateral() public { + vm.expectRevert(Errors.NotCollateral.selector); + hoax(guardian); + transmuter.setStablecoinCap(address(agToken), 1 ether); + + vm.expectRevert(Errors.NotCollateral.selector); + hoax(guardian); + transmuter.setStablecoinCap(address(this), 1 ether); + } + + function test_SetStablecoinCap_Success() public { + hoax(guardian); + transmuter.setStablecoinCap(address(eurA), 1 ether); + hoax(governor); + transmuter.setStablecoinCap(address(eurB), 1 ether); + hoax(guardian); + transmuter.setStablecoinCap(address(eurY), 1 ether); + + assertEq(transmuter.getStablecoinCap(address(eurA)), 1 ether); + assertEq(transmuter.getStablecoinCap(address(eurB)), 1 ether); + assertEq(transmuter.getStablecoinCap(address(eurY)), 1 ether); + } +} diff --git a/test/units/StablecoinCap.t.sol b/test/units/StablecoinCap.t.sol new file mode 100644 index 00000000..f00de8c5 --- /dev/null +++ b/test/units/StablecoinCap.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.19; + +import { IERC20 } from "oz/interfaces/IERC20.sol"; + +import { IAccessControlManager } from "interfaces/IAccessControlManager.sol"; +import { IAgToken } from "interfaces/IAgToken.sol"; +import { AggregatorV3Interface } from "interfaces/external/chainlink/AggregatorV3Interface.sol"; + +import { MockAccessControlManager } from "mock/MockAccessControlManager.sol"; +import { MockChainlinkOracle } from "mock/MockChainlinkOracle.sol"; +import { MockTokenPermit } from "mock/MockTokenPermit.sol"; + +import { Test } from "contracts/transmuter/configs/Test.sol"; +import { LibGetters } from "contracts/transmuter/libraries/LibGetters.sol"; +import "contracts/transmuter/Storage.sol"; +import "contracts/utils/Constants.sol"; +import "contracts/utils/Errors.sol" as Errors; + +import { Fixture } from "../Fixture.sol"; + +contract StablecoinCapTest is Fixture { + function test_GetStablecoinCap_Init_Success() public { + assertEq(transmuter.getStablecoinCap(address(eurA)), type(uint256).max); + assertEq(transmuter.getStablecoinCap(address(eurB)), type(uint256).max); + assertEq(transmuter.getStablecoinCap(address(eurY)), type(uint256).max); + } + + function test_RevertWhen_SetStablecoinCap_TooLargeMint() public { + uint256 amount = 2 ether; + uint256 stablecoinCap = 1 ether; + address collateral = address(eurA); + + vm.prank(governor); + transmuter.setStablecoinCap(collateral, stablecoinCap); + + deal(collateral, bob, amount); + startHoax(bob); + IERC20(collateral).approve(address(transmuter), amount); + vm.expectRevert(Errors.InvalidSwap.selector); + startHoax(bob); + transmuter.swapExactOutput(amount, type(uint256).max, collateral, address(agToken), bob, block.timestamp * 2); + } + + function test_RevertWhen_SetStablecoinCap_SlightlyLargeMint() public { + uint256 amount = 1.0000000000001 ether; + uint256 stablecoinCap = 1 ether; + address collateral = address(eurA); + + vm.prank(governor); + transmuter.setStablecoinCap(collateral, stablecoinCap); + + deal(collateral, bob, amount); + startHoax(bob); + IERC20(collateral).approve(address(transmuter), amount); + vm.expectRevert(Errors.InvalidSwap.selector); + startHoax(bob); + transmuter.swapExactOutput(amount, type(uint256).max, collateral, address(agToken), bob, block.timestamp * 2); + } + + function test_SetStablecoinCap_Success() public { + uint256 amount = 0.99 ether; + uint256 stablecoinCap = 1 ether; + address collateral = address(eurA); + + vm.prank(governor); + transmuter.setStablecoinCap(collateral, stablecoinCap); + + deal(collateral, bob, amount); + startHoax(bob); + IERC20(collateral).approve(address(transmuter), amount); + startHoax(bob); + transmuter.swapExactOutput(amount, type(uint256).max, collateral, address(agToken), bob, block.timestamp * 2); + } +}