Repo:
Multyr/multyr-core(public, BUSL-1.1) Chains: Arbitrum One (42161), Base (8453), Ethereum Mainnet (1) — resolved at runtime frommultyr-core/script/config/ChainConfig.sol, the single source of truth for chain-specific addresses/params. Deploying to an unsupported chain reverts. Modular Path B: canonical reference perdocs/09-audit/deployment-flow.md §147-242Last updated: 2026-08-21 · multi-chain config added
multyr-core deploys the Multyr vault protocol core: an ERC-4626-compatible, module-routing
vault supporting Open-Ended (OE) and Fixed-Maturity (FM) vault modes. All scripts resolve
ChainConfig.current() from block.chainid (Arbitrum One, Base, or Ethereum Mainnet; anything
else reverts) and require a pre-deployed ROOT_TIMELOCK as final owner.
| Mode | Script | Notes |
|---|---|---|
| OE (Open-Ended) | multyr-core/script/DeployCoreSystem.s.sol |
Standard always-available deposits/withdrawals (queue-gated) |
| FM (Fixed-Maturity) | multyr-core/script/DeployFixedMaturityVault.s.sol |
Hard maturity timestamp, funding window, locked capital |
| Integrated (OE) | multyr-core/script/DeployCoreIntegrated.s.sol |
Single-command with Permit2, periphery entry points |
| Variable | Used by | Notes |
|---|---|---|
DEPLOYER_PRIVATE_KEY |
All scripts | EOA with ETH+USDC for gas + dead deposit |
GOVERNOR_ADDRESS |
multyr-core/script/DeployCoreSystem.s.sol:655 |
ROOT_TIMELOCK address (from Step 1) |
GUARDIAN_ADDRESS |
multyr-core/script/DeployCoreSystem.s.sol:656 |
SAFE_GUARDIAN multisig |
TREASURY_ADDRESS |
multyr-core/script/DeployCoreSystem.s.sol:657 |
Fee treasury wallet |
OPS_ADDRESS |
multyr-core/script/DeployCoreSystem.s.sol:658 |
Ops wallet |
SAFETY_RESERVE_ADDRESS |
multyr-core/script/DeployCoreSystem.s.sol:659 |
Safety reserve wallet |
CHAINLINK_USDC_FEED |
multyr-core/script/DeployCoreSystem.s.sol |
Optional; defaults to the current chain's feed in ChainConfig |
MORPHO_VAULT |
multyr-core/script/DeployCoreSystem.s.sol |
Required when DEPLOY_WARM_ADAPTERS=true on a chain with no vetted default (Base, Ethereum) |
| Variable | Default | Notes |
|---|---|---|
TIMELOCK_ADDRESS |
GOVERNOR_ADDRESS |
Explicit timelock if different from governor |
VETOER_ADDRESS |
address(0) |
Safe veto multisig |
DEPLOY_INCENTIVES |
false |
Deploy Incentives module inline |
DEPLOY_UPKEEP |
false |
Deploy VaultUpkeep inline |
DEPLOY_WARM_ADAPTERS |
true |
Deploy Aave + Morpho warm adapters |
SKIP_ORACLE_CONFIG |
false |
Intentionally leave the oracle unconfigured (PRE-SEAL phase) instead of using the chain default |
OUTPUT_JSON |
broadcast/core-addresses.json |
Address book output path |
USDC/Aave/Chainlink addresses are no longer hardcoded per script — they live in
multyr-core/script/config/ChainConfig.sol, keyed by block.chainid. See that file for the
current Arbitrum/Base/Ethereum values (cross-checked against bgd-labs/aave-address-book,
Circle's USDC docs, and Chainlink's feed directory) and its header comment for the verification
caveat — re-verify addresses independently before any real mainnet deployment. Morpho vault
selection has no protocol-level canonical address (unlike Aave's Pool/DataProvider), so only
Arbitrum ships with a default; Base/Ethereum deploys must pass MORPHO_VAULT explicitly.
- ROOT_TIMELOCK deployed (Step 1 below, or
multyr-deployment/script/DeployTimelock.s.sol:30) - Deployer EOA funded: ETH for gas + ≥2 USDC (1 USDC dead deposit + 1 USDC buffer)
- Chainlink USDC/USD feed address confirmed for the target chain (see
ChainConfig.sol) - Safe multisig addresses confirmed for GOVERNOR, GUARDIAN, VETOER
Timelock must be deployed before core. Run from multyr-deployment/:
forge script script/DeployTimelock.s.sol:DeployTimelock \
--rpc-url $RPC_URL --broadcast --verifyScript: multyr-deployment/script/DeployTimelock.s.sol:30
Role setup (multyr-deployment/script/DeployTimelock.s.sol:59):
PROPOSER_ROLE→ [SAFE_GOVERNOR, SAFE_GUARDIAN]EXECUTOR_ROLE→ [SAFE_GOVERNOR]CANCELLER_ROLE→ [SAFE_VETO]DEFAULT_ADMIN_ROLE→ timelock itself (multyr-deployment/script/DeployTimelock.s.sol:78:admin = address(0))- Default min delay: 172800s (2 days) — use 0 for fork testing (
multyr-deployment/script/DeployTimelock.s.sol:37)
Export TIMELOCK_ADDRESS from the broadcast output before proceeding.
cd multyr-core
forge script script/DeployCoreSystem.s.sol:DeployCoreSystem \
--rpc-url $RPC_URL --broadcast --verifyEntry point: multyr-core/script/DeployCoreSystem.s.sol:132
Chain guard: multyr-core/script/DeployCoreSystem.s.sol:133
require(block.chainid == 42161, "WRONG_CHAIN: DeployCoreSystem is Arbitrum-only (chainId 42161)");flowchart TD
A["Phase 1: Infrastructure<br/>VaultFactory + GlobalConfig + FeeCollector<br/>PriceOracle + StrategyHealthRegistry<br/>(script/DeployCoreSystem.s.sol:228)"] --> B
B["Phase 2: Security<br/>SelectorRegistry + SystemSealer<br/>(script/DeployCoreSystem.s.sol:278)"] --> C
C["Phase 3: Core + Modules<br/>CoreVault (PAUSED) + EpochedQueueModule<br/>AdminModule + ERC4626Module + LiquidityOpsModule<br/>(script/DeployCoreSystem.s.sol)"] --> D
D["Phase 4: Ecosystem Base<br/>BufferManager + StrategyRouter<br/>+ WarmAdapters (opt) + Incentives (opt) + VaultUpkeep (opt)<br/>(script/DeployCoreSystem.s.sol:356)"] --> E
E["Phase 5: Wiring<br/>Module routing + Ecosystem config<br/>Oracle config + Fees + Perf params<br/>Dead deposit + ComponentsTimelock<br/>Ownership transfer → ROOT_TIMELOCK<br/>(script/DeployCoreSystem.s.sol:450)"] --> F
F["Phase 6: Inline Assertions<br/>17 MUST-pass postconditions<br/>(script/DeployCoreSystem.s.sol:188)"]
| Contract | Citation | Constructor notes |
|---|---|---|
VaultFactory |
multyr-core/script/DeployCoreSystem.s.sol:228 |
No constructor args; used for vault registry + subgraph template |
GlobalConfig |
multyr-core/script/DeployCoreSystem.s.sol:233 |
Temp governor = cfg.deployer; transferred to ROOT_TIMELOCK in Phase 5 |
FeeCollector |
multyr-core/script/DeployCoreSystem.s.sol:250 |
IMMUTABLE governor = ROOT_TIMELOCK; treasuryBps=7000, safetyReserveBps=100, opsMaxBps=3000 |
PriceOracleMiddleware |
multyr-core/script/DeployCoreSystem.s.sol:263 |
Owner = deployer; transferred in Phase 5 |
StrategyHealthRegistry |
multyr-core/script/DeployCoreSystem.s.sol:267 |
Owner = deployer; guardian = SAFE_GUARDIAN |
Critical invariant:
FeeCollector.governoris immutable post-deploy. If ROOT_TIMELOCK address is wrong, re-deploy the entire system. Verified bySystemSealer.verifyAndSeal()(multyr-core/script/DeployCoreSystem.s.sol:249).
| Contract | Citation | Role |
|---|---|---|
SelectorRegistry |
multyr-core/script/DeployCoreSystem.s.sol:283 |
Immutable source of truth for selector-role mappings; activates guardrail when set on vault |
SystemSealer |
multyr-core/script/DeployCoreSystem.s.sol:289 |
Stateless verification contract for final seal |
| Contract | Citation | Notes |
|---|---|---|
CoreVault |
multyr-core/script/DeployCoreSystem.s.sol:304 |
Starts PAUSED — invariant verified at multyr-core/script/DeployCoreSystem.s.sol:313 |
| Factory registration | multyr-core/script/DeployCoreSystem.s.sol:316 |
Immediate after deploy (subgraph event ordering) |
EpochedQueueModule |
multyr-core/script/DeployCoreSystem.s.sol |
Stateless delegatecall target; the sole withdrawal-queue mechanism |
AdminModule |
multyr-core/script/DeployCoreSystem.s.sol:337 |
Stateless; handles admin operations |
ERC4626Module |
multyr-core/script/DeployCoreSystem.s.sol:340 |
Stateless; implements ERC-4626 vault interface |
LiquidityOpsModule |
multyr-core/script/DeployCoreSystem.s.sol:344 |
Stateless; handles liquidity operations |
ERC4626Module invariant: must use
_ensureFreshWarmNav()(self-healing on deposit/mint), NOT_requireFreshWarmNav(). Without this, deposits block after 15 minutes. Seemultyr-core/script/DeployCoreSystem.s.sol:341inline comment (v8 regression lesson).
Liquidity policy invariants (enforced at multyr-core/script/DeployCoreSystem.s.sol:378):
targetHotBps (400) + targetWarmBps (600) = 1000(10% total reserve)maxWarmBps (800) == 1000 - minHotBps (200)opsReserveTargetBps (400) == targetHotBps (400)
BufferManager config (multyr-core/script/DeployCoreSystem.s.sol:362):
| Parameter | Value | Meaning |
|---|---|---|
targetHotBps |
400 | 4% idle in CoreVault |
minHotBps |
200 | 2% trigger for warm refill |
targetWarmBps |
600 | 6% in warm adapters (Aave/Morpho) |
maxWarmBps |
800 | 8% hard cap on warm adapters |
maxWarmSlippageBps |
50 | 0.5% slippage cap on warm operations |
new StrategyRouter(cfg.deployer, address(result.vault), address(result.globalConfig))Owner transferred to ROOT_TIMELOCK in Phase 5.
| Adapter | Citation | Protocol |
|---|---|---|
AaveV3WarmAdapter_USDC |
multyr-core/script/DeployCoreSystem.s.sol:395 |
Aave V3 USDC pool |
MorphoVaultWarmAdapter_USDC |
multyr-core/script/DeployCoreSystem.s.sol:403 |
Morpho Gauntlet Core USDC |
Standalone redeploy: multyr-core/script/DeployWarmAdapters.s.sol
Executed within a single vm.startBroadcast(cfg.deployerPk) context.
Configured via _configureModuleRouting() (multyr-core/script/DeployCoreSystem.s.sol:596):
| Module | Selectors | Role |
|---|---|---|
EpochedQueueModule |
write + view selectors | ROLE_PUBLIC |
AdminModule |
owner selectors | ROLE_OWNER |
AdminModule |
view selectors | ROLE_PUBLIC |
ERC4626Module |
all selectors | ROLE_PUBLIC |
LiquidityOpsModule |
all selectors | ROLE_PUBLIC |
Post-routing gate (multyr-core/script/DeployCoreSystem.s.sol:624):
require(vault.moduleOf(withdraw.selector) == address(erc4626Module), "GATE: withdraw routing");
require(vault.moduleOf(redeem.selector) == address(erc4626Module), "GATE: redeem routing");
require(vault.moduleOf(requestClaim.selector) == address(queueModule), "GATE: requestClaim routing");Sets bufferManager, strategyRouter, healthRegistry, guardian, vetoer on the vault.
router.setHealthRegistry(healthRegistry)
bufferManager.refreshWarmNav()
bufferManager.setRebalanceParams(cooldown=600, minMove=1_000_000, interval=21600) // multyr-core/script/DeployCoreSystem.s.sol:479
Oracle staleness: 86400s (24h) — Chainlink USDC/USD heartbeat. Do NOT use 3600s.
priceOracle.setOracleFeed(USDC, cfg.chainlinkUsdcFeed, 86400); // multyr-core/script/DeployCoreSystem.s.sol:503
globalConfig.setDefaultOracleConfig(address(priceOracle), 86400); // multyr-core/script/DeployCoreSystem.s.sol:504
globalConfig.setAssetOracleConfig(USDC, address(priceOracle), 86400); // multyr-core/script/DeployCoreSystem.s.sol:505WARNING: StrategyRouter hard-fails without oracle (
multyr-core/script/DeployCoreSystem.s.sol:511). Oracle must be configured before ANY deposit operations.
globalConfig.setGovernor(cfg.governor); // ROOT_TIMELOCK (after oracle config)Governor must be transferred AFTER oracle configuration (deployer had temp governor role to call setOracleFeed).
vault.setSelectorRegistry(address(selectorRegistry));
// Guardrail NOW ACTIVE — all subsequent calls through routing guardrail| Parameter | Value | Notes |
|---|---|---|
depositFeeBps |
25 (0.25%) | ERC-4626 entry fee |
withdrawFeeBps |
25 (0.25%) | Queue withdrawal fee |
immediateExitPenaltyBps |
100 (1%) | Immediate exit cost |
forceExitPenaltyBps |
150 (1.5%) | Force exit cost |
| Parameter | Value |
|---|---|
perfRateX |
6e16 (6% WAD-scaled) |
minCrystallizeInterval |
43200s (12h) |
IERC20(USDC).approve(address(vault), 1_000_000); // 1 USDC (6 decimals)
IAdminModule(vault).seedDeadDeposit(1_000_000);
require(IAdminModule(vault).isDeadDepositDone(), "DEPLOY_BUG: dead deposit not done");Inflation attack hardening: seedDeadDeposit must be called when totalAssets == 0.
The deployer EOA must hold ≥1 USDC at this point.
IAdminModule(vault).enableComponentsTimelock();All component ownerships transferred/pending to ROOT_TIMELOCK:
bufferManager.transferOwnership(timelock)strategyRouter.transferOwnership(timelock)healthRegistry.transferOwnership(timelock)priceOracle.transferOwnership(timelock)vault.beginOwnerTransfer(timelock)— pending (requiresacceptOwnerTransferfrom Timelock)
All 6 must pass before broadcast completes:
require(vault.moduleOf(withdraw.selector) == address(erc4626Module), "FINAL: withdraw routing"); // multyr-core/script/DeployCoreSystem.s.sol:191
require(vault.moduleOf(redeem.selector) == address(erc4626Module), "FINAL: redeem routing"); // multyr-core/script/DeployCoreSystem.s.sol:192
require(IAdminModule(vault).isFeesInitialized(), "FINAL: fees not initialized"); // multyr-core/script/DeployCoreSystem.s.sol:193
require(IAdminModule(vault).isDeadDepositDone(), "FINAL: dead deposit not done"); // multyr-core/script/DeployCoreSystem.s.sol:194
require(IAdminModule(vault).isPerfInitialized(), "FINAL: perf not initialized"); // multyr-core/script/DeployCoreSystem.s.sol:195
require(IAdminModule(vault).getImmediateExitPenalty() == 100, "FINAL: ..."); // multyr-core/script/DeployCoreSystem.s.sol:196
require(!vault.isRoutingFrozen(), "FINAL: routing NOT frozen yet"); // multyr-core/script/DeployCoreSystem.s.sol:197
require(vault.paused(), "FINAL: vault must still be paused"); // multyr-core/script/DeployCoreSystem.s.sol:198| Variable | Notes |
|---|---|
FM_MATURITY_TS |
Unix timestamp for vault maturity |
FM_FUNDING_DEADLINE_TS |
Funding window close timestamp |
FM_MIN_FUNDING_ASSETS |
Minimum USDC to activate vault (6 decimals) |
FM_TARGET_FUNDING_ASSETS |
Target TVL for the FM vault |
FIXED_TERM_STRATEGY |
Address of the fixed-term strategy to register |
FM_AUTO_CLOSE |
Optional; enables auto-close at maturity |
FM_INSTANT_EXIT |
Optional; enables instant exit mode |
FM_FORCE_PENALTY_BPS |
Optional; force exit penalty override |
Script: multyr-core/script/DeployFixedMaturityVault.s.sol:49
flowchart TD
A["Phase 1: Infrastructure<br/>GlobalConfig + FeeCollector + PriceOracle<br/>StrategyHealthRegistry<br/>(script/DeployFixedMaturityVault.s.sol)"] --> B
B["Phase 2: Core + FixedMaturityModule<br/>CoreVault + Queue/Admin/ERC4626/<br/>LiquidityOps + FixedMaturityModule<br/>(includes FM mode activation)"] --> C
C["Phase 3: Ecosystem<br/>BufferManager (hot-only, no warm) + StrategyRouter<br/>+ Permit2DepositHelper"] --> D
D["Phase 4: Module routing<br/>92 selectors including FM selectors"] --> E
E["Phase 5: FM config<br/>setVaultModeFixedMaturity<br/>(maturity + funding window + strategy)"] --> F
F["Phase 6: Ecosystem wiring<br/>Governor transfer + Oracle setup"] --> G
G["Phase 7: SelectorRegistry activation<br/>(guardrail NOW active)"] --> H
H["Phase 8: FixedMaturityVaultUpkeep<br/>(standalone: DeployFixedMaturityVaultUpkeep.s.sol)"] --> I
I["Phase 9: seedDeadDeposit<br/>1 USDC — MANDATORY inflation hardening<br/>(script/DeployFixedMaturityVault.s.sol:51-ref)"] --> J
J["Phase 10: Final assertions"]
FM-only notes:
FixedMaturityModuledeployed as stateless module (included alongside standard modules)BufferManagerin FM mode is hot-only (no warm adapters) —targetWarmBps = 0- FM vault CANNOT switch to OE mode post-deploy
- Script:
multyr-core/script/DeployFixedMaturityVault.s.sol
For partial re-deployments (e.g., after migration or upkeep contract failure):
| Script | Citation | Purpose |
|---|---|---|
DeployVaultUpkeep.s.sol |
multyr-core/script/DeployVaultUpkeep.s.sol |
Redeploy VaultUpkeep + wire to BufferManager |
DeployBufferManager.s.sol |
multyr-core/script/DeployBufferManager.s.sol |
Redeploy BufferManager + migrate warm adapters |
DeployStrategyRouter.s.sol |
multyr-core/script/DeployStrategyRouter.s.sol |
Redeploy StrategyRouter + re-register strategies |
DeployQueueModule.s.sol |
multyr-core/script/DeployQueueModule.s.sol |
Redeploy EpochedQueueModule + update module routing |
DeployWarmAdapters.s.sol |
multyr-core/script/DeployWarmAdapters.s.sol |
Redeploy Aave + Morpho warm adapters |
Note: All standalone scripts require
VAULT_ADDRESSandTIMELOCK_ADDRESSenv vars. Routing changes (adding new modules) require an unpaused vault AND owner authorization.
For single-command full deploy with Permit2 + Incentives + Upkeep:
# Run from vault-usdc2/ root
forge script multyr-core/script/DeployCoreIntegrated.s.sol:DeployCoreIntegrated \
--rpc-url $RPC_URL --broadcast --verifyScript: multyr-core/script/DeployCoreIntegrated.s.sol
This combines core deploy with Permit2DepositHelper wiring and optional Incentives module in a single broadcast. Preferred for fresh chain/testnet deployments. For mainnet, use Modular Path B (step-by-step) for auditability.
After deploy, broadcast/core-addresses.json is written (multyr-core/script/DeployCoreSystem.s.sol:688):
{
"chainId": 42161,
"blockNumber": ...,
"state": "PRE-SEAL",
"vaultFactory": "0x...",
"globalConfig": "0x...",
"feeCollector": "0x...",
"priceOracle": "0x...",
"healthRegistry": "0x...",
"selectorRegistry": "0x...",
"systemSealer": "0x...",
"vault": "0x...",
"queueModule": "0x...",
"adminModule": "0x...",
"erc4626Module": "0x...",
"liquidityOpsModule": "0x...",
"bufferManager": "0x...",
"strategyRouter": "0x...",
"aaveWarmAdapter": "0x...",
"morphoWarmAdapter": "0x..."
}Export the relevant addresses as env vars for subsequent deploy steps
(e.g., VAULT_ADDRESS, STRATEGY_ROUTER_ADDRESS).
After DeployCoreSystem.s.sol completes (multyr-core/script/DeployCoreSystem.s.sol:769):
| State | Value |
|---|---|
vault.paused() |
true — vault is PAUSED |
vault.isRoutingFrozen() |
false — routing NOT frozen (freeze in Phase 6 Hardening) |
vault.owner() |
pending transfer (requires acceptOwnerTransfer from ROOT_TIMELOCK) |
IAdminModule(vault).isFeesInitialized() |
true |
IAdminModule(vault).isDeadDepositDone() |
true |
IAdminModule(vault).isComponentsTimelocked() |
true |
globalConfig.governor() |
ROOT_TIMELOCK |
feeCollector.governor() |
ROOT_TIMELOCK (immutable) |
Three settings are NOT written by any deploy script. The system deploys and seals without them, so nothing fails loudly at deploy time; each one is either a risk control that silently sits wide open, or a hard dependency for queue settlement. Set all three before the vault takes real deposits.
| Parameter | Where | Script default | Why it matters |
|---|---|---|---|
| Vault deposit cap | GlobalConfig.setVaultDepositLimits(vault, cap, userCap, minDeposit), read via IParamsProvider.getDepositLimits |
10,000,000e6 (10M USDC) from defaultVaultDepositCap |
No script narrows it. A vault intended to launch at 20,000 USDC will accept 10M until governance says otherwise. |
| Asset oracle | GlobalConfig oracle config for the vault's asset, read via oracleConfigFor(asset, vault) |
unset | StrategyRouter.executeRedeemBatch values the asset through OracleValuationLib and reverts OracleNotConfigured without it — for 6-decimal USDC as much as an 18-decimal asset. fundEpoch swallows that revert, so with no oracle the strategy-redeem leg of the funding waterfall silently does nothing and epochs stay Closed. The quote must also be fresher than the configured staleness window at the moment fundEpoch runs, which for a multi-day epoch means the keeper has to refresh it near funding time, not at close. |
queueStressThreshold |
GlobalConfig dynamic-cap config, read via IParamsProvider.getDynamicCapParams |
100 claims | Drives WithdrawalCapLib.calculateDynamicCapBps: once outstandingClaimCount reaches it, the instant-exit cap collapses to minBps for everyone. At the default, and with minClaimAmount at its own 100 USDC default, pinning the cap at its floor costs roughly 100 x 100 = 10,000 USDC of refundable capital. On a 20,000 USDC vault that is half the deposit cap; on a 10M vault it is negligible. Tune it against the real cap. |
Also worth setting deliberately rather than accepting the default:
- Warm adapter allowance cap —
CoreVault.approveWarmAdapters(adapters, cap)takes an explicit ceiling instead of granting an unlimited allowance.DeployCoreSystempassesWARM_ADAPTER_ALLOWANCE_CAP, defaulting to 1,000,000e6. The allowance depletes as adapters pull and does not renew, so an undersized cap eventually stalls warm deploys, and an oversized one weakens the bound. Size it against the deposit cap and expected warm cycling.
cast call $GLOBAL_CONFIG "getDepositLimits(address)" $VAULT --rpc-url $RPC
cast call $GLOBAL_CONFIG "oracleConfigFor(address,address)" $ASSET $VAULT --rpc-url $RPC
cast call $GLOBAL_CONFIG "getDynamicCapParams(address)" $VAULT --rpc-url $RPC
cast call $ASSET "allowance(address,address)" $VAULT $WARM_ADAPTER --rpc-url $RPCA zero oracle address in the second call means queue settlement cannot pull from
strategies. Watch EpochFundingShortfall for the runtime symptom.
multyr-core/script/DeployCoreSystem.s.sol:769 — _printNextSteps() output:
1. Deploy strategy:
VAULT_ADDRESS = <vault>
STRATEGY_ROUTER_ADDRESS = <strategyRouter>
BUFFER_MANAGER_ADDRESS = <bufferManager>
HEALTH_REGISTRY_ADDRESS = <healthRegistry>
GLOBAL_CONFIG_ADDRESS = <globalConfig>
PRICE_ORACLE_ADDRESS = <priceOracle>
→ Run: multyr-strategies/script/DeployUsdcLendingStrategy.s.sol
2. Timelock: acceptOwnerTransfer + setAuthorizedSealer + systemSealer.verifyAndSeal(config)
(single atomic call — see `docs/architecture.md` §10.1)
3. Verify all contracts on Arbiscan
See multyr-deployment/runbooks/full-system-deploy.md for the complete 7-day mainnet timeline.
If CHAINLINK_USDC_FEED is not set, the oracle is skipped with a WARNING. The StrategyRouter
will hard-fail on any deposit/withdraw attempt. Configure oracle via Timelock before going live.
The deployer must hold ≥1 USDC (6 decimals = 1,000,000) at deploy time. Transfer USDC to deployer before running the script.
After deploying FeeCollectorUpkeep (from multyr-periphery), call addToken(vault) or fees
will never distribute. This is a v9 mainnet incident lesson.
vault.beginOwnerTransfer(timelock) sets pending owner; the ROOT_TIMELOCK must call
acceptOwnerTransfer() to finalize. Until then, vault.owner() is still the deployer
(or pending state). Use multyr-deployment/script/ExecuteFinalSeal.s.sol to finalize.
FeeCollector.governoris immutable: Set at deploy time (multyr-core/script/DeployCoreSystem.s.sol:250). Wrong address requires full re-deploy.- Oracle staleness = 86400s: Chainlink USDC/USD heartbeat. Never use 3600s. (
multyr-core/script/DeployCoreSystem.s.sol:503) - Dead deposit before ecosystem wiring:
seedDeadDepositmust run whentotalAssets == 0(multyr-core/script/DeployCoreSystem.s.sol:564). - Guardrail activation: After
setSelectorRegistry(multyr-core/script/DeployCoreSystem.s.sol:526), all routing goes through the immutableSelectorRegistry. - Do NOT freeze routing pre-strategy: Routing is frozen in Phase 6 Hardening (
ExecuteFinalSeal.s.sol), AFTER strategy wiring and smoke test. - Adapter constructor admin ordering: Use
cfg.deployeras temp admin, transfer AFTERPARAM_ROLEgrants (V9 lesson).
multyr-deployment/script/lib/DeploymentAssertions.sol provides 19 CRITICAL invariant checks:
# Run from vault-usdc2/ root
forge script multyr-deployment/script/lib/DeploymentAssertions.sol --rpc-url $RPC_URLRun on fork before mainnet deployment (Day 1 of multyr-deployment/runbooks/full-system-deploy.md).
Generated code-first from multyr-core/script/DeployCoreSystem.s.sol, multyr-deployment/script/DeployTimelock.s.sol, and multyr-core/script/DeployFixedMaturityVault.s.sol. All citations verified against source.