diff --git a/src/modules/logicModule/LM_ManualExternalPriceSetter_v1.sol b/src/modules/logicModule/LM_ManualExternalPriceSetter_v1.sol index a40d76144..86f463c31 100644 --- a/src/modules/logicModule/LM_ManualExternalPriceSetter_v1.sol +++ b/src/modules/logicModule/LM_ManualExternalPriceSetter_v1.sol @@ -136,20 +136,17 @@ contract LM_ManualExternalPriceSetter_v1 is _setRedemptionPrice(redemptionPrice_); } - /// @notice Gets current price for token issuance (buying tokens). - /// @return price_ Current price in 18 decimals (collateral tokens per 1 - /// issuance token). - /// @dev Example: If price is 2 USDC/ISS, returns 2e18 (2 USDC needed for - /// 1 ISS). + /// @inheritdoc ILM_ManualExternalPriceSetter_v1 + function getCollateralTokenDecimals() external view returns (uint8) { + return _collateralTokenDecimals; + } + + /// @inheritdoc ILM_ManualExternalPriceSetter_v1 function getPriceForIssuance() external view returns (uint) { return _issuancePrice; } - /// @notice Gets current price for token redemption (selling tokens). - /// @return price_ Current price in 18 decimals (collateral tokens per 1 - /// issuance token). - /// @dev Example: If price is 1.9 USDC/ISS, returns 1.9e18 (1.9 USDC - /// received for 1 ISS). + /// @inheritdoc ILM_ManualExternalPriceSetter_v1 function getPriceForRedemption() external view returns (uint) { return _redemptionPrice; } diff --git a/src/modules/logicModule/interfaces/ILM_ManualExternalPriceSetter_v1.sol b/src/modules/logicModule/interfaces/ILM_ManualExternalPriceSetter_v1.sol index 2a7fa0493..4a5dc74bf 100644 --- a/src/modules/logicModule/interfaces/ILM_ManualExternalPriceSetter_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_ManualExternalPriceSetter_v1.sol @@ -34,33 +34,59 @@ interface ILM_ManualExternalPriceSetter_v1 is IOraclePrice_v1 { error Module__LM_ExternalPriceSetter__InvalidPrice(); // ------------------------------------------------------------------------- - // External Functions + /// @notice Gets current price for token issuance (buying tokens). + /// @return price_ Current price denominated in the collateral token decimals. + /// @dev The price is denominated in the collateral token decimals. For + /// example, if the collateral token has 6 decimals and the issuance + /// price is 1.5, returns 1500000. + function getPriceForIssuance() external view returns (uint); + + /// @notice Gets current price for token redemption (selling tokens). + /// @return price_ Current price denominated in the collateral token decimals. + /// @dev The price is denominated in the collateral token decimals. For + /// example, if the collateral token has 6 decimals and the redemption + /// price is 0.5, the price_ parameter should be 500000. + function getPriceForRedemption() external view returns (uint); + /// @notice Sets the issuance price. /// @dev The price_ parameter should be provided with the same number /// of decimals as the collateral token. For example, if the - /// collateral token has 6 decimals and the price is 1.5, input - /// should be 1500000. - /// @param price_ The price to set. + /// collateral token has 6 decimals and the issuance price is 1.5, + /// the price_ parameter should be 1500000. + /// @param price_ The issuance price to set, denominated in the collateral + /// token decimals. function setIssuancePrice(uint price_) external; /// @notice Sets the redemption price. /// @dev The price_ parameter should be provided with the same number of - /// decimals as the issuance token. For example, if the issuance - /// token has 18 decimals and the price is 1.5, input should be - /// 1500000000000000000. - /// @param price_ The price to set. + /// of decimals as the collateral token. For example, if the + /// collateral token has 6 decimals and the redemption price is 0.5, + /// the price_ parameter should be 500000. + /// @param price_ The redemption price to set, denominated in the collateral + /// token decimals. function setRedemptionPrice(uint price_) external; - /// @notice Sets both issuance and redemption prices atomically. - /// @dev Both prices must be non-zero. The issuancePrice_ should be in - /// collateral token decimals and redemptionPrice_ in issuance token - /// decimals. - /// @param issuancePrice_ The issuance price to set. - /// @param redemptionPrice_ The redemption price to set. + /// @notice Sets both issuance and redemption prices atomically, denominated + /// in the collateral token decimals. + /// @dev Both prices must be non-zero. Both the issuance and redemption + /// prices should be denominated in the collateral token decimals. + /// For example, if the collateral token has 6 decimals and the + /// issuance and redemption price are both 1.5, the issuancePrice_ + /// and redemptionPrice_ parameters should be 1500000. + /// @param issuancePrice_ The issuance price to set, denominated in the + /// collateral token decimals. + /// @param redemptionPrice_ The redemption price to set, denominated in + /// the collateral token decimals. function setIssuanceAndRedemptionPrice( uint issuancePrice_, uint redemptionPrice_ ) external; + + /// @notice Gets the decimals of the collateral token. + /// @dev Decimals in which the issuance and redemption prices + /// are denominated. + /// @return decimals_ The decimals of the collateral token. + function getCollateralTokenDecimals() external view returns (uint8); } diff --git a/test/external/ERC20Issuance_blacklist_v1.t.sol b/test/external/ERC20Issuance_blacklist_v1.t.sol index 211d8489c..8b0dfaa8f 100644 --- a/test/external/ERC20Issuance_blacklist_v1.t.sol +++ b/test/external/ERC20Issuance_blacklist_v1.t.sol @@ -33,7 +33,7 @@ contract ERC20Issuance_Blacklist_v1_Test is Test { // State ERC20Issuance_Blacklist_v1_Exposed token; - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ + // ================================================================================ // Setup function setUp() public { // Setup token diff --git a/test/modules/fundingManager/oracle/FM_PC_ExternalPrice_Redeeming_v1.t.sol b/test/modules/fundingManager/oracle/FM_PC_ExternalPrice_Redeeming_v1.t.sol index 24f0dbfe6..ebaf5dc71 100644 --- a/test/modules/fundingManager/oracle/FM_PC_ExternalPrice_Redeeming_v1.t.sol +++ b/test/modules/fundingManager/oracle/FM_PC_ExternalPrice_Redeeming_v1.t.sol @@ -1032,6 +1032,7 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { vm.assume(buyer != address(0)); vm.assume(buyer != address(this)); vm.assume(buyer != admin); + vm.assume(buyer != address(fundingManager)); // Given - Grant whitelist role to the user // _authorizer.grantRole(roleId, buyer); diff --git a/test/modules/logicModule/LM_ManualExternalPriceSetter_v1.t.sol b/test/modules/logicModule/LM_ManualExternalPriceSetter_v1.t.sol index bcf0801df..e0ec00864 100644 --- a/test/modules/logicModule/LM_ManualExternalPriceSetter_v1.t.sol +++ b/test/modules/logicModule/LM_ManualExternalPriceSetter_v1.t.sol @@ -1,360 +1,427 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; - -import {Test, console} from "forge-std/Test.sol"; // @todo remove -import {LM_ManualExternalPriceSetter_v1} from - "src/modules/logicModule/LM_ManualExternalPriceSetter_v1.sol"; -import {ILM_ManualExternalPriceSetter_v1} from - "@lm/interfaces/ILM_ManualExternalPriceSetter_v1.sol"; -import {LM_ManualExternalPriceSetter_v1} from - "src/modules/logicModule/LM_ManualExternalPriceSetter_v1.sol"; -import {ERC20Decimals_Mock} from "test/utils/mocks/ERC20Decimals_Mock.sol"; -import {ERC20Mock} from "test/utils/mocks/ERC20Mock.sol"; -import {AuthorizerV1Mock} from "test/utils/mocks/modules/AuthorizerV1Mock.sol"; -import {Clones} from "@oz/proxy/Clones.sol"; -import {OZErrors} from "test/utils/errors/OZErrors.sol"; +pragma solidity ^0.8.0; + +// Internal import { ModuleTest, IModule_v1, IOrchestrator_v1 } from "test/modules/ModuleTest.sol"; +import {OZErrors} from "test/utils/errors/OZErrors.sol"; +import {IOraclePrice_v1} from "@lm/interfaces/IOraclePrice_v1.sol"; -/** - * @title LM_ManualExternalPriceSetter_v1_Test - * @notice Test contract for LM_ManualExternalPriceSetter_v1 - */ -contract LM_ManualExternalPriceSetter_v1_Test is ModuleTest { - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - // Storage - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - - LM_ManualExternalPriceSetter_v1 priceSetter; +// External +import {Clones} from "@oz/proxy/Clones.sol"; - address admin; - address priceSetter_; - address user; +// Tests and Mocks +import {Test} from "forge-std/Test.sol"; +import {LM_ManualExternalPriceSetter_v1_Exposed} from + "test/modules/logicModule/LM_ManualExternalPriceSetter_v1_Exposed.sol"; +import {ERC20Decimals_Mock} from "test/utils/mocks/ERC20Decimals_Mock.sol"; - ERC20Decimals_Mock inputToken; - ERC20Mock outputToken; +// System under testing +import { + LM_ManualExternalPriceSetter_v1, + ILM_ManualExternalPriceSetter_v1 +} from "@lm/LM_ManualExternalPriceSetter_v1.sol"; - bytes32 constant PRICE_SETTER_ROLE = "PRICE_SETTER_ROLE"; - uint8 constant INTERNAL_DECIMALS = 18; +/** + * @title LM_ManualExternalPriceSetter_v1_Test + * @dev Test contract for LM_ManualExternalPriceSetter_v1 + * @author Zealynx Security + */ +contract LM_ManualExternalPriceSetter_v1_Test is ModuleTest { + // ================================================================================ + // Constants + uint8 constant TOKEN_DECIMALS = 6; string constant TOKEN_NAME = "MOCK USDC"; string constant TOKEN_SYMBOL = "M-USDC"; - // Module Constants - uint constant MAJOR_VERSION = 1; - uint constant MINOR_VERSION = 0; - uint constant PATCH_VERSION = 0; - string constant URL = "https://github.com/organization/module"; // @todo update with module information - string constant TITLE = "Module"; + // ================================================================================ + // State + LM_ManualExternalPriceSetter_v1_Exposed manualExternalPriceSetter; + ERC20Decimals_Mock collateralToken; - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ + // ================================================================================ // Setup - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - function setUp() public { - admin = makeAddr("admin"); - priceSetter_ = makeAddr("priceSetter"); - user = makeAddr("user"); - - vm.startPrank(admin); - - // Create mock tokens with different decimals - inputToken = new ERC20Decimals_Mock(TOKEN_NAME, TOKEN_SYMBOL, 6); // Like USDC - outputToken = _token; // Like most ERC20s - - // Setup price setter - address impl = address(new LM_ManualExternalPriceSetter_v1()); - priceSetter = LM_ManualExternalPriceSetter_v1(Clones.clone(impl)); - - bytes memory configData = - abi.encode(address(inputToken), address(outputToken)); - - _setUpOrchestrator(priceSetter); - - priceSetter.init(_orchestrator, _METADATA, configData); - - // Grant price setter role - bytes32 roleId = - _authorizer.generateRoleId(address(priceSetter), PRICE_SETTER_ROLE); - _authorizer.grantRole(roleId, priceSetter_); - - vm.stopPrank(); + // Create mock token with 6 decimals like USDC + collateralToken = + new ERC20Decimals_Mock(TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS); + + // Setup manual external price setter + address impl = address(new LM_ManualExternalPriceSetter_v1_Exposed()); + manualExternalPriceSetter = + LM_ManualExternalPriceSetter_v1_Exposed(Clones.clone(impl)); + _setUpOrchestrator(manualExternalPriceSetter); + + // Init module + bytes memory configData = abi.encode(address(collateralToken)); + manualExternalPriceSetter.init(_orchestrator, _METADATA, configData); + + // Grant PRICE_SETTER_ROLE and PRICE_SETTER_ROLE_ADMIN to the test contract + manualExternalPriceSetter.grantModuleRole( + manualExternalPriceSetter.PRICE_SETTER_ROLE(), address(this) + ); + manualExternalPriceSetter.grantModuleRole( + manualExternalPriceSetter.PRICE_SETTER_ROLE_ADMIN(), address(this) + ); } - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - // Initialization Tests - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - - /* testInit() - Tests initialization of the oracle contract - - Tree structure: - └── Given a newly deployed oracle contract - └── When checking the orchestrator address - └── Then it should match the provided orchestrator - */ + // ================================================================================ + // Test Init + + // This function also tests all the gettersx function testInit() public override(ModuleTest) { assertEq( - address(priceSetter.orchestrator()), - address(_orchestrator), - "Orchestrator not set correctly" + manualExternalPriceSetter.getCollateralTokenDecimals(), + TOKEN_DECIMALS, + "Token decimals not set correctly" ); } - /* testReinitFails() - └── Given an initialized contract - └── When trying to initialize again - └── Then it should revert with InvalidInitialization - */ function testReinitFails() public override(ModuleTest) { vm.expectRevert(OZErrors.Initializable__InvalidInitialization); - priceSetter.init( - _orchestrator, - _METADATA, - abi.encode(address(inputToken), address(outputToken)) + manualExternalPriceSetter.init( + _orchestrator, _METADATA, abi.encode(address(collateralToken)) ); } - /* testSupportsInterface_GivenValidInterface() - └── Given the contract interface - └── When checking interface support - └── Then it should support ILM_ManualExternalPriceSetter_v1 - */ function testSupportsInterface_GivenValidInterface() public { assertTrue( - priceSetter.supportsInterface( + manualExternalPriceSetter.supportsInterface( type(ILM_ManualExternalPriceSetter_v1).interfaceId ) ); } - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - // Token Configuration Tests - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - - /* testTokenProperties_GivenTokensWithDifferentDecimals() - ├── Given the input token (USDC mock) - │ ├── When checking its decimals - │ │ └── Then it should have 6 decimals - │ ├── When checking its name - │ │ └── Then it should be "Mock Token" - │ ├── When checking its symbol - │ │ └── Then it should be "MOCK" - │ └── When checking its initial supply - │ └── Then it should be 0 - └── Given the output token (TOKEN mock) - ├── When checking its decimals - │ └── Then it should have 18 decimals - ├── When checking its name - │ └── Then it should be "Mock Token" - ├── When checking its symbol - │ └── Then it should be "MOCK" - └── When checking its initial supply - └── Then it should be 0 + // ================================================================================ + // Test External (public + external) + + /* Test: Function SetIssuancePrice() + ├── Given the caller has not PRICE_SETTER_ROLE + │ └── When the function setIssuancePrice() is called + │ └── Then the function should revert (Modifier in place test) + └── Given the caller has PRICE_SETTER_ROLE + └── When the function setIssuancePrice() is called + └── Then the price should be set correctly (redirects to internal func) */ - function testTokenProperties_GivenTokensWithDifferentDecimals() public { - // Test input token (USDC mock) - assertEq(inputToken.decimals(), 6, "Input token should have 6 decimals"); - assertEq( - inputToken.name(), - TOKEN_NAME, - "Input token should have correct name" + + function testSetIssuancePrice_worksGivenModifierInPlace( + address unauthorized_, + uint price_ + ) public { + // Setup + vm.assume(unauthorized_ != address(this)); + vm.assume(price_ > 0); + bytes32 roleId = _authorizer.generateRoleId( + address(manualExternalPriceSetter), + manualExternalPriceSetter.PRICE_SETTER_ROLE() ); - assertEq( - inputToken.symbol(), - TOKEN_SYMBOL, - "Input token should have correct symbol" + + // Test + vm.startPrank(unauthorized_); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, + roleId, + unauthorized_ + ) ); + manualExternalPriceSetter.setIssuancePrice(price_); + } + + function testSetIssuancePrice_worksGivenPriceIsSet( + uint initialPrice_, + uint updatePrice_ + ) public { + // Setup + vm.assume(initialPrice_ > 0 && updatePrice_ > 0); + vm.assume(initialPrice_ != updatePrice_); + // Set initial price + manualExternalPriceSetter.setIssuancePrice(initialPrice_); + // pre-condition assertEq( - inputToken.totalSupply(), - 0, - "Input token should have 0 initial supply" + manualExternalPriceSetter.getPriceForIssuance(), + initialPrice_, + "Initial issuance price not set correctly" ); - // Test output token (TOKEN mock) + // Test + manualExternalPriceSetter.setIssuancePrice(updatePrice_); + + // Assert assertEq( - outputToken.decimals(), 18, "Output token should have 18 decimals" + manualExternalPriceSetter.getPriceForIssuance(), + updatePrice_, + "Issuance price not set correctly" ); - assertEq( - outputToken.name(), - "Mock Token", - "Output token should have correct name" + } + + /* Test: Function: SetRedemptionPrice() + ├── Given the caller has not PRICE_SETTER_ROLE + │ └── When the function setRedemptionPrice() is called + │ └── Then the function should revert (Modifier in place test) + └── Given the caller has PRICE_SETTER_ROLE + └── When the function setRedemptionPrice() is called + └── Then the price should be set correctly (redirects to internal func) + */ + + function testSetRedemptionPrice_worksGivenModifierInPlace( + address unauthorized_, + uint price_ + ) public { + // Setup + vm.assume(unauthorized_ != address(this)); + vm.assume(price_ > 0); + bytes32 roleId = _authorizer.generateRoleId( + address(manualExternalPriceSetter), + manualExternalPriceSetter.PRICE_SETTER_ROLE() + ); + + // Test + vm.startPrank(unauthorized_); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, + roleId, + unauthorized_ + ) ); + manualExternalPriceSetter.setRedemptionPrice(price_); + } + + function testSetRedemptionPrice_worksGivenPriceIsSet( + uint initialPrice_, + uint updatePrice_ + ) public { + // Setup + vm.assume(initialPrice_ > 0 && updatePrice_ > 0); + vm.assume(initialPrice_ != updatePrice_); + // Set initial price + manualExternalPriceSetter.setRedemptionPrice(initialPrice_); + // pre-condition assertEq( - outputToken.symbol(), - "MOCK", - "Output token should have correct symbol" + manualExternalPriceSetter.getPriceForRedemption(), + initialPrice_, + "Initial redemption price not set correctly" ); + + // Test + manualExternalPriceSetter.setRedemptionPrice(updatePrice_); + + // Assert assertEq( - outputToken.totalSupply(), - 0, - "Output token should have 0 initial supply" + manualExternalPriceSetter.getPriceForRedemption(), + updatePrice_, + "Redemption price not set correctly" ); } - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - // Price Management Tests - // ═══════════════════════════════════════════════════════════════════════════════════════════════════════ - - /* testInitialSetup_GivenNoSetPrices() - └── Given no prices have been set - ├── When querying issuance price - │ └── Then it should revert with InvalidPrice - └── When querying redemption price - └── Then it should revert with InvalidPrice + /* Test: Function: SetIssuanceAndRedemptionPrice() + ├── Given the caller has not PRICE_SETTER_ROLE + │ └── When the function setIssuanceAndRedemptionPrice() is called + │ └── Then the function should revert (Modifier in place test) + └── Given the caller has PRICE_SETTER_ROLE + └── When the function setIssuanceAndRedemptionPrice() is called + └── Then the price should be set correctly (redirects to internal funcs) */ - function testInitialSetup_GivenNoSetPrices() public { - // Verify initial prices revert - vm.startPrank(priceSetter_); - vm.expectRevert( - abi.encodeWithSignature( - "Module__LM_ExternalPriceSetter__InvalidPrice()" - ) + + function testSetIssuanceAndRedemptionPrice_worksGivenModifierInPlace( + address unauthorized_, + uint issuancePrice_, + uint redemptionPrice_ + ) public { + // Setup + vm.assume(unauthorized_ != address(this)); + vm.assume(issuancePrice_ > 0 && redemptionPrice_ > 0); + bytes32 roleId = _authorizer.generateRoleId( + address(manualExternalPriceSetter), + manualExternalPriceSetter.PRICE_SETTER_ROLE() ); - priceSetter.setIssuancePrice(0); - vm.startPrank(priceSetter_); + // Test + vm.startPrank(unauthorized_); vm.expectRevert( - abi.encodeWithSignature( - "Module__LM_ExternalPriceSetter__InvalidPrice()" + abi.encodeWithSelector( + IModule_v1.Module__CallerNotAuthorized.selector, + roleId, + unauthorized_ ) ); - priceSetter.setIssuanceAndRedemptionPrice(0, 0); + manualExternalPriceSetter.setIssuanceAndRedemptionPrice( + issuancePrice_, redemptionPrice_ + ); } - /* testSetIssuancePrice_GivenUnauthorizedUser() - ├── Given a non-authorized user and random price - │ └── When setting issuance price - │ └── Then it should revert with NotPriceSetter - ├── Given an authorized price setter - │ ├── When setting a zero price - │ │ └── Then it should revert with InvalidPrice - │ └── When setting a valid random price - │ └── Then the price should be set and normalized correctly - */ - function testSetIssuancePrice_GivenUnauthorizedUser( - uint price, - address unauthorizedUser + function testSetIssuanceAndRedemptionPrice_worksGivenPricesAreSet( + uint initialIssuancePrice_, + uint initialRedemptionPrice_, + uint updateIssuancePrice_, + uint updateRedemptionPrice_ ) public { - // Assume valid unauthorized user - vm.assume(unauthorizedUser != address(0)); - vm.assume(unauthorizedUser != address(this)); - vm.assume(unauthorizedUser != priceSetter_); - - price = bound(price, 1, 1_000_000_000_000 * 1e6); - - // Test unauthorized access with random price - vm.startPrank(unauthorizedUser); - vm.expectRevert( - abi.encodeWithSignature( - "Module__CallerNotAuthorized(bytes32,address)", - _authorizer.generateRoleId( - address(priceSetter), PRICE_SETTER_ROLE - ), - unauthorizedUser - ) + // Setup + vm.assume( + initialIssuancePrice_ > 0 && initialRedemptionPrice_ > 0 + && updateIssuancePrice_ > 0 && updateRedemptionPrice_ > 0 + ); + vm.assume(initialIssuancePrice_ != updateIssuancePrice_); + vm.assume(initialRedemptionPrice_ != updateRedemptionPrice_); + // Set initial prices + manualExternalPriceSetter.setIssuanceAndRedemptionPrice( + initialIssuancePrice_, initialRedemptionPrice_ + ); + // pre-condition + assertEq( + manualExternalPriceSetter.getPriceForIssuance(), + initialIssuancePrice_, + "Initial issuance price not set correctly" + ); + assertEq( + manualExternalPriceSetter.getPriceForRedemption(), + initialRedemptionPrice_, + "Initial redemption price not set correctly" ); - priceSetter.setIssuancePrice(price); - vm.stopPrank(); - // Test zero price with authorized user - vm.startPrank(priceSetter_); - vm.expectRevert( - abi.encodeWithSignature( - "Module__LM_ExternalPriceSetter__InvalidPrice()" - ) + // Test + manualExternalPriceSetter.setIssuanceAndRedemptionPrice( + updateIssuancePrice_, updateRedemptionPrice_ ); - priceSetter.setIssuancePrice(0); - // Test valid price setting with random price - priceSetter.setIssuancePrice(price); - // Price should be normalized from 6 decimals to 18 decimals + // Assert assertEq( - priceSetter.getPriceForIssuance(), - price, + manualExternalPriceSetter.getPriceForIssuance(), + updateIssuancePrice_, "Issuance price not set correctly" ); - - vm.stopPrank(); + assertEq( + manualExternalPriceSetter.getPriceForRedemption(), + updateRedemptionPrice_, + "Redemption price not set correctly" + ); } - /* testSetRedemptionPrice_GivenUnauthorizedUser() - ├── Given an unauthorized user - │ └── When trying to set redemption price - │ └── Then it should revert with NotPriceSetter error - ├── Given an authorized price setter and zero price - │ └── When setting redemption price to zero - │ └── Then it should revert with InvalidPrice error - ├── Given an authorized price setter and valid price - │ └── When setting redemption price - │ └── Then the price should be set correctly with 18 decimals - └── Given both redemption and issuance prices are set - └── When modifying issuance price - ├── Then redemption price should remain unchanged - └── Then both prices should maintain their correct decimal precision - ├── Redemption: 18 decimals - └── Issuance: 6 decimals + /* Test: Function getPriceForIssuance() + └── Given a price is set + └── When the function getPriceForIssuance() is called + └── Then the function should return the correct price */ - function testSetRedemptionPrice_GivenUnauthorizedUser( - uint price, - address unauthorizedUser - ) public { - // Assume valid unauthorized user - vm.assume(unauthorizedUser != address(0)); - vm.assume(unauthorizedUser != address(this)); - vm.assume(unauthorizedUser != priceSetter_); - price = bound(price, 1e6, 1_000_000_000_000 * 1e6); + function testGetPriceForIssuance_worksGivenPriceIsSet(uint price_) public { + // Setup + vm.assume(price_ > 0); + manualExternalPriceSetter.setIssuancePrice(price_); - // Test unauthorized access - vm.startPrank(unauthorizedUser); - vm.expectRevert( - abi.encodeWithSignature( - "Module__CallerNotAuthorized(bytes32,address)", - _authorizer.generateRoleId( - address(priceSetter), PRICE_SETTER_ROLE - ), - unauthorizedUser - ) + // Test + assertEq( + manualExternalPriceSetter.getPriceForIssuance(), + price_, + "Issuance price not set correctly" ); - priceSetter.setRedemptionPrice(price); - vm.stopPrank(); + } - // Test zero price with authorized user - vm.startPrank(priceSetter_); - vm.expectRevert( - abi.encodeWithSignature( - "Module__LM_ExternalPriceSetter__InvalidPrice()" - ) - ); - priceSetter.setRedemptionPrice(0); + /* Test: Function getPriceForRedemption() + └── Given a price is set + └── When the function getPriceForRedemption() is called + └── Then the function should return the correct price + */ - // Test valid price setting - priceSetter.setRedemptionPrice(price); + function testGetPriceForRedemption_worksGivenPriceIsSet(uint price_) + public + { + // Setup + vm.assume(price_ > 0); + manualExternalPriceSetter.setRedemptionPrice(price_); + + // Test assertEq( - priceSetter.getPriceForRedemption(), - price, + manualExternalPriceSetter.getPriceForRedemption(), + price_, "Redemption price not set correctly" ); + } - // Test price independence by setting a different issuance price - uint issuancePrice = price; // Convert to 6 decimals for issuance - priceSetter.setIssuancePrice(issuancePrice); + // ================================================================================ + // Test Internal + + /* Test: Function _setIssuancePrice() + ├── Given the price is 0 + │ └── When the function _setIssuancePrice() is called + │ └── Then it should revert + └── Given the price is bigger than 0 + └── When the function _setIssuancePrice() is called + ├── Then it should set the issuance price correctly + └── And it should emit an event + */ - // Verify both prices maintain their values independently - assertEq( - priceSetter.getPriceForRedemption(), - price, - "Redemption price changed unexpectedly" + function testInternalSetIssuancePrice_revertGivenPriceIsZero() public { + // Test + vm.expectRevert( + abi.encodeWithSelector( + ILM_ManualExternalPriceSetter_v1 + .Module__LM_ExternalPriceSetter__InvalidPrice + .selector + ) ); + manualExternalPriceSetter.exposed_setIssuancePrice(0); + } + + function testInternalSetIssuancePrie_worksGivenPriceGreaterThanZero( + uint price_ + ) public { + // Setup + vm.assume(price_ > 0); + + // Test + vm.expectEmit(true, true, true, true); + emit IOraclePrice_v1.IssuancePriceSet(price_, address(this)); + manualExternalPriceSetter.exposed_setIssuancePrice(price_); + + // Assert assertEq( - priceSetter.getPriceForIssuance(), - issuancePrice, + manualExternalPriceSetter.getPriceForIssuance(), + price_, "Issuance price not set correctly" ); + } + /* Test: Function _setRedemptionPrice() + ├── Given the price is 0 + │ └── When the function _setRedemptionPrice() is called + │ └── Then it should revert + └── Given the price is bigger than 0 + └── When the function _setRedemptionPrice() is called + ├── Then it should set the redemption price correctly + └── And it should emit an event + */ + + function testInternalSetRedemptionPrice_revertGivenPriceIsZero() public { + // Test + vm.expectRevert( + abi.encodeWithSelector( + ILM_ManualExternalPriceSetter_v1 + .Module__LM_ExternalPriceSetter__InvalidPrice + .selector + ) + ); + manualExternalPriceSetter.exposed_setRedemptionPrice(0); + } + + function testInternalSetRedemptionPrice_worksGivenPriceGreaterThanZero( + uint price_ + ) public { + // Setup + vm.assume(price_ > 0); + + // Test + vm.expectEmit(true, true, true, true); + emit IOraclePrice_v1.RedemptionPriceSet(price_, address(this)); + manualExternalPriceSetter.exposed_setRedemptionPrice(price_); - vm.stopPrank(); + // Assert + assertEq( + manualExternalPriceSetter.getPriceForRedemption(), + price_, + "Redemption price not set correctly" + ); } } diff --git a/test/modules/logicModule/LM_ManualExternalPriceSetter_v1_Exposed.sol b/test/modules/logicModule/LM_ManualExternalPriceSetter_v1_Exposed.sol new file mode 100644 index 000000000..ea549cf37 --- /dev/null +++ b/test/modules/logicModule/LM_ManualExternalPriceSetter_v1_Exposed.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.23; + +import {LM_ManualExternalPriceSetter_v1} from + "@lm/LM_ManualExternalPriceSetter_v1.sol"; + +contract LM_ManualExternalPriceSetter_v1_Exposed is + LM_ManualExternalPriceSetter_v1 +{ + function exposed_setIssuancePrice(uint price_) public { + _setIssuancePrice(price_); + } + + function exposed_setRedemptionPrice(uint price_) public { + _setRedemptionPrice(price_); + } +}