diff --git a/foundry.toml b/foundry.toml index 15b98d56c..e030e32fe 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,8 @@ # This overrides the `auto_detect_solc` value solc_version = '0.8.21' auto_detect_solc = false -evm_version = 'cancun' +evm_version = 'london' +viaIR = true optimizer = true optimizer_runs = 200 fs_permissions = [{ access = "read-write", path = "./" }] @@ -31,8 +32,8 @@ base = "${BASE_RPC_URL}" zircuit = "${ZIRCUIT_RPC_URL}" scroll = "${SCROLL_RPC_URL}" linea = "${LINEA_RPC_URL}" +mantra = "${MANTRA_RPC_URL}" mantra_dukong = "${MANTRA_DUKONG_RPC_URL}" -# mantra = "${MANTRA_RPC_URL}" [etherscan] # mainnet = { key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/api" } @@ -42,8 +43,9 @@ mantra_dukong = "${MANTRA_DUKONG_RPC_URL}" # arbitrum = { key = "${ARBISCAN_KEY}" } # optimism = { key = "${OPTIMISMSCAN_KEY}" } # base = { key = "${BASESCAN_KEY}" } -mantra_dukong = { key = "empty", url = "https://explorer.dukong.io/api" } -5887 = { key = "empty", url = "https://explorer.dukong.io/api" } +mantra = { key = "any_random_string", chain = 5888, url = "https://blockscout.mantrascan.io/api" } +mantra_dukong = { key = "any_random_string", chain = 5887, url = "https://explorer.dukong.io/api" } + [fmt] FOUNDRY_FMT_LINE_LENGTH = 120 diff --git a/mantra.example.env b/mantra.example.env new file mode 100644 index 000000000..65158df0e --- /dev/null +++ b/mantra.example.env @@ -0,0 +1,37 @@ +# ===================================================================== +# 1. REQUIRED INPUTS (Fill these before running anything) +# ===================================================================== + +# The Private Key of the wallet deploying the contracts. +# Found in all scripts via: vm.envUint("ETHERFI_LIQUID_DEPLOYER") +MANTRA_DEPLOYER=0xYourPrivateKeyHere... + +# RPC URL for Mantra Chain. +# Needed for: vm.createSelectFork("mantra") and the --rpc-url command +MANTRA_RPC_URL=https://rpc.mantrachain.io + +# For testnet deployments +MANTRA_DUKONG_RPC_URL=https://rpc.dukong.io + +# ===================================================================== +# 2. NETWORK TOGGLE +# ===================================================================== + +# Set to 'true' to use Mainnet addresses (mUSD_MAINNET). +# Set to 'false' to use Testnet addresses (mUSD_TESTNET). +# Used as: vm.envOr("MANTRA_MAINNET", false) +MANTRA_MAINNET=false + +# ===================================================================== +# 3. GENERATED ADDRESSES (Fill these AFTER running Script 01) +# ===================================================================== +# Script 01 prints these addresses to the console. +# You must copy/paste them here before running Scripts 02, 03, or 04. + +# The address of the deployed 'Deployer' contract +# Found in Scripts 02, 03, 04 via: vm.envAddress("DEPLOYER_CONTRACT_ADDRESS") +DEPLOYER_CONTRACT_ADDRESS= + +# The address of the deployed 'RolesAuthority' contract +# Found in Scripts 02, 03 via: vm.envAddress("ROLES_AUTH_CONTRACT_ADDRESS") +ROLES_AUTH_CONTRACT_ADDRESS= \ No newline at end of file diff --git a/script/00_MantraConstants.sol b/script/00_MantraConstants.sol new file mode 100644 index 000000000..964e0cc70 --- /dev/null +++ b/script/00_MantraConstants.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +library MantraConstants { + // ========================================== + // USER CONFIGURATION + // ========================================== + + /* + EXAMPLE: + 1. RATE_UPDATER: The address (e.g. Cron Job Bot) that updates exchange rates. + 2. MANAGER: The address (e.g. Multisig) for pausing/emergency actions. + 3. OWNER: The address (e.g. Multisig) that will OWN the system after deployment. + */ + + // --- Testnet (Dukong) --- + address internal constant RATE_UPDATER_TESTNET = + 0x37723e376FdF70854665B5f1a5C49cB30E1691AC; + address internal constant MANAGER_TESTNET = + 0x09676Ee4685B618d0DCc85E221019c9Ce3810211; + address internal constant OWNER_TESTNET = + 0x09676Ee4685B618d0DCc85E221019c9Ce3810211; + + // --- Mainnet --- + address internal constant RATE_UPDATER_MAINNET = + 0x37723e376FdF70854665B5f1a5C49cB30E1691AC; + address internal constant MANAGER_MAINNET = + 0x9bbf09dEC8B93CE548eA6fE50e9786964e47Ee5e; + address internal constant OWNER_MAINNET = + 0x9bbf09dEC8B93CE548eA6fE50e9786964e47Ee5e; + + // ========================================== + // INTERNAL CONSTANTS + // ========================================== + + // --- External Tokens --- + // Testnet (Dukong) + address internal constant mUSD_TESTNET = + 0x4B545d0758eda6601B051259bD977125fbdA7ba2; + address internal constant WETH_TESTNET = address(0); + + // Mainnet (Placeholder) + address internal constant mUSD_MAINNET = + 0xd2b95283011E47257917770D28Bb3EE44c849f6F; + address internal constant WETH_MAINNET = address(0); + + // ========================================== + // Roles Authority Configuration + // ========================================== + + /* + DEPLOYER_ROLE (id=1) + Purpose: Permission to deploy contracts via helper Deployer. + Applied in current deploy scripts: + - script/01_DeployDeployer.s.sol: + capability: Deployer.deployContract() + contract: Deployer + grantee: deployer EOA (MANTRA_DEPLOYER) + Role holder: dedicated deployer key or deployment multisig. + */ + uint8 internal constant DEPLOYER_ROLE = 1; + + /* + MANAGER_ROLE (id=1) + Purpose: Generic operational/admin permissions (manage/pause/config/update). + Applied in current deploy scripts: + - Not explicitly granted in 01-04 scripts. + - Same id as DEPLOYER_ROLE, so any capability attached to role id=1 + is effectively shared between MANAGER_ROLE and DEPLOYER_ROLE. + Typical holder: Multisig or admin EOA. + */ + uint8 internal constant MANAGER_ROLE = DEPLOYER_ROLE; + + /* + MINTER_ROLE (id=2) + Purpose: Mint BoringVault shares through vault.enter(). + Applied in current deploy scripts: + - script/02_DeployRFRYieldVault.s.sol: + - script/03_DeployPointsVault.s.sol: + capability: BoringVault.enter() + contract: RFR Yield BoringVault, Points BoringVault + grantee: RFR Yield TellerWithMultiAssetSupport, Points TellerWithMultiAssetSupport + Role holder: Teller contract that processes deposits. + */ + uint8 internal constant MINTER_ROLE = 2; + + /* + BURNER_ROLE (id=3) + Purpose: Burn BoringVault shares through vault.exit() during withdrawals/refunds. + Applied in current deploy scripts: + - script/02_DeployRFRYieldVault.s.sol: + - script/03_DeployPointsVault.s.sol: + capability: BoringVault.exit() + contract: RFR Yield BoringVault, Points BoringVault + grantee: RFR Yield DelayedWithdraw, Points DelayedWithdraw + Role holder: withdrawal coordinator contract (DelayedWithdraw). + */ + uint8 internal constant BURNER_ROLE = 3; + + /* + OWNER_ROLE (id=8) + Purpose: Day-to-day configuration for Teller/Withdraw flows. + Applied in current deploy scripts: + - script/02_DeployRFRYieldVault.s.sol: + - script/03_DeployPointsVault.s.sol: + TellerWithMultiAssetSupport.setShareLockPeriod() + TellerWithMultiAssetSupport.updateAssetData() + DelayedWithdraw.setupWithdrawAsset() + DelayedWithdraw.setPullFundsFromVault() + contract: RFR Yield | Points TellerWithMultiAssetSupport, RFR Yield | Points DelayedWithdraw + grantee: owner EOA (MANTRA_DEPLOYER) + Role holder: operations multisig (preferable) or trusted admin EOA. + */ + uint8 internal constant OWNER_ROLE = 8; + + /* + UPDATE_EXCHANGE_RATE_ROLE (id=11) + Purpose: Call accountant.updateExchangeRate() to move vault share price. + Applied in current deploy scripts: + - script/02_DeployRFRYieldVault.s.sol: + - script/02_DeployRFRYieldVault.s.sol: + contract: RFR Yield AccountantWithRateProviders, Points AccountantWithFixedRate + grantee: owner EOA (MANTRA_DEPLOYER) + capability: AccountantWithRateProviders.updateExchangeRate() + Role holder: dedicated updater bot/service with strict monitoring. + */ + uint8 internal constant UPDATE_EXCHANGE_RATE_ROLE = 11; + + // ========================================== + // Contract Names + // ========================================== + + // Arctic Lens + string internal constant ARCTIC_LENS_NAME = "Lens V1.0"; + + // RFR Yield Vault + string internal constant RFR_VAULT_NAME = "wmantraUSD (Yield)"; + string internal constant RFR_SYMBOL = "wmantraUSD-Yld"; + uint8 internal constant RFR_DECIMALS = 6; + string internal constant RFR_TOKEN_NAME = "wmantraUSD (Yield)"; + string internal constant RFR_ACCOUNTANT_NAME = + "wmantraUSD (Yield) Accountant v1.0"; + string internal constant RFR_TELLER_NAME = "wmantraUSD (Yield) Teller v1.0"; + string internal constant RFR_DW_NAME = + "wmantraUSD (Yield) DelayedWithdraw v1.0"; + + // Points Vault + string internal constant POINTS_VAULT_NAME = "wmantraUSD (Points)"; + string internal constant POINTS_SYMBOL = "wmantraUSD-Pts"; + uint8 internal constant POINTS_DECIMALS = 6; + string internal constant POINTS_TOKEN_NAME = "wmantraUSD (Points)"; + string internal constant POINTS_ACCOUNTANT_NAME = + "wmantraUSD (Points) Accountant v1.0"; + string internal constant POINTS_TELLER_NAME = + "wmantraUSD (Points) Teller v1.0"; + string internal constant POINTS_DW_NAME = + "wmantraUSD (Points) DelayedWithdraw v1.0"; + + // ========================================== + // Accountant Configuration + // ========================================== + uint96 internal constant ACCOUNTANT_STARTING_EXCHANGE_RATE = 1e6; + uint16 internal constant ACCOUNTANT_ALLOWED_EXCHANGE_RATE_CHANGE_UPPER = + 1.5e4; + uint16 internal constant ACCOUNTANT_ALLOWED_EXCHANGE_RATE_CHANGE_LOWER = + 0.5e4; + uint64 internal constant ACCOUNTANT_MINIMUM_UPDATE_DELAY = 20 hours; + uint16 internal constant ACCOUNTANT_PLATFORM_FEE = 0; + uint16 internal constant ACCOUNTANT_PERFORMANCE_FEE = 500; + + // ========================================== + // Teller Configuration + // ========================================== + uint64 internal constant TELLER_SHARE_LOCK_PERIOD = 86400; // 24 hours + + // ========================================== + // Delayed Withdraw Configuration + // ========================================== + uint32 internal constant DW_WITHDRAW_DELAY = 0; // zero delay on withdraw + uint32 internal constant DW_COMPLETION_WINDOW = 7 days; + uint16 internal constant DW_WITHDRAW_FEE = 0; + uint16 internal constant DW_MAX_LOSS = 100; // 1% +} diff --git a/script/01_DeployDeployer.s.sol b/script/01_DeployDeployer.s.sol new file mode 100644 index 000000000..e0c920755 --- /dev/null +++ b/script/01_DeployDeployer.s.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +import {Deployer} from "src/helper/Deployer.sol"; +import { + RolesAuthority, + Authority +} from "@solmate/auth/authorities/RolesAuthority.sol"; +import "forge-std/Script.sol"; +import {MantraConstants as Constants} from "./00_MantraConstants.sol"; + +// forge script script/01_DeployDeployer.s.sol --rpc-url mantra_dukong --broadcast --slow + +contract DeployDeployer is Script { + function run() external { + uint256 deployerKey = vm.envUint("MANTRA_DEPLOYER"); + address deployerAddr = vm.addr(deployerKey); + + vm.startBroadcast(deployerKey); + + // 1. Deploy Deployer + Deployer deployer = new Deployer(deployerAddr, Authority(address(0))); + + // 2. Deploy RolesAuthority + RolesAuthority auth = new RolesAuthority( + deployerAddr, + Authority(address(0)) + ); + + // 3. Link them + deployer.setAuthority(auth); + + // 4. Grant permissions + auth.setRoleCapability( + Constants.DEPLOYER_ROLE, + address(deployer), + Deployer.deployContract.selector, + true + ); + auth.setUserRole(deployerAddr, Constants.DEPLOYER_ROLE, true); + + vm.stopBroadcast(); + + console.log("Deployer deployed at:", address(deployer)); + console.log("RolesAuthority deployed at:", address(auth)); + } +} diff --git a/script/02_DeployMaxiYieldVault.s.sol b/script/02_DeployMaxiYieldVault.s.sol new file mode 100644 index 000000000..b01706cd5 --- /dev/null +++ b/script/02_DeployMaxiYieldVault.s.sol @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +import {BoringVault} from "src/base/BoringVault.sol"; +import { + AccountantWithRateProviders +} from "src/base/Roles/AccountantWithRateProviders.sol"; +import { + TellerWithMultiAssetSupport +} from "src/base/Roles/TellerWithMultiAssetSupport.sol"; +import {DelayedWithdraw} from "src/base/Roles/DelayedWithdraw.sol"; +import { + RolesAuthority, + Authority +} from "@solmate/auth/authorities/RolesAuthority.sol"; +import {Deployer} from "src/helper/Deployer.sol"; +import {ERC20} from "@solmate/tokens/ERC20.sol"; +import "forge-std/Script.sol"; +import {MantraConstants as Constants} from "./00_MantraConstants.sol"; + +contract DeployMaxiYieldVault is Script { + address public deployerAddr = vm.envAddress("DEPLOYER_CONTRACT_ADDRESS"); + address public rolesAuthAddr = vm.envAddress("ROLES_AUTH_CONTRACT_ADDRESS"); + + // forge script script/02_DeployMaxiYieldVault.s.sol \ + // --rpc-url mantra_dukong \ + // --broadcast \ + // --chain-id 5887 \ + // --legacy \ + // --skip-simulation \ + // -vvvv + + function run() external { + string memory forkName = vm.envOr("MANTRA_MAINNET", false) + ? "mantra" + : "mantra_dukong"; + vm.createSelectFork(forkName); + uint256 deployerKey = vm.envUint("MANTRA_DEPLOYER"); + address owner = vm.addr(deployerKey); + + // Environment Toggle + bool isMainnet = vm.envOr("MANTRA_MAINNET", false); + address mUSD = isMainnet + ? Constants.mUSD_MAINNET + : Constants.mUSD_TESTNET; + address WETH = isMainnet + ? Constants.WETH_MAINNET + : Constants.WETH_TESTNET; + + Deployer deployer = Deployer(deployerAddr); + RolesAuthority auth = RolesAuthority(rolesAuthAddr); + + vm.startBroadcast(deployerKey); + + // --- 1. Deploy Core Components --- + address vault = deployer.deployContract( + Constants.RFR_VAULT_NAME, + type(BoringVault).creationCode, + abi.encode( + owner, + Constants.RFR_TOKEN_NAME, + Constants.RFR_SYMBOL, + Constants.RFR_DECIMALS + ), + 0 + ); + + address accountant = deployer.deployContract( + Constants.RFR_ACCOUNTANT_NAME, + type(AccountantWithRateProviders).creationCode, + abi.encode( + owner, // owner + vault, // vault + owner, // payoutAddress + Constants.ACCOUNTANT_STARTING_EXCHANGE_RATE, // startingExchangeRate + mUSD, // base (Dynamic) + Constants.ACCOUNTANT_ALLOWED_EXCHANGE_RATE_CHANGE_UPPER, + Constants.ACCOUNTANT_ALLOWED_EXCHANGE_RATE_CHANGE_LOWER, + Constants.ACCOUNTANT_MINIMUM_UPDATE_DELAY, + Constants.ACCOUNTANT_PLATFORM_FEE, + Constants.ACCOUNTANT_PERFORMANCE_FEE + ), + 0 + ); + + address teller = deployer.deployContract( + Constants.RFR_TELLER_NAME, + type(TellerWithMultiAssetSupport).creationCode, + abi.encode(owner, vault, accountant, WETH), // WETH (Dynamic) + 0 + ); + + address delayedWithdraw = deployer.deployContract( + Constants.RFR_DW_NAME, + type(DelayedWithdraw).creationCode, + abi.encode(owner, vault, accountant, owner), + 0 + ); + + // --- 2. Post-Deployment Setup --- + + // Roles Permissions + BoringVault(payable(vault)).setAuthority(auth); + AccountantWithRateProviders(accountant).setAuthority(auth); + TellerWithMultiAssetSupport(payable(teller)).setAuthority(auth); + DelayedWithdraw(delayedWithdraw).setAuthority(auth); + + // --- Role Configuration --- + address rateUpdater = isMainnet + ? Constants.RATE_UPDATER_MAINNET + : Constants.RATE_UPDATER_TESTNET; + address manager = isMainnet + ? Constants.MANAGER_MAINNET + : Constants.MANAGER_TESTNET; + + // Fallback to Owner if not set (for safety/testing) + if (rateUpdater == address(0)) rateUpdater = owner; + if (manager == address(0)) manager = owner; + + // Grant Roles + + // MINTER_ROLE (Teller) + auth.setRoleCapability( + Constants.MINTER_ROLE, + vault, + BoringVault.enter.selector, + true + ); + auth.setUserRole(teller, Constants.MINTER_ROLE, true); + + // BURNER_ROLE (DelayedWithdraw, BoringVault) + // exit.selector allows DW to burn shares and trigger the Vault to send underlying assets directly to the user + auth.setRoleCapability( + Constants.BURNER_ROLE, + vault, + BoringVault.exit.selector, + true + ); + auth.setUserRole(delayedWithdraw, Constants.BURNER_ROLE, true); + + // UPDATE_EXCHANGE_RATE_ROLE (Rate Updater) + auth.setRoleCapability( + Constants.UPDATE_EXCHANGE_RATE_ROLE, + accountant, + AccountantWithRateProviders.updateExchangeRate.selector, + true + ); + auth.setUserRole( + rateUpdater, + Constants.UPDATE_EXCHANGE_RATE_ROLE, + true + ); + + // MANAGER_ROLE (Manager) + auth.setUserRole(manager, Constants.MANAGER_ROLE, true); + + // OWNER_ROLE (Owner - kept as deployer/owner for now) + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.setShareLockPeriod.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.updateAssetData.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.setupWithdrawAsset.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.setPullFundsFromVault.selector, + true + ); + + // --- OWNER_ROLE Maintenance & Fees --- + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.changeWithdrawFee.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.changeWithdrawDelay.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.changeCompletionWindow.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.changeMaxLoss.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.stopWithdrawalsInAsset.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.setFeeAddress.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.updatePlatformFee.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.updatePerformanceFee.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.updatePayoutAddress.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.updateDelay.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.updateUpper.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.updateLower.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.pause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.unpause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.resetHighwaterMark.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.pause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.unpause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.denyAll.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.allowAll.selector, + true + ); + auth.setUserRole(owner, Constants.OWNER_ROLE, true); + + // Public Capabilities + auth.setPublicCapability( + teller, + TellerWithMultiAssetSupport.deposit.selector, + true + ); + auth.setPublicCapability( + teller, + TellerWithMultiAssetSupport.depositWithPermit.selector, + true + ); + auth.setPublicCapability( + delayedWithdraw, + DelayedWithdraw.requestWithdraw.selector, + true + ); + auth.setPublicCapability( + delayedWithdraw, + DelayedWithdraw.completeWithdraw.selector, + true + ); + + // Logic Config + TellerWithMultiAssetSupport(payable(teller)).setShareLockPeriod( + Constants.TELLER_SHARE_LOCK_PERIOD + ); + TellerWithMultiAssetSupport(payable(teller)).updateAssetData( + ERC20(mUSD), + true, // isSupported + true, // isDepositAsset + 0 // sharePremium + ); + + DelayedWithdraw(delayedWithdraw).setupWithdrawAsset( + ERC20(mUSD), + Constants.DW_WITHDRAW_DELAY, + Constants.DW_COMPLETION_WINDOW, + Constants.DW_WITHDRAW_FEE, + Constants.DW_MAX_LOSS + ); + DelayedWithdraw(delayedWithdraw).setPullFundsFromVault(true); + + BoringVault(payable(vault)).setBeforeTransferHook(teller); + + // --- Ownership Transfer --- + // 1. Identify Target Owner + address mantraOwner = vm.envOr("MANTRA_OWNER", address(0)); + if (mantraOwner == address(0)) { + mantraOwner = isMainnet + ? Constants.OWNER_MAINNET + : Constants.OWNER_TESTNET; + } + + // 2. Perform Transfer if valid and different from deployer + if (mantraOwner != address(0) && mantraOwner != owner) { + console.log("Transferring ownership to:", mantraOwner); + + // Grant OWNER_ROLE to new owner first + if (!auth.doesUserHaveRole(mantraOwner, Constants.OWNER_ROLE)) { + auth.setUserRole(mantraOwner, Constants.OWNER_ROLE, true); + } + + // Transfer Auth Ownership of components + if (BoringVault(payable(vault)).owner() != mantraOwner) + BoringVault(payable(vault)).transferOwnership(mantraOwner); + if (AccountantWithRateProviders(accountant).owner() != mantraOwner) + AccountantWithRateProviders(accountant).transferOwnership( + mantraOwner + ); + if ( + TellerWithMultiAssetSupport(payable(teller)).owner() != + mantraOwner + ) + TellerWithMultiAssetSupport(payable(teller)).transferOwnership( + mantraOwner + ); + if (DelayedWithdraw(delayedWithdraw).owner() != mantraOwner) + DelayedWithdraw(delayedWithdraw).transferOwnership(mantraOwner); + + // Revoke Roles from Deployer + // if (auth.doesUserHaveRole(owner, Constants.OWNER_ROLE)) { + // auth.setUserRole(owner, Constants.OWNER_ROLE, false); + // console.log("Revoked OWNER_ROLE from deployer"); + // } + } + + vm.stopBroadcast(); + + console.log("Environment:", isMainnet ? "Mainnet" : "Testnet"); + console.log("Yield Vault:", vault); + console.log("Yield Accountant:", accountant); + console.log("Yield Teller:", teller); + console.log("Yield DelayedWithdraw:", delayedWithdraw); + } +} diff --git a/script/03_DeployPointsVault.s.sol b/script/03_DeployPointsVault.s.sol new file mode 100644 index 000000000..98fa1d337 --- /dev/null +++ b/script/03_DeployPointsVault.s.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +import {BoringVault} from "src/base/BoringVault.sol"; +import { + AccountantWithRateProviders +} from "src/base/Roles/AccountantWithRateProviders.sol"; +import { + AccountantWithFixedRate +} from "src/base/Roles/AccountantWithFixedRate.sol"; +import { + TellerWithMultiAssetSupport +} from "src/base/Roles/TellerWithMultiAssetSupport.sol"; +import {DelayedWithdraw} from "src/base/Roles/DelayedWithdraw.sol"; +import { + RolesAuthority, + Authority +} from "@solmate/auth/authorities/RolesAuthority.sol"; +import {Deployer} from "src/helper/Deployer.sol"; +import {ERC20} from "@solmate/tokens/ERC20.sol"; +import "forge-std/Script.sol"; +import {MantraConstants as Constants} from "./00_MantraConstants.sol"; + +// forge script script/03_DeployPointsVault.s.sol \ +// --rpc-url mantra_dukong \ +// --broadcast \ +// --chain-id 5887 \ +// --legacy \ +// --skip-simulation \ +// -vvvv + +contract DeployPointsVault is Script { + address public deployerAddr = vm.envAddress("DEPLOYER_CONTRACT_ADDRESS"); + address public rolesAuthAddr = vm.envAddress("ROLES_AUTH_CONTRACT_ADDRESS"); + + function run() external { + string memory forkName = vm.envOr("MANTRA_MAINNET", false) + ? "mantra" + : "mantra_dukong"; + vm.createSelectFork(forkName); + uint256 deployerKey = vm.envUint("MANTRA_DEPLOYER"); + address owner = vm.addr(deployerKey); + + // Environment Toggle + bool isMainnet = vm.envOr("MANTRA_MAINNET", false); + address mUSD = isMainnet + ? Constants.mUSD_MAINNET + : Constants.mUSD_TESTNET; + address WETH = isMainnet + ? Constants.WETH_MAINNET + : Constants.WETH_TESTNET; + + Deployer deployer = Deployer(deployerAddr); + RolesAuthority auth = RolesAuthority(rolesAuthAddr); + + vm.startBroadcast(deployerKey); + + // --- 1. Deploy Core Components --- + address vault = deployer.deployContract( + Constants.POINTS_VAULT_NAME, + type(BoringVault).creationCode, + abi.encode( + owner, + Constants.POINTS_TOKEN_NAME, + Constants.POINTS_SYMBOL, + Constants.POINTS_DECIMALS + ), + 0 + ); + + address accountant = deployer.deployContract( + Constants.POINTS_ACCOUNTANT_NAME, + type(AccountantWithFixedRate).creationCode, + abi.encode( + owner, // owner + vault, // vault + owner, // payoutAddress + Constants.ACCOUNTANT_STARTING_EXCHANGE_RATE, // startingExchangeRate + mUSD, // base (Dynamic) + Constants.ACCOUNTANT_ALLOWED_EXCHANGE_RATE_CHANGE_UPPER, + Constants.ACCOUNTANT_ALLOWED_EXCHANGE_RATE_CHANGE_LOWER, + Constants.ACCOUNTANT_MINIMUM_UPDATE_DELAY, + Constants.ACCOUNTANT_PLATFORM_FEE, + Constants.ACCOUNTANT_PERFORMANCE_FEE + ), + 0 + ); + + address teller = deployer.deployContract( + Constants.POINTS_TELLER_NAME, + type(TellerWithMultiAssetSupport).creationCode, + abi.encode(owner, vault, accountant, WETH), // WETH (Dynamic) + 0 + ); + + address delayedWithdraw = deployer.deployContract( + Constants.POINTS_DW_NAME, + type(DelayedWithdraw).creationCode, + abi.encode(owner, vault, accountant, owner), + 0 + ); + + // --- 2. Post-Deployment Setup --- + + // Roles Permissions + BoringVault(payable(vault)).setAuthority(auth); + AccountantWithFixedRate(accountant).setAuthority(auth); + TellerWithMultiAssetSupport(payable(teller)).setAuthority(auth); + DelayedWithdraw(delayedWithdraw).setAuthority(auth); + + // --- Role Configuration --- + address rateUpdater = isMainnet + ? Constants.RATE_UPDATER_MAINNET + : Constants.RATE_UPDATER_TESTNET; + address manager = isMainnet + ? Constants.MANAGER_MAINNET + : Constants.MANAGER_TESTNET; + + // Fallback to Owner if not set (for safety/testing) + if (rateUpdater == address(0)) rateUpdater = owner; + if (manager == address(0)) manager = owner; + + // Grant Roles + + // MINTER_ROLE (Teller) + auth.setRoleCapability( + Constants.MINTER_ROLE, + vault, + BoringVault.enter.selector, + true + ); + auth.setUserRole(teller, Constants.MINTER_ROLE, true); + + // BURNER_ROLE (DelayedWithdraw, BoringVault) + auth.setRoleCapability( + Constants.BURNER_ROLE, + vault, + BoringVault.exit.selector, + true + ); + auth.setUserRole(delayedWithdraw, Constants.BURNER_ROLE, true); + + // UPDATE_EXCHANGE_RATE_ROLE (Rate Updater) + auth.setRoleCapability( + Constants.UPDATE_EXCHANGE_RATE_ROLE, + accountant, + AccountantWithRateProviders.updateExchangeRate.selector, + true + ); + auth.setUserRole( + rateUpdater, + Constants.UPDATE_EXCHANGE_RATE_ROLE, + true + ); + + // MANAGER_ROLE (Manager) + auth.setUserRole(manager, Constants.MANAGER_ROLE, true); + + // OWNER_ROLE (Owner - kept as deployer/owner for now) + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.setShareLockPeriod.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.updateAssetData.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.setupWithdrawAsset.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.setPullFundsFromVault.selector, + true + ); + + // --- OWNER_ROLE Maintenance & Fees --- + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.changeWithdrawFee.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.changeWithdrawDelay.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.changeCompletionWindow.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.changeMaxLoss.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + delayedWithdraw, + DelayedWithdraw.setFeeAddress.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.updatePayoutAddress.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.pause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithRateProviders.unpause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + accountant, + AccountantWithFixedRate.setYieldDistributor.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.pause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.unpause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.denyAll.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + teller, + TellerWithMultiAssetSupport.allowAll.selector, + true + ); + auth.setUserRole(owner, Constants.OWNER_ROLE, true); + + // Public Capabilities + auth.setPublicCapability( + teller, + TellerWithMultiAssetSupport.deposit.selector, + true + ); + auth.setPublicCapability( + delayedWithdraw, + DelayedWithdraw.requestWithdraw.selector, + true + ); + auth.setPublicCapability( + delayedWithdraw, + DelayedWithdraw.completeWithdraw.selector, + true + ); + + // Logic Config + TellerWithMultiAssetSupport(payable(teller)).setShareLockPeriod( + Constants.TELLER_SHARE_LOCK_PERIOD + ); + TellerWithMultiAssetSupport(payable(teller)).updateAssetData( + ERC20(mUSD), + true, + true, + 0 + ); + + DelayedWithdraw(delayedWithdraw).setupWithdrawAsset( + ERC20(mUSD), + Constants.DW_WITHDRAW_DELAY, + Constants.DW_COMPLETION_WINDOW, + Constants.DW_WITHDRAW_FEE, + Constants.DW_MAX_LOSS + ); + DelayedWithdraw(delayedWithdraw).setPullFundsFromVault(true); + + BoringVault(payable(vault)).setBeforeTransferHook(teller); + + // --- Ownership Transfer --- + // 1. Identify Target Owner + address mantraOwner = vm.envOr("MANTRA_OWNER", address(0)); + if (mantraOwner == address(0)) { + mantraOwner = isMainnet + ? Constants.OWNER_MAINNET + : Constants.OWNER_TESTNET; + } + + // 2. Perform Transfer if valid and different from deployer + if (mantraOwner != address(0) && mantraOwner != owner) { + console.log("Transferring ownership to:", mantraOwner); + + // Grant OWNER_ROLE to new owner first + if (!auth.doesUserHaveRole(mantraOwner, Constants.OWNER_ROLE)) { + auth.setUserRole(mantraOwner, Constants.OWNER_ROLE, true); + } + + // Transfer Auth Ownership of components + if (BoringVault(payable(vault)).owner() != mantraOwner) + BoringVault(payable(vault)).transferOwnership(mantraOwner); + if (AccountantWithFixedRate(accountant).owner() != mantraOwner) + AccountantWithFixedRate(accountant).transferOwnership( + mantraOwner + ); + if ( + TellerWithMultiAssetSupport(payable(teller)).owner() != + mantraOwner + ) + TellerWithMultiAssetSupport(payable(teller)).transferOwnership( + mantraOwner + ); + if (DelayedWithdraw(delayedWithdraw).owner() != mantraOwner) + DelayedWithdraw(delayedWithdraw).transferOwnership(mantraOwner); + + // Revoke Roles from Deployer + if (auth.doesUserHaveRole(owner, Constants.OWNER_ROLE)) { + auth.setUserRole(owner, Constants.OWNER_ROLE, false); + console.log("Revoked OWNER_ROLE from deployer"); + } + } + + vm.stopBroadcast(); + + console.log("Environment:", isMainnet ? "Mainnet" : "Testnet"); + console.log("Points Vault:", vault); + console.log("Points Accountant (Fixed):", accountant); + console.log("Points Teller:", teller); + console.log("Points DelayedWithdraw:", delayedWithdraw); + } +} diff --git a/script/04_DeployArcticLens.s.sol b/script/04_DeployArcticLens.s.sol new file mode 100644 index 000000000..0f160c8cd --- /dev/null +++ b/script/04_DeployArcticLens.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +import {ArcticArchitectureLens} from "src/helper/ArcticArchitectureLens.sol"; +import {Deployer} from "src/helper/Deployer.sol"; +import "forge-std/Script.sol"; +import {MantraConstants as Constants} from "./00_MantraConstants.sol"; + +contract DeployArcticLens is Script { + address public deployerAddr = vm.envAddress("DEPLOYER_CONTRACT_ADDRESS"); + + function run() external { + string memory forkName = vm.envOr("MANTRA_MAINNET", false) + ? "mantra" + : "mantra_dukong"; + vm.createSelectFork(forkName); + uint256 deployerKey = vm.envUint("MANTRA_DEPLOYER"); + Deployer deployer = Deployer(deployerAddr); + + vm.startBroadcast(deployerKey); + + // Deploy Lens via Deployer using Constants name + address lens = deployer.deployContract( + Constants.ARCTIC_LENS_NAME, + type(ArcticArchitectureLens).creationCode, + hex"", + 0 + ); + + vm.stopBroadcast(); + + console.log("Lens deployed at:", lens); + } +} diff --git a/test/MantraPermissionsVerification.t.sol b/test/MantraPermissionsVerification.t.sol new file mode 100644 index 000000000..51a336d42 --- /dev/null +++ b/test/MantraPermissionsVerification.t.sol @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +import {Test, console} from "forge-std/Test.sol"; +import {BoringVault} from "src/base/BoringVault.sol"; +import { + AccountantWithRateProviders +} from "src/base/Roles/AccountantWithRateProviders.sol"; +import { + AccountantWithFixedRate +} from "src/base/Roles/AccountantWithFixedRate.sol"; +import { + TellerWithMultiAssetSupport +} from "src/base/Roles/TellerWithMultiAssetSupport.sol"; +import {DelayedWithdraw} from "src/base/Roles/DelayedWithdraw.sol"; +import { + RolesAuthority, + Authority +} from "@solmate/auth/authorities/RolesAuthority.sol"; +import {Deployer} from "src/helper/Deployer.sol"; +import {ERC20} from "@solmate/tokens/ERC20.sol"; +import {MantraConstants as Constants} from "script/00_MantraConstants.sol"; + +contract MockERC20 is ERC20 { + constructor( + string memory name, + string memory symbol, + uint8 decimals + ) ERC20(name, symbol, decimals) {} + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} + +contract MantraPermissionsVerificationTest is Test { + RolesAuthority public auth; + Deployer public deployer; + + BoringVault public maxiVault; + AccountantWithRateProviders public maxiAccountant; + TellerWithMultiAssetSupport public maxiTeller; + DelayedWithdraw public maxiDelayedWithdraw; + + BoringVault public pointsVault; + AccountantWithFixedRate public pointsAccountant; + TellerWithMultiAssetSupport public pointsTeller; + DelayedWithdraw public pointsDelayedWithdraw; + + address public contractOwner = address(0xDE1); + address public roleHolder = address(0xDE2); + address public manager = address(0xDE3); + address public rateUpdater = address(0xDE4); + address public payoutAddress = address(0xDE5); + + MockERC20 public mUSD; + + function setUp() public { + // Deploy mock token first + mUSD = new MockERC20("Mock USD", "mUSD", 6); + + // We mock a deployment state that mirrors what the scripts do + vm.startPrank(contractOwner); + + auth = new RolesAuthority(contractOwner, Authority(address(0))); + deployer = new Deployer(contractOwner, auth); + + // Grant owner permission to deploy + auth.setRoleCapability( + 1, + address(deployer), + Deployer.deployContract.selector, + true + ); + auth.setUserRole(contractOwner, 1, true); + + // Deploy Maxi + maxiVault = BoringVault( + payable( + deployer.deployContract( + "Maxi", + type(BoringVault).creationCode, + abi.encode(contractOwner, "M", "S", 6), + 0 + ) + ) + ); + maxiAccountant = AccountantWithRateProviders( + deployer.deployContract( + "MaxiAcc", + type(AccountantWithRateProviders).creationCode, + abi.encode( + contractOwner, + address(maxiVault), + payoutAddress, + 1e6, + address(mUSD), + 1.5e4, + 0.5e4, + 20 hours, + 0, + 0 + ), + 0 + ) + ); + maxiTeller = TellerWithMultiAssetSupport( + payable( + deployer.deployContract( + "MaxiTel", + type(TellerWithMultiAssetSupport).creationCode, + abi.encode( + contractOwner, + address(maxiVault), + address(maxiAccountant), + address(0) + ), + 0 + ) + ) + ); + maxiDelayedWithdraw = DelayedWithdraw( + deployer.deployContract( + "MaxiDW", + type(DelayedWithdraw).creationCode, + abi.encode( + contractOwner, + address(maxiVault), + address(maxiAccountant), + contractOwner + ), + 0 + ) + ); + + // Deploy Points + pointsVault = BoringVault( + payable( + deployer.deployContract( + "Points", + type(BoringVault).creationCode, + abi.encode(contractOwner, "P", "S", 6), + 0 + ) + ) + ); + pointsAccountant = AccountantWithFixedRate( + deployer.deployContract( + "PointsAcc", + type(AccountantWithFixedRate).creationCode, + abi.encode( + contractOwner, + address(pointsVault), + contractOwner, + 1e6, + address(mUSD), + 1.5e4, + 0.5e4, + 20 hours, + 0, + 0 + ), + 0 + ) + ); + pointsTeller = TellerWithMultiAssetSupport( + payable( + deployer.deployContract( + "PointsTel", + type(TellerWithMultiAssetSupport).creationCode, + abi.encode( + contractOwner, + address(pointsVault), + address(pointsAccountant), + address(0) + ), + 0 + ) + ) + ); + pointsDelayedWithdraw = DelayedWithdraw( + deployer.deployContract( + "PointsDW", + type(DelayedWithdraw).creationCode, + abi.encode( + contractOwner, + address(pointsVault), + address(pointsAccountant), + contractOwner + ), + 0 + ) + ); + + // Apply same authority to all + maxiVault.setAuthority(auth); + maxiAccountant.setAuthority(auth); + maxiTeller.setAuthority(auth); + maxiDelayedWithdraw.setAuthority(auth); + + pointsVault.setAuthority(auth); + pointsAccountant.setAuthority(auth); + pointsTeller.setAuthority(auth); + pointsDelayedWithdraw.setAuthority(auth); + + // Apply Role Setup (Mirrored from scripts) + _applyRoleSetup(); + + vm.stopPrank(); + } + + function _applyRoleSetup() internal { + // MINTER_ROLE (2) + auth.setRoleCapability( + Constants.MINTER_ROLE, + address(maxiVault), + BoringVault.enter.selector, + true + ); + auth.setUserRole(address(maxiTeller), Constants.MINTER_ROLE, true); + + // BURNER_ROLE (3) + auth.setRoleCapability( + Constants.BURNER_ROLE, + address(maxiVault), + BoringVault.exit.selector, + true + ); + auth.setUserRole( + address(maxiDelayedWithdraw), + Constants.BURNER_ROLE, + true + ); + + // OWNER_ROLE (8) - The new capabilities we added + // Maxi Accountant + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiAccountant), + AccountantWithRateProviders.updatePlatformFee.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiAccountant), + AccountantWithRateProviders.updatePerformanceFee.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiAccountant), + AccountantWithRateProviders.updatePayoutAddress.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiAccountant), + AccountantWithRateProviders.unpause.selector, + true + ); + + // Points Accountant + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(pointsAccountant), + AccountantWithRateProviders.updatePayoutAddress.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(pointsAccountant), + AccountantWithFixedRate.setYieldDistributor.selector, + true + ); + + // DelayedWithdraw + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiDelayedWithdraw), + DelayedWithdraw.setupWithdrawAsset.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiDelayedWithdraw), + DelayedWithdraw.changeWithdrawFee.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(pointsDelayedWithdraw), + DelayedWithdraw.setupWithdrawAsset.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(pointsDelayedWithdraw), + DelayedWithdraw.changeWithdrawFee.selector, + true + ); + + // Teller + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiTeller), + TellerWithMultiAssetSupport.setShareLockPeriod.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiTeller), + TellerWithMultiAssetSupport.updateAssetData.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiTeller), + TellerWithMultiAssetSupport.pause.selector, + true + ); + auth.setRoleCapability( + Constants.OWNER_ROLE, + address(maxiTeller), + TellerWithMultiAssetSupport.denyAll.selector, + true + ); + + auth.setUserRole(roleHolder, Constants.OWNER_ROLE, true); + + // MANAGER_ROLE (1) + auth.setUserRole(manager, Constants.MANAGER_ROLE, true); + + // Grant MANAGER_ROLE manage capability on Vault + auth.setRoleCapability( + Constants.MANAGER_ROLE, + address(maxiVault), + bytes4(keccak256("manage(address,bytes,uint256)")), + true + ); + auth.setRoleCapability( + Constants.MANAGER_ROLE, + address(maxiVault), + bytes4(keccak256("manage(address[],bytes[],uint256[])")), + true + ); + + // UPDATE_EXCHANGE_RATE_ROLE (11) + auth.setRoleCapability( + Constants.UPDATE_EXCHANGE_RATE_ROLE, + address(maxiAccountant), + AccountantWithRateProviders.updateExchangeRate.selector, + true + ); + auth.setUserRole( + rateUpdater, + Constants.UPDATE_EXCHANGE_RATE_ROLE, + true + ); + } + + // --- Tests --- + + function testOwnerCanChangeFees() public { + vm.startPrank(roleHolder); + + // 1. Verify Maxi Performance Fee change + maxiAccountant.updatePerformanceFee(25); // 25 bps + (, , , , , , , , , , , uint16 performanceFee) = maxiAccountant + .accountantState(); + assertEq(performanceFee, 25, "Maxi Performance fee should be 25 bps"); + + // 2. Verify Points Performance Fee change (SHOULD REVERT now) + vm.expectRevert(); + pointsAccountant.updatePerformanceFee(25); + + // 3. Verify Withdraw Fee change + // First setup asset + maxiDelayedWithdraw.setupWithdrawAsset( + ERC20(address(mUSD)), + 0, + 7 days, + 0, + 100 + ); + + maxiDelayedWithdraw.changeWithdrawFee(ERC20(address(mUSD)), 50); // 50 bps + (, , , , uint16 withdrawFee, ) = maxiDelayedWithdraw.withdrawAssets( + ERC20(address(mUSD)) + ); + assertEq(withdrawFee, 50, "Withdraw fee should be 50 bps"); + + vm.stopPrank(); + } + + function testManagerPermissions() public { + vm.startPrank(manager); + + // Manager has Role ID 1, which in this system is also the DEPLOYER_ROLE. + // We should verify that manager CANNOT change fees (unless explicitly granted). + + vm.expectRevert(); + maxiAccountant.updatePerformanceFee(100); + + vm.stopPrank(); + } + + function testUnprivilegedUserCannotChangeFees() public { + address hacker = address(0xBAD); + vm.startPrank(hacker); + + vm.expectRevert(); + maxiAccountant.updatePerformanceFee(100); + + vm.stopPrank(); + } + + function testFeeWithdrawalFlow() public { + // 1. Setup Performance Fee (25 bps) + vm.prank(roleHolder); + maxiAccountant.updatePerformanceFee(25); + + // 2. Accumulate Fees (Simulate logic: rate goes from 1.0 to 1.1) + // Give Vault some mUSD to pay fees + mUSD.mint(address(maxiVault), 1000e6); + + // Mint some shares so total supply > 0 + mUSD.mint(address(this), 1000e6); + mUSD.approve(address(maxiVault), 1000e6); + vm.prank(address(maxiTeller)); + maxiVault.enter(address(this), mUSD, 1000e6, address(this), 1000e6); + + // Warp to avoid update delay pause + vm.warp(block.timestamp + 1 days); + + // Checkpoint shares by calling updateExchangeRate once with current rate + vm.prank(rateUpdater); + maxiAccountant.updateExchangeRate(1.0e6); + + // Update exchange rate to 1.1e6 after another 1 day + vm.warp(block.timestamp + 1 days); + vm.prank(rateUpdater); + maxiAccountant.updateExchangeRate(1.1e6); + + // If the accountant paused (auto-pause), unpause it (Owner has unpause rights) + (, , , , , bool isPaused, , , , , , , , ) = maxiAccountant + .accountantState(); + if (isPaused) { + vm.prank(roleHolder); + maxiAccountant.unpause(); + } + + // Check fees owed + (, , uint128 feesOwedInBase, , , , , , , , , ) = maxiAccountant + .accountantState(); + assertTrue(feesOwedInBase > 0, "Fees should have accumulated"); + + // 3. Manager Withdrawal Flow + vm.startPrank(manager); + + // A. Approve Accountant to take fees from Vault + // Data for: mUSD.approve(accountant, type(uint256).max) + bytes memory approveData = abi.encodeWithSelector( + ERC20.approve.selector, + address(maxiAccountant), + type(uint256).max + ); + maxiVault.manage(address(mUSD), approveData, 0); + + // B. Claim Fees + // Data for: accountant.claimFees(mUSD) + bytes memory claimData = abi.encodeWithSelector( + AccountantWithRateProviders.claimFees.selector, + address(mUSD) + ); + maxiVault.manage(address(maxiAccountant), claimData, 0); + + vm.stopPrank(); + + // 4. Verify Payout + uint256 payoutBalance = mUSD.balanceOf(payoutAddress); + assertTrue( + payoutBalance > 0, + "Payout address should have received fees" + ); + assertEq( + payoutBalance, + uint256(feesOwedInBase), + "Payout should match fees owed" + ); + } +} diff --git a/test/MantraVaultsIntegration.t.sol b/test/MantraVaultsIntegration.t.sol new file mode 100644 index 000000000..7057a9e78 --- /dev/null +++ b/test/MantraVaultsIntegration.t.sol @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +import {Test, console} from "forge-std/Test.sol"; +import {BoringVault} from "src/base/BoringVault.sol"; +import { + AccountantWithRateProviders +} from "src/base/Roles/AccountantWithRateProviders.sol"; +import { + AccountantWithFixedRate +} from "src/base/Roles/AccountantWithFixedRate.sol"; +import { + TellerWithMultiAssetSupport +} from "src/base/Roles/TellerWithMultiAssetSupport.sol"; +import {DelayedWithdraw} from "src/base/Roles/DelayedWithdraw.sol"; +import { + RolesAuthority, + Authority +} from "@solmate/auth/authorities/RolesAuthority.sol"; +import {Deployer} from "src/helper/Deployer.sol"; +import {ArcticArchitectureLens} from "src/helper/ArcticArchitectureLens.sol"; +import {ERC20} from "@solmate/tokens/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor( + string memory name, + string memory symbol, + uint8 decimals + ) ERC20(name, symbol, decimals) {} + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} + +contract MantraVaultsIntegrationTest is Test { + // --- State Variables --- + MockERC20 public mUSD; + Deployer public deployer; + RolesAuthority public auth; + ArcticArchitectureLens public lens; + + // Yield Vault components + BoringVault public yieldVault; + AccountantWithRateProviders public yieldAccountant; + TellerWithMultiAssetSupport public yieldTeller; + DelayedWithdraw public yieldDelayedWithdraw; + + // Points Vault components + BoringVault public pointsVault; + AccountantWithFixedRate public pointsAccountant; + TellerWithMultiAssetSupport public pointsTeller; + DelayedWithdraw public pointsDelayedWithdraw; + + address public owner = address(0xDE1); + address public userA = address(0xA11ce); + + // Standard Project Roles + uint8 public constant MANAGER_ROLE = 1; + uint8 public constant MINTER_ROLE = 2; + uint8 public constant BURNER_ROLE = 3; + uint8 public constant OWNER_ROLE = 8; + uint8 public constant MULTISIG_ROLE = 9; + uint8 public constant UPDATE_EXCHANGE_RATE_ROLE = 11; + + // --- Setup --- + function setUp() public { + vm.startPrank(owner); + + // 1. Setup Mock Token + mUSD = new MockERC20("Mock USD", "mUSD", 6); + deal(address(mUSD), userA, 1000e6); + + // 2. Deploy Infrastructure + deployer = new Deployer(owner, Authority(address(0))); + auth = new RolesAuthority(owner, Authority(address(0))); + deployer.setAuthority(auth); + + // Grant owner Deployer role (Role 1) + auth.setRoleCapability( + 1, + address(deployer), + Deployer.deployContract.selector, + true + ); + auth.setUserRole(owner, 1, true); + + // 3. Deploy Yield Vault via Deployer + yieldVault = BoringVault( + payable( + deployer.deployContract( + "Maxi Yield Vault V1.0", + type(BoringVault).creationCode, + abi.encode(owner, "Maxi Yield mUSD", "my-mUSD", 6), + 0 + ) + ) + ); + + yieldAccountant = AccountantWithRateProviders( + deployer.deployContract( + "Maxi Yield Accountant V1.0", + type(AccountantWithRateProviders).creationCode, + abi.encode( + owner, + address(yieldVault), + owner, + 1e6, + address(mUSD), + 1.5e4, + 0.5e4, + 20 hours, + 0, + 0 + ), + 0 + ) + ); + + yieldTeller = TellerWithMultiAssetSupport( + payable( + deployer.deployContract( + "Maxi Yield Teller V1.0", + type(TellerWithMultiAssetSupport).creationCode, + abi.encode( + owner, + address(yieldVault), + address(yieldAccountant), + address(0) + ), + 0 + ) + ) + ); + + yieldDelayedWithdraw = DelayedWithdraw( + deployer.deployContract( + "Maxi Yield DelayedWithdraw V1.0", + type(DelayedWithdraw).creationCode, + abi.encode( + owner, + address(yieldVault), + address(yieldAccountant), + owner + ), + 0 + ) + ); + + // 4. Deploy Points Vault via Deployer + pointsVault = BoringVault( + payable( + deployer.deployContract( + "Points Vault V1.0", + type(BoringVault).creationCode, + abi.encode(owner, "Points mUSD", "pts-mUSD", 6), + 0 + ) + ) + ); + + pointsAccountant = AccountantWithFixedRate( + deployer.deployContract( + "Points Accountant V1.0", + type(AccountantWithFixedRate).creationCode, + abi.encode( + owner, + address(pointsVault), + owner, + 1e6, + address(mUSD), + 1.5e4, + 0.5e4, + 20 hours, + 0, + 0 + ), + 0 + ) + ); + + pointsTeller = TellerWithMultiAssetSupport( + payable( + deployer.deployContract( + "Points Teller V1.0", + type(TellerWithMultiAssetSupport).creationCode, + abi.encode( + owner, + address(pointsVault), + address(pointsAccountant), + address(0) + ), + 0 + ) + ) + ); + + pointsDelayedWithdraw = DelayedWithdraw( + deployer.deployContract( + "Points DelayedWithdraw V1.0", + type(DelayedWithdraw).creationCode, + abi.encode( + owner, + address(pointsVault), + address(pointsAccountant), + owner + ), + 0 + ) + ); + + // 5. Deploy Lens + lens = ArcticArchitectureLens( + deployer.deployContract( + "Arctic Architecture Lens V1.0", + type(ArcticArchitectureLens).creationCode, + hex"", + 0 + ) + ); + + // 6. Permissions & Config + _setupVault( + yieldVault, + yieldAccountant, + yieldTeller, + yieldDelayedWithdraw + ); + _setupVault( + pointsVault, + pointsAccountant, + pointsTeller, + pointsDelayedWithdraw + ); + + vm.stopPrank(); + } + + function _setupVault( + BoringVault v, + AccountantWithRateProviders a, + TellerWithMultiAssetSupport t, + DelayedWithdraw d + ) internal { + v.setAuthority(auth); + a.setAuthority(auth); + t.setAuthority(auth); + d.setAuthority(auth); + + // Minter Role (2) + auth.setRoleCapability( + MINTER_ROLE, + address(v), + BoringVault.enter.selector, + true + ); + auth.setUserRole(address(t), MINTER_ROLE, true); + + // Burner Role (3) + auth.setRoleCapability( + BURNER_ROLE, + address(v), + BoringVault.exit.selector, + true + ); + auth.setUserRole(address(d), BURNER_ROLE, true); + + // Update Rate Role (11) + auth.setRoleCapability( + UPDATE_EXCHANGE_RATE_ROLE, + address(a), + AccountantWithRateProviders.updateExchangeRate.selector, + true + ); + auth.setUserRole(owner, UPDATE_EXCHANGE_RATE_ROLE, true); + + // Owner Role (8) + auth.setRoleCapability( + OWNER_ROLE, + address(t), + TellerWithMultiAssetSupport.setShareLockPeriod.selector, + true + ); + auth.setUserRole(owner, OWNER_ROLE, true); + + // Public capabilities for UI interaction + auth.setPublicCapability( + address(t), + TellerWithMultiAssetSupport.deposit.selector, + true + ); + auth.setPublicCapability( + address(d), + DelayedWithdraw.requestWithdraw.selector, + true + ); + auth.setPublicCapability( + address(d), + DelayedWithdraw.completeWithdraw.selector, + true + ); + + t.setShareLockPeriod(24 hours); + t.updateAssetData(mUSD, true, true, 0); + d.setupWithdrawAsset(mUSD, 0, 7 days, 0, 100); + d.setPullFundsFromVault(true); + v.setBeforeTransferHook(address(t)); + } + + // --- Tests --- + + function testDeterministicDeployment() public { + assertEq( + address(yieldVault), + deployer.getAddress("Maxi Yield Vault V1.0") + ); + assertEq( + address(pointsVault), + deployer.getAddress("Points Vault V1.0") + ); + assertEq( + address(lens), + deployer.getAddress("Arctic Architecture Lens V1.0") + ); + } + + function testYieldVaultMechanism() public { + uint256 amount = 100e6; + vm.startPrank(userA); + mUSD.approve(address(yieldVault), amount); + yieldTeller.deposit(mUSD, amount, 0); + vm.stopPrank(); + + assertEq(lens.exchangeRate(yieldAccountant), 1e6); + + deal( + address(mUSD), + address(yieldVault), + mUSD.balanceOf(address(yieldVault)) + 10e6 + ); + vm.prank(owner); + yieldAccountant.updateExchangeRate(1_100_000); + + assertEq(lens.exchangeRate(yieldAccountant), 1_100_000); + assertEq( + lens.balanceOfInAssets(userA, yieldVault, yieldAccountant), + 110e6 + ); + } + + function testPointsVaultMechanism() public { + uint256 amount = 100e6; + vm.startPrank(userA); + mUSD.approve(address(pointsVault), amount); + pointsTeller.deposit(mUSD, amount, 0); + vm.stopPrank(); + + assertEq(lens.exchangeRate(pointsAccountant), 1e6); + + deal( + address(mUSD), + address(pointsVault), + mUSD.balanceOf(address(pointsVault)) + 10e6 + ); + vm.prank(owner); + pointsAccountant.updateExchangeRate(1_200_000); + + assertEq(lens.exchangeRate(pointsAccountant), 1_000_000); // Fixed at 1.0 + } + + function testShareLockEnforcement() public { + vm.startPrank(userA); + mUSD.approve(address(yieldVault), 10e6); + yieldTeller.deposit(mUSD, 10e6, 0); + + vm.expectRevert(); + yieldVault.transfer(address(0x1), 1e6); + + skip(24 hours + 1); + yieldVault.transfer(address(0x1), 1e6); + vm.stopPrank(); + } +} diff --git a/verify_dukong.sh b/verify_dukong.sh new file mode 100755 index 000000000..7eec93220 --- /dev/null +++ b/verify_dukong.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Configuration +RPC_URL="mantra_dukong" +VERIFIER="blockscout" +VERIFIER_URL="https://explorer.dukong.io/api" + +# Deployed Addresses +DEPLOYER="0xa54DD6f938EB7C6394BF10B06E267f10aA3fE2eF" +ROLES_AUTHORITY="0x921b20e49ec45B3441CF45e2BeccBEF33620AeE6" +LENS="0x0FDCf61ed820a7e323f74D83B0A2302beD5e29E0" + +# Owner (retrieved from broadcast logs) +OWNER="0x37723e376FdF70854665B5f1a5C49cB30E1691AC" + +echo "Verifying Deployer..." +forge verify-contract $DEPLOYER src/helper/Deployer.sol:Deployer \ + --constructor-args $(cast abi-encode "constructor(address,address)" $OWNER 0x0000000000000000000000000000000000000000) \ + --rpc-url $RPC_URL \ + --verifier $VERIFIER \ + --verifier-url $VERIFIER_URL + +echo "Verifying RolesAuthority..." +forge verify-contract $ROLES_AUTHORITY lib/solmate/src/auth/authorities/RolesAuthority.sol:RolesAuthority \ + --constructor-args $(cast abi-encode "constructor(address,address)" $OWNER 0x0000000000000000000000000000000000000000) \ + --rpc-url $RPC_URL \ + --verifier $VERIFIER \ + --verifier-url $VERIFIER_URL + +echo "Verifying ArcticArchitectureLens..." +forge verify-contract $LENS src/helper/ArcticArchitectureLens.sol:ArcticArchitectureLens \ + --rpc-url $RPC_URL \ + --verifier $VERIFIER \ + --verifier-url $VERIFIER_URL