### Install Forge-Std Source: https://github.com/mainstreet-labs/mainstreet-core/blob/main/lib/forge-std/README.md Use this command to install the Forge Standard Library into your Foundry project. ```bash forge install foundry-rs/forge-std ``` -------------------------------- ### StaticPriceOracle Deployment and Usage Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Example of deploying a `StaticPriceOracle` for USDC.e and using its methods to calculate value and amount. ```APIDOC ## StaticPriceOracle Deployment and Usage ### Description Example of deploying a `StaticPriceOracle` for USDC.e and using its methods to calculate value and amount. ### Deployment Example ```solidity // Deploy StaticPriceOracle for USDC.e at $1.00 (6-decimal token, 8-decimal price) // constructor(address _token, uint256 staticPrice, uint8 decimals) StaticPriceOracle oracle = new StaticPriceOracle( address(USDC_E), // 6-decimal token 1e8, // $1.00 in 8-decimal price format 8 // price decimals ); ``` ### Usage Examples #### Get USD value of USDC.e ```solidity // Get USD value of 500 USDC.e in 18-decimal msUSD units (floor rounding) uint256 usdValue = oracle.valueOf(500e6, Math.Rounding.Floor); // usdValue == 500e18 (500 USD in 18-decimal units) ``` #### Get USDC.e amount for msUSD ```solidity // Get USDC.e amount for 300 msUSD (during redemption) uint256 usdcAmt = oracle.amountOf(300e18, Math.Rounding.Floor); // usdcAmt == 300e6 (300 USDC.e in 6-decimal units) ``` #### Hypothetical value at a different price ```solidity // Hypothetical: value at a different price (e.g., $0.999 depeg scenario) uint256 depegPrice = 0.999e18; // 18-decimal internal representation uint256 depegValue = oracle.valueOfAtPrice(500e6, depegPrice, Math.Rounding.Floor); // depegValue < 500e18 (slight underpeg) ``` ``` -------------------------------- ### Example Usage of Hoax and Prank Cheat Codes Source: https://github.com/mainstreet-labs/mainstreet-core/blob/main/lib/forge-std/README.md Demonstrates the usage of `hoax` and `startHoax` cheat codes for setting the `msg.sender` and `msg.value` in tests. Ensure `forge-std/Test.sol` is imported. ```solidity // SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport "forge-std/Test.sol";\n\n// Inherit the stdCheats\ncontract StdCheatsTest is Test {\n Bar test;\n function setUp() public {\n test = new Bar();\n }\n\n function testHoax() public {\n // we call `hoax`, which gives the target address\n // eth and then calls `prank`\n hoax(address(1337));\n test.bar{value: 100}(address(1337));\n\n // overloaded to allow you to specify how much eth to\n // initialize the address with\n hoax(address(1337), 1);\n test.bar{value: 1}(address(1337));\n }\n\n function testStartHoax() public {\n // we call `startHoax`, which gives the target address\n // eth and then calls `startPrank`\n //\n // it is also overloaded so that you can specify an eth amount\n startHoax(address(1337));\n test.bar{value: 100}(address(1337));\n test.bar{value: 100}(address(1337));\n vm.stopPrank();\n test.bar(address(this));\n }\n}\n\ncontract Bar {\n function bar(address expectedSender) public payable {\n require(msg.sender == expectedSender, \"!prank\");\n }\n}\n ``` -------------------------------- ### IOracle Interface Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Defines the interface for oracle contracts, including methods for getting the value of an amount, the amount for a value, and hypothetical valuations at specific prices. ```APIDOC ## IOracle Interface ### Description Defines the interface for oracle contracts, including methods for getting the value of an amount, the amount for a value, and hypothetical valuations at specific prices. ### Methods - `valueOf(uint256 amount, Math.Rounding rounding) external view returns (uint256 value)` - `valueOf(uint256 amount, uint256 maxAge, Math.Rounding rounding) external view returns (uint256 value)` - `amountOf(uint256 value, Math.Rounding rounding) external view returns (uint256 amount)` - `amountOf(uint256 value, uint256 maxAge, Math.Rounding rounding) external view returns (uint256 amount)` - `latestPrice() external view returns (uint256 price)` - `amountOfAtPrice(uint256 value, uint256 price, Math.Rounding rounding) external view returns (uint256)` - `valueOfAtPrice(uint256 amount, uint256 price, Math.Rounding rounding) external view returns (uint256)` ``` -------------------------------- ### Get USDC.e amount for msUSD using StaticPriceOracle Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Calculates the amount of USDC.e corresponding to a given amount of msUSD, using the StaticPriceOracle and floor rounding. Useful during redemption processes. ```solidity // Get USDC.e amount for 300 msUSD (during redemption) uint256 usdcAmt = oracle.amountOf(300e18, Math.Rounding.Floor); // usdcAmt == 300e6 (300 USDC.e in 6-decimal units) ``` -------------------------------- ### Get USD value of USDC.e using StaticPriceOracle Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Calculates the USD value of a given amount of USDC.e using the StaticPriceOracle, with floor rounding. The result is in 18-decimal msUSD units. ```solidity // Get USD value of 500 USDC.e in 18-decimal msUSD units (floor rounding) uint256 usdValue = oracle.valueOf(500e6, Math.Rounding.Floor); // usdValue == 500e18 (500 USD in 18-decimal units) ``` -------------------------------- ### Initiate Withdrawal with Cooldown (ERC-4626) Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt When a cooldown duration is active, standard withdrawals are disabled. Users must use `cooldownAssets` or `cooldownShares` to move assets to a silo and start a timer. After the cooldown expires, `unstake` can be called to claim the assets. ```Solidity interface IStakedmsUSD { function cooldownAssets(uint256 assets, address owner) external returns (uint256 shares); function cooldownShares(uint256 shares, address owner) external returns (uint256 assets); function cooldowns(address user) external view returns (uint104 cooldownEnd, uint256 underlyingAmount); function cooldownDuration() external view returns (uint24); function unstake(address receiver) external; } address S_MSUSD = 0x; uint256 myShares = 500 ether; // smsUSD shares to redeem // Check current cooldown window uint24 duration = IStakedmsUSD(S_MSUSD).cooldownDuration(); // e.g., 7 days // Option A: initiate cooldown by share count uint256 assetsLocked = IStakedmsUSD(S_MSUSD).cooldownShares(myShares, msg.sender); // assetsLocked: msUSD moved to silo, cooldownEnd = now + cooldownDuration // Option B: initiate cooldown by asset amount // uint256 sharesUsed = IStakedmsUSD(S_MSUSD).cooldownAssets(1000 ether, msg.sender); // Check cooldown status (uint104 cooldownEnd, uint256 underlyingAmount) = IStakedmsUSD(S_MSUSD).cooldowns(msg.sender); // cooldownEnd: timestamp after which unstake() may be called // underlyingAmount: msUSD waiting in silo // After cooldown expires: // block.timestamp >= cooldownEnd required or CooldownNotFinished is thrown IStakedmsUSD(S_MSUSD).unstake(msg.sender); // Transfers underlyingAmount * coverageRatio / 1e18 msUSD from silo to receiver // Emits: Unstake(msg.sender, receiver, underlyingAmount, amountForRedeemer) ``` -------------------------------- ### Using console.sol for Hardhat Compatibility Source: https://github.com/mainstreet-labs/mainstreet-core/blob/main/lib/forge-std/README.md Employs `console.sol` for compatibility with Hardhat. Note that logs using `uint256` or `int256` may not be decoded in Forge traces due to a known bug. ```solidity // import it indirectly via Test.sol\nimport "forge-std/Test.sol";\n// or directly import it\nimport "forge-std/console.sol";\n...\nconsole.log(someValue); ``` -------------------------------- ### Interact with Contract Storage using Forge Standard Library Source: https://github.com/mainstreet-labs/mainstreet-core/blob/main/lib/forge-std/README.md Demonstrates how to find and write to storage slots of contract variables, including public variables, mappings, structs, and hidden storage. Requires importing `forge-std/Test.sol` and inheriting from `Test`. ```solidity import "forge-std/Test.sol"; contract TestContract is Test { using stdStorage for StdStorage; Storage test; function setUp() public { test = new Storage(); } function testFindExists() public { // Lets say we want to find the slot for the public // variable `exists`. We just pass in the function selector // to the `find` command uint256 slot = stdstore.target(address(test)).sig("exists()").find(); assertEq(slot, 0); } function testWriteExists() public { // Lets say we want to write to the slot for the public // variable `exists`. We just pass in the function selector // to the `checked_write` command stdstore.target(address(test)).sig("exists()").checked_write(100); assertEq(test.exists(), 100); } // It supports arbitrary storage layouts, like assembly based storage locations function testFindHidden() public { // `hidden` is a random hash of a bytes, iteration through slots would // not find it. Our mechanism does // Also, you can use the selector instead of a string uint256 slot = stdstore.target(address(test)).sig(test.hidden.selector).find(); assertEq(slot, uint256(keccak256("my.random.var"))); } // If targeting a mapping, you have to pass in the keys necessary to perform the find // i.e.: function testFindMapping() public { uint256 slot = stdstore .target(address(test)) .sig(test.map_addr.selector) .with_key(address(this)) .find(); // in the `Storage` constructor, we wrote that this address' value was 1 in the map // so when we load the slot, we expect it to be 1 assertEq(uint(vm.load(address(test), bytes32(slot))), 1); } // If the target is a struct, you can specify the field depth: function testFindStruct() public { // NOTE: see the depth parameter - 0 means 0th field, 1 means 1st field, etc. uint256 slot_for_a_field = stdstore .target(address(test)) .sig(test.basicStruct.selector) .depth(0) .find(); uint256 slot_for_b_field = stdstore .target(address(test)) .sig(test.basicStruct.selector) .depth(1) .find(); assertEq(uint(vm.load(address(test), bytes32(slot_for_a_field))), 1); assertEq(uint(vm.load(address(test), bytes32(slot_for_b_field))), 2); } } // A complex storage contract contract Storage { struct UnpackedStruct { uint256 a; uint256 b; } constructor() { map_addr[msg.sender] = 1; } uint256 public exists = 1; mapping(address => uint256) public map_addr; // mapping(address => Packed) public map_packed; mapping(address => UnpackedStruct) public map_struct; mapping(address => mapping(address => uint256)) public deep_map; mapping(address => mapping(address => UnpackedStruct)) public deep_map_struct; UnpackedStruct public basicStruct = UnpackedStruct({ a: 1, b: 2 }); function hidden() public view returns (bytes32 t) { // an extremely hidden storage slot bytes32 slot = keccak256("my.random.var"); assembly { t := sload(slot) } } } ``` -------------------------------- ### Schedule and Execute UUPS Upgrade Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Use `scheduleUpgrade` to initiate a timelocked upgrade, followed by `upgradeToAndCall` after the delay. Ensure the new implementation address is set and the upgrade delay has passed before execution. ```solidity interface IUpgraderTimelock { function scheduleUpgrade(address newImpl) external; function cancelScheduledUpgrade() external; function setUpgradeDelay(uint256 newDelay) external; function upgradeDelay() external view returns (uint256); function pendingImplementation() external view returns (address); function activationTime() external view returns (uint256); } address PROXY = 0x; // or StakedmsUSD / MainstreetMinter address NEW_IMPL = 0x; // Step 1: Schedule upgrade (starts the delay clock) IUpgraderTimelock(PROXY).scheduleUpgrade(NEW_IMPL); // Emits: UpgradeScheduled(NEW_IMPL, block.timestamp + upgradeDelay) // Check scheduled state address pending = IUpgraderTimelock(PROXY).pendingImplementation(); // NEW_IMPL uint256 readyAt = IUpgraderTimelock(PROXY).activationTime(); // future timestamp // Cancel if needed // IUpgraderTimelock(PROXY).cancelScheduledUpgrade(); // Emits: UpgradeCanceled(NEW_IMPL) // Step 2: After delay passes, execute upgrade // (calls _authorizeUpgrade which calls _checkTimelock internally) UUPSUpgradeable(PROXY).upgradeToAndCall(NEW_IMPL, ""); // Reverts with DelayNotPassed if called before activationTime // Reverts with ImplementationMismatch if newImpl != pendingImplementation ``` -------------------------------- ### Deploy StaticPriceOracle for USDC.e Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Deploys a StaticPriceOracle for a 6-decimal token (USDC.e) with a fixed price of $1.00, using 8-decimal precision for the price. ```solidity // Deploy StaticPriceOracle for USDC.e at $1.00 (6-decimal token, 8-decimal price) // constructor(address _token, uint256 staticPrice, uint8 decimals) StaticPriceOracle oracle = new StaticPriceOracle( address(USDC_E), // 6-decimal token 1e8, // $1.00 in 8-decimal price format 8 // price decimals ); ``` -------------------------------- ### Calculate hypothetical value at a different price Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Demonstrates calculating the value of an amount of collateral at a hypothetical price, simulating scenarios like a depeg. Uses floor rounding. ```solidity // Hypothetical: value at a different price (e.g., $0.999 depeg scenario) uint256 depegPrice = 0.999e18; // 18-decimal internal representation uint256 depegValue = oracle.valueOfAtPrice(500e6, depegPrice, Math.Rounding.Floor); // depegValue < 500e18 (slight underpeg) ``` -------------------------------- ### Using console2.sol for Decoded Logs Source: https://github.com/mainstreet-labs/mainstreet-core/blob/main/lib/forge-std/README.md Utilizes `console2.sol` for logging values, which provides decoded logs in Forge traces. Import `forge-std/Test.sol` or `forge-std/console2.sol`. ```solidity // import it indirectly via Test.sol\nimport "forge-std/Test.sol";\n// or directly import it\nimport "forge-std/console2.sol";\n...\nconsole2.log(someValue); ``` -------------------------------- ### MainstreetMinter.mint Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Allows whitelisted users to deposit collateral assets and mint an equivalent USD-valued amount of msUSD. A configurable tax is deducted, and the transaction reverts if the output falls below a specified minimum. ```APIDOC ## mint — Deposit collateral to mint msUSD ### Description Whitelisted users transfer a supported collateral asset to receive an equivalent USD-valued amount of msUSD. A configurable `tax` (basis points per 1000) is deducted from `amountIn` before oracle valuation. Reverts if output falls below `minAmountOut`. ### Method `mint(address asset, uint256 amountIn, uint256 minAmountOut) external returns (uint256 amountOut)` ### Parameters #### Path Parameters - **asset** (address) - The address of the collateral token to deposit. - **amountIn** (uint256) - The amount of collateral token to deposit. - **minAmountOut** (uint256) - The minimum amount of msUSD expected to be minted, to prevent excessive slippage. ### Request Example ```solidity // Usage: mint 1000 USDC.e -> msUSD (assuming 0% tax and 1:1 oracle price) address MINTER = 0x; address USDC_E = 0x; uint256 amountIn = 1000e6; // 1000 USDC.e (6 decimals) // 1. Preview expected output uint256 expected = IMainstreetMinter(MINTER).quoteMint(USDC_E, amountIn); // expected ≈ 1000e18 msUSD (18 decimals, scaled by oracle) // 2. Approve minter to pull collateral IERC20(USDC_E).approve(MINTER, amountIn); // 3. Mint with 0.5% slippage tolerance uint256 minOut = expected * 995 / 1000; uint256 amountOut = IMainstreetMinter(MINTER).mint(USDC_E, amountIn, minOut); // amountOut: actual msUSD minted to msg.sender // Emits: Mint(msg.sender, USDC_E, 1000e6, amountOut) ``` ### Response #### Success Response - **amountOut** (uint256) - The amount of msUSD minted to the caller. ``` -------------------------------- ### Manage Supported Assets Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Owner-only functions to add, remove, or update collateral assets and their associated price oracles. Use `isSupportedAsset` to check status and `getActiveAssets` to list current assets. ```solidity interface IMainstreetMinter { function addSupportedAsset(address asset, address oracle) external; function removeSupportedAsset(address asset) external; function restoreAsset(address asset) external; function modifyOracleForAsset(address asset, address newOracle) external; function isSupportedAsset(address asset) external view returns (bool); function getActiveAssets() external view returns (address[] memory); function getAllAssets() external view returns (address[] memory); } address MINTER = 0x; address NEW_TOKEN = 0x; address NEW_ORACLE = 0x; // or Chainlink-based oracle // Add new collateral (owner only) IMainstreetMinter(MINTER).addSupportedAsset(NEW_TOKEN, NEW_ORACLE); // Emits: AssetAdded(NEW_TOKEN, NEW_ORACLE) // Verify registration bool supported = IMainstreetMinter(MINTER).isSupportedAsset(NEW_TOKEN); // true address[] memory active = IMainstreetMinter(MINTER).getActiveAssets(); // active: array of all non-removed collateral addresses // Update oracle for existing asset IMainstreetMinter(MINTER).modifyOracleForAsset(NEW_TOKEN, 0x); // Deactivate asset (blocks new mints/redemptions, pending claims still claimable) IMainstreetMinter(MINTER).removeSupportedAsset(NEW_TOKEN); // Emits: AssetRemoved(NEW_TOKEN) // Restore if needed IMainstreetMinter(MINTER).restoreAsset(NEW_TOKEN); ``` -------------------------------- ### addSupportedAsset / removeSupportedAsset / restoreAsset / modifyOracleForAsset Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Manages the registry of supported collateral assets. Owners can add new assets with their price oracles, remove existing ones, restore removed assets, or update the oracle for an asset. The `activeAssetsLength` counter tracks live assets. ```APIDOC ## Asset Management Functions ### Description Functions to manage the collateral registry. `addSupportedAsset` registers new collateral tokens with price oracles. `removeSupportedAsset` deactivates existing ones, blocking new mints/redemptions while allowing pending claims. `restoreAsset` can reactivate removed assets. `modifyOracleForAsset` updates the price oracle for a given asset. ### Methods - `addSupportedAsset(address asset, address oracle)`: Adds a new supported asset and its oracle. - `removeSupportedAsset(address asset)`: Removes an asset from the supported list. - `restoreAsset(address asset)`: Restores a previously removed asset. - `modifyOracleForAsset(address asset, address newOracle)`: Updates the oracle for an existing asset. ### Parameters - **asset** (address) - The address of the collateral token. - **oracle** (address) - The address of the price oracle for the asset. - **newOracle** (address) - The address of the updated price oracle. ### Request Example ```solidity address MINTER = 0x; address NEW_TOKEN = 0x; address NEW_ORACLE = 0x; // Add new collateral (owner only) IMainstreetMinter(MINTER).addSupportedAsset(NEW_TOKEN, NEW_ORACLE); // Update oracle for existing asset IMainstreetMinter(MINTER).modifyOracleForAsset(NEW_TOKEN, 0x); // Deactivate asset IMainstreetMinter(MINTER).removeSupportedAsset(NEW_TOKEN); // Restore if needed IMainstreetMinter(MINTER).restoreAsset(NEW_TOKEN); ``` ### Emits - `AssetAdded(asset, oracle)` when `addSupportedAsset` is called. - `AssetRemoved(asset)` when `removeSupportedAsset` is called. ``` -------------------------------- ### UpgraderTimelock Interface Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Provides functions to schedule, cancel, and manage timelocked upgrades for core contracts. ```APIDOC ## UpgraderTimelock Interface ### Description Manages the two-step upgrade process for core contracts, requiring a delay between scheduling and execution to prevent malicious upgrades. ### Interface ```solidity interface IUpgraderTimelock { function scheduleUpgrade(address newImpl) external; function cancelScheduledUpgrade() external; function setUpgradeDelay(uint256 newDelay) external; function upgradeDelay() external view returns (uint256); function pendingImplementation() external view returns (address); function activationTime() external view returns (uint256); } ``` ### Usage Examples #### Schedule Upgrade ```solidity address PROXY = 0x; // or StakedmsUSD / MainstreetMinter address NEW_IMPL = 0x; // Step 1: Schedule upgrade (starts the delay clock) IUpgraderTimelock(PROXY).scheduleUpgrade(NEW_IMPL); // Emits: UpgradeScheduled(NEW_IMPL, block.timestamp + upgradeDelay) ``` #### Check Scheduled State ```solidity address pending = IUpgraderTimelock(PROXY).pendingImplementation(); // NEW_IMPL uint256 readyAt = IUpgraderTimelock(PROXY).activationTime(); // future timestamp ``` #### Cancel Upgrade ```solidity // Cancel if needed // IUpgraderTimelock(PROXY).cancelScheduledUpgrade(); // Emits: UpgradeCanceled(NEW_IMPL) ``` #### Execute Upgrade ```solidity // Step 2: After delay passes, execute upgrade // (calls _authorizeUpgrade which calls _checkTimelock internally) UUPSUpgradeable(PROXY).upgradeToAndCall(NEW_IMPL, ""); // Reverts with DelayNotPassed if called before activationTime // Reverts with ImplementationMismatch if newImpl != pendingImplementation ``` ``` -------------------------------- ### IOracle Interface Definition Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Defines the interface for price oracles, including methods for valuing amounts, calculating amounts, and retrieving the latest price. Supports hypothetical price scenarios. ```solidity interface IOracle { function valueOf(uint256 amount, Math.Rounding rounding) external view returns (uint256 value); function valueOf(uint256 amount, uint256 maxAge, Math.Rounding rounding) external view returns (uint256 value); function amountOf(uint256 value, Math.Rounding rounding) external view returns (uint256 amount); function amountOf(uint256 value, uint256 maxAge, Math.Rounding rounding) external view returns (uint256 amount); function latestPrice() external view returns (uint256 price); function amountOfAtPrice(uint256 value, uint256 price, Math.Rounding rounding) external view returns (uint256); function valueOfAtPrice(uint256 amount, uint256 price, Math.Rounding rounding) external view returns (uint256); } ``` -------------------------------- ### Initiate Withdrawal with Cooldown Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt When a cooldown duration is active, users initiate withdrawals by specifying either an amount of assets or shares, which are then moved to a silo and subject to a cooldown timer. ```APIDOC ## cooldownAssets / cooldownShares — Initiate withdrawal with cooldown ### Description When `cooldownDuration > 0`, standard ERC-4626 `withdraw`/`redeem` are disabled. Users must call `cooldownAssets` (specify msUSD amount) or `cooldownShares` (specify smsUSD shares) to start the cooldown timer. Assets are immediately moved to the `msUSDSilo`. ### Interface ```solidity interface IStakedmsUSD { function cooldownAssets(uint256 assets, address owner) external returns (uint256 shares); function cooldownShares(uint256 shares, address owner) external returns (uint256 assets); function cooldowns(address user) external view returns (uint104 cooldownEnd, uint256 underlyingAmount); function cooldownDuration() external view returns (uint24); function unstake(address receiver) external; } ``` ### Example Usage ```solidity address S_MSUSD = 0x; uint256 myShares = 500 ether; // smsUSD shares to redeem // Check current cooldown window uint24 duration = IStakedmsUSD(S_MSUSD).cooldownDuration(); // e.g., 7 days // Option A: initiate cooldown by share count uint256 assetsLocked = IStakedmsUSD(S_MSUSD).cooldownShares(myShares, msg.sender); // assetsLocked: msUSD moved to silo, cooldownEnd = now + cooldownDuration // Option B: initiate cooldown by asset amount // uint256 sharesUsed = IStakedmsUSD(S_MSUSD).cooldownAssets(1000 ether, msg.sender); // Check cooldown status (uint104 cooldownEnd, uint256 underlyingAmount) = IStakedmsUSD(S_MSUSD).cooldowns(msg.sender); // cooldownEnd: timestamp after which unstake() may be called // underlyingAmount: msUSD waiting in silo // After cooldown expires: claim assets // block.timestamp >= cooldownEnd required or CooldownNotFinished is thrown IStakedmsUSD(S_MSUSD).unstake(msg.sender); // Transfers underlyingAmount * coverageRatio / 1e18 msUSD from silo to receiver // Emits: Unstake(msg.sender, receiver, underlyingAmount, amountForRedeemer) ``` ``` -------------------------------- ### RewardsWrapper Interface Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Handles reward distribution for StakedmsUSD, gating minting and tracking the vault's implied APR. ```APIDOC ## RewardsWrapper Interface ### Description Manages reward distribution for `StakedmsUSD`, controlled by a `masterMinter` role. It automatically tracks the vault's implied Annual Percentage Rate (APR) using an exponential moving average (EMA) of recent rate observations. ### Interface ```solidity interface IRewardsWrapper { function mintRewards(uint256 amount) external; function updateMasterMinter(address newMasterMinter) external; function apr(address vault) external view returns (int256); function getCurrentInterestRate(address vault) external view returns (int256 currentRate); function masterMinter() external view returns (address); } ``` ### Usage Examples #### Mint Rewards ```solidity address REWARDS_WRAPPER = 0x; address S_MSUSD = 0x; // Distribute 2000 msUSD in rewards (masterMinter only) IRewardsWrapper(REWARDS_WRAPPER).mintRewards(2000 ether); // Internally: calls StakedmsUSD.mintRewards(2000e18) // Tracks vault balance change -> calculates new APR -> updates EMA // Emits: InterestRateUpdated(S_MSUSD, newRate) ``` #### Query APR ```solidity // Query current EMA APR (scaled to 1e18 = 100% APR) int256 emaAPR = IRewardsWrapper(REWARDS_WRAPPER).apr(S_MSUSD); // e.g., 0.05e18 = 5% APR ``` #### Query Current Interest Rate ```solidity // Query instantaneous last rate int256 lastRate = IRewardsWrapper(REWARDS_WRAPPER).getCurrentInterestRate(S_MSUSD); ``` ``` -------------------------------- ### Bridge msUSD via OFT - IOFTCore Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Facilitates cross-chain transfer of msUSD using LayerZero's OFT protocol. On the home chain, tokens are transferred to the bridge contract, preserving total supply. Requires prior approval and payment of bridging fees. ```solidity interface IOFTCore { function sendFrom( address _from, uint16 _dstChainId, bytes calldata _toAddress, uint256 _amount, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams ) external payable; function estimateSendFee( uint16 _dstChainId, bytes calldata _toAddress, uint256 _amount, bool _useZro, bytes calldata _adapterParams ) external view returns (uint256 nativeFee, uint256 zroFee); } address MS_USD = 0x; uint16 SONIC_CHAIN_ID = 332; // LayerZero chain ID for Sonic address recipient = 0x; uint256 bridgeAmt = 200 ether; // Encode recipient as bytes bytes memory toAddress = abi.encodePacked(recipient); // Estimate bridge fee (uint256 fee, ) = IOFTCore(MS_USD).estimateSendFee(SONIC_CHAIN_ID, toAddress, bridgeAmt, false, ""); // Approve and bridge (caller must own bridgeAmt of msUSD) IERC20(MS_USD).approve(MS_USD, bridgeAmt); IOFTCore(MS_USD).sendFrom{value: fee}( msg.sender, // from SONIC_CHAIN_ID, // destination chain toAddress, // recipient (bytes) bridgeAmt, // amount payable(msg.sender), // refund address address(0), // ZRO payment (not used) "" // adapter params (default gas) ); // Home chain: tokens transferred to msUSDV2 contract (supply preserved) // Satellite chain: equivalent amount minted to recipient ``` -------------------------------- ### Use stdError for Expect Revert Source: https://github.com/mainstreet-labs/mainstreet-core/blob/main/lib/forge-std/README.md Demonstrates using stdError with the expectRevert cheatcode to test specific compiler builtin errors. Ensure the ErrorsTest contract is available. ```solidity import "forge-std/Test.sol"; contract TestContract is Test { ErrorsTest test; function setUp() public { test = new ErrorsTest(); } function testExpectArithmetic() public { vm.expectRevert(stdError.arithmeticError); test.arithmeticError(10); } } contract ErrorsTest { function arithmeticError(uint256 a) public { uint256 a = a - 100; } } ``` -------------------------------- ### Mint msUSD by Depositing Collateral Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Use this function to mint msUSD by depositing a supported collateral asset. Ensure you approve the minter contract to pull the collateral first. A tax is deducted from the input amount before oracle valuation. ```Solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IMainstreetMinter { function mint(address asset, uint256 amountIn, uint256 minAmountOut) external returns (uint256 amountOut); function quoteMint(address asset, uint256 amountIn) external view returns (uint256 amountAsset); } // Usage: mint 1000 USDC.e -> msUSD (assuming 0% tax and 1:1 oracle price) address MINTER = 0x; address USDC_E = 0x; uint256 amountIn = 1000e6; // 1000 USDC.e (6 decimals) // 1. Preview expected output uint256 expected = IMainstreetMinter(MINTER).quoteMint(USDC_E, amountIn); // expected ≈ 1000e18 msUSD (18 decimals, scaled by oracle) // 2. Approve minter to pull collateral IERC20(USDC_E).approve(MINTER, amountIn); // 3. Mint with 0.5% slippage tolerance uint256 minOut = expected * 995 / 1000; uint256 amountOut = IMainstreetMinter(MINTER).mint(USDC_E, amountIn, minOut); // amountOut: actual msUSD minted to msg.sender // Emits: Mint(msg.sender, USDC_E, 1000e6, amountOut) ``` -------------------------------- ### sendFrom — Cross-chain OFT bridge (home chain) Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Facilitates the bridging of msUSD tokens to satellite chains using the LayerZero OFT (Omni-Chain Fungible Token) standard. On the home chain, tokens are transferred to the bridge contract, preserving the total supply. ```APIDOC ## `sendFrom` — Cross-chain OFT bridge (home chain) ### Description Bridges msUSD to satellite chains via LayerZero. On the home chain (Ethereum), tokens are transferred to the contract rather than burned, preserving total supply. On arrival at the destination, the satellite contract mints an equivalent amount. ### Method `IOFTCore.sendFrom( address _from, uint16 _dstChainId, bytes calldata _toAddress, uint256 _amount, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams )` ### Parameters - **_from** (address): The address from which tokens are sent. - **_dstChainId** (uint16): The LayerZero chain ID of the destination chain. - **_toAddress** (bytes): The recipient address on the destination chain, encoded as bytes. - **_amount** (uint256): The amount of msUSD to bridge. - **_refundAddress** (address payable): The address to refund any excess gas fees. - **_zroPaymentAddress** (address): The address for ZRO payment (set to `address(0)` if not used). - **_adapterParams** (bytes): Parameters for the LayerZero adapter (e.g., gas limits). ### Example ```solidity address MS_USD = 0x; uint16 SONIC_CHAIN_ID = 332; // LayerZero chain ID for Sonic address recipient = 0x; uint256 bridgeAmt = 200 ether; // Encode recipient as bytes bytes memory toAddress = abi.encodePacked(recipient); // Approve and bridge (caller must own bridgeAmt of msUSD) IERC20(MS_USD).approve(MS_USD, bridgeAmt); IOFTCore(MS_USD).sendFrom{value: fee}( msg.sender, // from SONIC_CHAIN_ID, // destination chain toAddress, // recipient (bytes) bridgeAmt, // amount payable(msg.sender), // refund address address(0), // ZRO payment (not used) "" // adapter params (default gas) ); ``` ### Related Methods - `estimateSendFee`: Estimates the native and ZRO fees for sending tokens across chains. ``` ```APIDOC ## `estimateSendFee` — Estimate bridge fee ### Description Estimates the native token fee and ZRO token fee required to send tokens across chains using the LayerZero OFT bridge. ### Method `IOFTCore.estimateSendFee( uint16 _dstChainId, bytes calldata _toAddress, uint256 _amount, bool _useZro, bytes calldata _adapterParams ) external view returns (uint256 nativeFee, uint256 zroFee)` ### Parameters - **_dstChainId** (uint16): The LayerZero chain ID of the destination chain. - **_toAddress** (bytes): The recipient address on the destination chain, encoded as bytes. - **_amount** (uint256): The amount of msUSD to bridge. - **_useZro** (bool): Whether to use ZRO for fee payment. - **_adapterParams** (bytes): Parameters for the LayerZero adapter. ### Returns - **nativeFee** (uint256): The estimated fee in the native token of the home chain. - **zroFee** (uint256): The estimated fee in ZRO tokens. ### Example ```solidity // Estimate bridge fee (uint256 fee, ) = IOFTCore(MS_USD).estimateSendFee(SONIC_CHAIN_ID, toAddress, bridgeAmt, false, ""); ``` ``` -------------------------------- ### Manage msUSDV2 Token Supply - ImsUSDV2 Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Controls the supply of msUSDV2 tokens. Only the registered minter or stakedmsUSD contract can mint. Owners can set supply limits. Holders can burn their own tokens. ```solidity interface ImsUSDV2 { function mint(address to, uint256 amount) external; function burn(uint256 amount) external; function burnFrom(address account, uint256 amount) external; function setSupplyLimit(uint256 limit) external; function supplyLimit() external view returns (uint256); function minter() external view returns (address); } address MS_USD = 0x; // Check current supply limit uint256 limit = ImsUSDV2(MS_USD).supplyLimit(); // default: 10_000_000 ether // Owner raises supply limit to 50M ImsUSDV2(MS_USD).setSupplyLimit(50_000_000 ether); // Emits: SupplyLimitUpdated(50_000_000 ether) // Burn your own msUSD (e.g., 100 tokens) ImsUSDV2(MS_USD).burn(100 ether); // Third-party burnFrom (requires approval) — used by MainstreetMinter during requestTokens IERC20(MS_USD).approve(MINTER, 500 ether); ImsUSDV2(MS_USD).burnFrom(msg.sender, 500 ether); // Reverts with NotAuthorized if caller is neither minter nor stakedmsUSD for mint() ``` -------------------------------- ### Mint Rewards with RewardsWrapper Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt The `mintRewards` function distributes rewards and tracks the vault's APR using an EMA. This function can only be called by the `masterMinter` role. ```solidity interface IRewardsWrapper { function mintRewards(uint256 amount) external; function updateMasterMinter(address newMasterMinter) external; function apr(address vault) external view returns (int256); function getCurrentInterestRate(address vault) external view returns (int256 currentRate); function masterMinter() external view returns (address); } address REWARDS_WRAPPER = 0x; address S_MSUSD = 0x; // Distribute 2000 msUSD in rewards (masterMinter only) IRewardsWrapper(REWARDS_WRAPPER).mintRewards(2000 ether); // Internally: calls StakedmsUSD.mintRewards(2000e18) // Tracks vault balance change -> calculates new APR -> updates EMA // Emits: InterestRateUpdated(S_MSUSD, newRate) // Query current EMA APR (scaled to 1e18 = 100% APR) int256 emaAPR = IRewardsWrapper(REWARDS_WRAPPER).apr(S_MSUSD); // e.g., 0.05e18 = 5% APR // Query instantaneous last rate int256 lastRate = IRewardsWrapper(REWARDS_WRAPPER).getCurrentInterestRate(S_MSUSD); ``` -------------------------------- ### Set Redemption Coverage Ratio - IMainstreetMinter Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Adjusts the coverage ratio for matured redemption requests. A ratio below 1e18 proportionally reduces claimant payouts. Only callable by the admin. ```solidity interface IMainstreetMinter { function setCoverageRatio(uint256 ratio) external; function latestCoverageRatio() external view returns (uint256); function getCoverageRatioAt(uint48 timestamp) external view returns (uint256 ratio); } address MINTER = 0x; // Normal state: 100% coverage uint256 current = IMainstreetMinter(MINTER).latestCoverageRatio(); // 1e18 // Emergency: reduce to 90% (admin only) if protocol is 10% short IMainstreetMinter(MINTER).setCoverageRatio(0.9e18); // Emits: CoverageRatioUpdated(0.9e18) // Future claimTokens calls will return 90% of requested collateral // Query historical ratio at a specific timestamp uint256 ratioAtTime = IMainstreetMinter(MINTER).getCoverageRatioAt(uint48(block.timestamp - 1 days)); ``` -------------------------------- ### msUSDV2 Token Operations Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Manages the supply of the msUSDV2 token, including minting, burning, and setting supply limits. It also details the `burnFrom` functionality used during redemptions. ```APIDOC ## msUSDV2 ### `mint` / `burn` / `burnFrom` — Controlled token supply ### Description Only the registered `minter` (MainstreetMinter) or `stakedmsUSD` contract can mint new msUSD. Any holder can burn their own tokens. `burnFrom` requires prior approval and is used by the minter during redemptions. ### Methods - `mint(address to, uint256 amount)`: Mints new msUSD tokens. Only callable by `minter` or `stakedmsUSD`. - `burn(uint256 amount)`: Burns msUSD tokens owned by the caller. - `burnFrom(address account, uint256 amount)`: Burns msUSD tokens from a specified account, requiring prior approval. ### Example ```solidity // Burn your own msUSD (e.g., 100 tokens) ImsUSDV2(MS_USD).burn(100 ether); // Third-party burnFrom (requires approval) — used by MainstreetMinter during requestTokens IERC20(MS_USD).approve(MINTER, 500 ether); ImsUSDV2(MS_USD).burnFrom(msg.sender, 500 ether); ``` ### Emits `Transfer` event for mint and burn operations. ``` ```APIDOC ## `setSupplyLimit` / `supplyLimit` — Manage supply cap ### Description Allows the owner to set an upper limit on the total supply of msUSDV2 tokens and to query the current limit. ### Methods - `setSupplyLimit(uint256 limit)`: Sets the maximum supply limit for msUSDV2. Only callable by the owner. - `supplyLimit()`: Returns the current maximum supply limit. ### Example ```solidity // Check current supply limit uint256 limit = ImsUSDV2(MS_USD).supplyLimit(); // default: 10_000_000 ether // Owner raises supply limit to 50M ImsUSDV2(MS_USD).setSupplyLimit(50_000_000 ether); ``` ### Emits `SupplyLimitUpdated(uint256 newLimit)` ``` -------------------------------- ### Configure StakedmsUSD Cooldown Source: https://context7.com/mainstreet-labs/mainstreet-core/llms.txt Set or disable the cooldown duration for staking withdrawals. Use 0 to disable cooldown mode entirely. Ensure duration is within the maximum limit of 90 days. ```solidity interface IStakedmsUSD { function setCooldownDuration(uint24 duration) external; // onlyOwner, max 90 days function updateExistingCooldown(address account, uint104 newCooldownEnd) external; // onlyOwner function setCoverageRatio(uint256 _ratio) external; // onlyOwner, <= 1e18 function coverageRatio() external view returns (uint256); } address S_MSUSD = 0x; // Disable cooldown (enables immediate ERC-4626 withdrawals) IStakedmsUSD(S_MSUSD).setCooldownDuration(0); // Emits: CooldownDurationUpdated(previousDuration, 0) // Standard withdraw()/redeem() now functional // Re-enable with 14-day cooldown IStakedmsUSD(S_MSUSD).setCooldownDuration(14 days); // Emergency: extend a user's cooldown (e.g., system under stress) IStakedmsUSD(S_MSUSD).updateExistingCooldown(0x, uint104(block.timestamp + 30 days)); // Emergency: reduce coverage to 95% (undercollateralized scenario) IStakedmsUSD(S_MSUSD).setCoverageRatio(0.95e18); // Future unstake() calls return 95% of locked assets; 5% is burned ```