@morpho-org/evm-simulation
v8.6.1
Published
EVM simulation engine for Morpho transactions.
Keywords
Readme
@morpho-org/evm-simulation
Overview
EVM transaction simulation engine for Morpho — bundle execution preview, transfer parsing, and net per-account balance changes.
Installation
pnpm add @morpho-org/evm-simulationUsage
import {
simulate,
type SimulationConfig,
SimulationRevertedError
} from "@morpho-org/evm-simulation";
const config: SimulationConfig = {
chains: new Map([
[1, { simulateV1Url: "https://<your-mainnet-rpc>" }],
]),
timeoutMs: 5000,
};
try {
const { simulationTxs, calls, transfers, assetChanges } = await simulate(
config,
{
chainId: 1,
// mode: "final" (default) executes the signed calldata against actual
// permissions and checks supplied signed authorization descriptors.
// mode: "preview" prepares the descriptors as simulated permissions.
transactions: [{ from: user, to: vault, data: encodedDeposit }],
// limits is required: quote the deposit's pay-in cap and minimum
// minted shares. ("none" is reserved for transactions no operation
// type covers.)
limits: {
operations: [
{
type: "vaultV1Deposit",
vault,
quote: { assetsPaid: depositAssets, sharesMinted: quotedShares },
},
],
},
},
);
} catch (err) {
if (err instanceof SimulationRevertedError) {
// Display err.message; decode err.revertData with your ABI when needed.
}
throw err;
}Every call inside eth_simulateV1, including preparation calls, state reads and assertions, carries the request's chainId as a hex quantity, so nodes that check it (geth, Anvil, Monad and Stable do) reject an endpoint on another chain with InvalidChainIdError.
Every chain entry requires simulateV1Url, pointing to a JSON-RPC endpoint that supports eth_simulateV1. Requests send validation: false so gas is not charged. Monad (chain 143) is handled internally: its nodes reject false, so it sends true (its simulated block charges no gas either way), and it pins to the finalized block by default because its latest block is not final — supply a finalized block when passing block. On Monad, gasUsed reports the call's gas limit, not the gas consumed. Set blockOverrides.gasLimit on a chain entry to send that gas limit as the simulated block's blockOverrides.gasLimit; without it, no block override is sent. Set callGasLimit on a chain entry to send it as gas on every simulated call; without it, no per-call gas is sent. This includes state reads and assertions. A call that needs more gas than callGasLimit runs out of gas and fails as a revert or a read/assertion failure, so set it above the heaviest expected call. On geth-style nodes, it must also fit in the gas left in the simulated block. Set parentHashCheck on a chain entry to turn on or off the check that the simulated block's parentHash is the pinned block hash. It defaults to off on Stable (chain 988), whose nodes report a parentHash that never matches the pinned block, and on for every other chain. The block number and timestamp checks always apply. Execution uses the full timeoutMs budget (default 5000 ms), with no retries or provider fallback. RPC failures, timeouts and reverts throw typed errors. The optional logger still reports parsing and retention warnings.
Native-ETH movements are observed through traceTransfers logs on the simulated calls. No state overrides or helper contracts are added automatically.
Caller-provided state overrides
simulate() accepts an optional stateOverrides account-address map in JSON-RPC
format (SimulationStateOverrides). Each account can override code, balance,
nonce, and either state (replace all storage) or stateDiff (patch storage).
Balances and nonces are hex quantities; storage keys and values are 32-byte hex.
The SDK forwards override contents unchanged, without runtime validation or
normalization. Consumers are responsible for supplying valid overrides.
Overrides apply once before all calls in the simulated block, including state
reads, authorization preparations, transactions and assertions. Calldata-target
code checks use an explicit code override when supplied and reject code-less
EIP-7702 delegates. Metadata lookups outside
eth_simulateV1 still read the pinned onchain state.
For example, a caller can model a contract sender through an EIP-7702 delegation
on Monad, whose validation: true otherwise rejects contract senders:
await simulate(config, {
chainId: 143,
transactions,
limits,
stateOverrides: {
[sender]: { code: `0xef0100${scratchAddress.slice(2)}` },
[scratchAddress]: { code: senderBytecode },
},
});The caller chooses an unused scratch address and reads senderBytecode at the
same state block used for simulation (supply block or blockNumber to pin it).
Delegation executes that code with the sender's storage; code introspection sees
the delegation instead, and gas usage may differ. The SDK does not construct or
verify this workaround. Any caller override, especially a balance or storage
override, makes the result describe hypothetical state; it is not proof the
unmodified transaction will succeed at broadcast. Omitting stateOverrides
keeps real-balance funding and adds no implicit overrides. Calldata targets with
EIP-7702 delegation now also need code at their delegate, including existing
onchain delegations without overrides. This adds a pinned eth_getCode read
under the same timeout and rejects code-less delegates with
SimulationValidationError.
With overrides, signed-request use and approval allowlist checks describe only the overridden execution. Overriding token, Morpho, Midnight or EcrecoverAuthorizer code or storage can fake use events or hide approval grants. Integrators must control the overrides; a passing result with untrusted overrides does not prove signatures were used or that the broadcast transaction grants only allowed permissions.
Upgrading from v4? Start with the v4 → v5 migration guide for the backend cutover, then apply the v5 → v6, v6 → v7, and v7 → v8 guides in order. Upgrading from v5? Apply the v5 → v6 migration guide first (retention-guard address change and 6.1.0 fail-closed RPC responses), then the v6 → v7 migration guide for simplified errors and original revert bytes, then the v7 → v8 migration guide for required limits. Upgrading from v6? See the v6 → v7 migration guide for simplified errors and original revert bytes, then the v7 → v8 migration guide for required limits. Upgrading from v7? See the v7 → v8 migration guide for required limits.
API surface
All symbols below are re-exported from the package root.
simulate(config, params)— run a bundle through the simulation pipeline.Config types:
SimulationConfig,ChainSimulationConfig,SimulationLogger.Input types:
SimulateParams(v5 shape:mode,SimulationAuthorizationrequests,SimulationLimits),SimulationMode,SimulationTransaction,Assertion,LogAssertion,AssertionBounds,StateBlock(the optionalSimulateParams.block:{ number, hash, timestamp }),SimulationStateOverrides(the optionalSimulateParams.stateOverridesaccount map), andSimulationAccountOverride(per-accountcode,balance,nonce,stateandstateDiff).Assertion helpers:
erc20BalanceAssertion(token balance),nativeBalanceAssertion(native balance), andmaxDepositAssertion(vault deposit cap); all use inclusivemin/maxbounds.toSimulationAuthorizations({ owner, requirements })— map morpho-sdkActionRequirement[]ontoSimulationAuthorization[]straight fromaction.args— nothing is decoded or validated; validation happens insimulate()'s request parser.Authorizations and limits:
SimulationAuthorizationand its members (Erc20ApprovalAuthorization,Erc2612PermitAuthorization,Permit2TransferAuthorization,BlueAuthorization,BlueAuthorizationSignature,MidnightAuthorizationSignature) with their EIP-712 payloads (Eip712Domain,Eip712Field,Erc2612PermitTypedData,Permit2TransferTypedData,BlueAuthorizationTypedData);SimulationLimits,ApprovalLimit,OperationLimit, and the sharedSlippageLimits,CheckedSlippageLimits(the checked result carrying the appliedslippageTolerance),CheckedOperationLimitandCheckedSimulationLimits(the type ofverification.limitson a checked run),SlippageQuote,SharePriceLimit,InKindPositionQuote,ForceExitDeallocationQuote,BluePositionBounds,MidnightPositionBounds,MidnightDebtPositionBounds,MidnightMarketStruct, andMidnightCollateralAmount.toSimulationLimits(action, { owner, onBehalf?, receiver?, slippageTolerance?, market?, expected? })— build theSimulationLimitsof a morpho-sdk action transaction (tx.action), deriving quote sides the encoded args fix and merging the caller'sexpectedquote over them.marketis forwarded formidnightSupplyCollateral,midnightTakeLend,midnightTakeBorrow, andmidnightSupplyCollateralTakeBorrow; it is also forwarded formidnightCancelAndMakewith collateral supplies and noblueSupply, whose non-zero roots emitmidnightSupplyCollateralMakeBorrowlimits. The MidnightMarketUtils.toId(market)must equal the action's market id.receiverapplies tovaultV2ForceRedeem, Midnight redeem/sell actions,midnightSupplyCollateralTakeBorrow,midnightTakeBorrow,midnightTakeWithdraw, and repayment-withdraw actions with collateral withdrawals; for the latter, it must matchcollateralReceiver. For Midnight sells, pass the action's encoded receiver when it differs fromowner;takeLendacts formsg.senderand does not allow an external receiver.onBehalfapplies only tomidnightSupplyCollateralandmidnightRedeem; eligible maker bundles act forowner. Midnight sells,midnightTakeLendand repay-withdraw always measuremsg.senderand reject any explicitaccount; Blue parking andmidnightSupplyCollateralMakeBorrowmeasure the bundle sender, soaccountmust equalowneror be omitted; Midnight take bundles act forowner/msg.sender.Slippage-check result types:
VerifiedSimulationResult,SimulationVerification,SimulatedOperation,AuthorizationPreparation.Input-based limits:
InputOperationLimit,OperationInputs,AssetsOrShares,RepayInputs,SimulationInputLimitsandSimulateInputParams; checked input outputs useCheckedInputOperationLimit,CheckedSimulationInputLimits,SimulationInputVerificationandVerifiedSimulationInputResult.ScalarQuoteFieldnames the keys offieldChecks;FeelessInputTypenames the input operation types that rejectreferralFee, andReallocationPenaltyInputTypenames those that acceptreallocationPenalty. ExistingSimulateParams,SimulationLimits,CheckedOperationLimitand quote-only result types remain unchanged;simulate()accepts both forms and preserves its quote-only return overload, including wrappers typed withSimulateParams.Result types:
SimulationResult,SimulationCall,Transfer,AccountAssetChanges,AssetChange,RawLog.Errors:
SimulationPackageError(abstract base —instanceofit to catch any package error),SimulationRevertedError,SimulationAssertError,SimulationLogAssertError,BlacklistViolationError,RpcTransportError,SimulationValidationError,UnsupportedChainError, and the verification errorsUnsupportedOperationError,InvalidSimulationResponseError,InvalidChainIdError,SlippageCheckUnavailableError,ApprovalCheckUnavailableError,UnexpectedTokenApprovalError,UnexpectedMorphoAuthorizationError,UnexpectedMidnightAuthorizationError,AuthorizationRequestMismatchError,UnusedAuthorizationError,ConsumerLimitViolationError,UnexpectedSimulationError, and the limit-building errorsLimitAccountMismatchError,MissingExpectedQuoteError,AmbiguousExpectedQuoteError,UnsupportedLimitActionError.Error helpers:
SIMULATION_ERROR_CODES/SimulationErrorCode(everyerror.code),isSimulationPackageError(structural guard narrowing toSimulationPackageError),toSimulationErrorResponse(error)/SimulationErrorResponse(JSON-safe failure fields for API responses),RetainedAsset. Errors use standardmessageand optionalcause;SimulationRevertedErrorcarriesrevertData?: HexandtransactionIndex?: number;SimulationAssertErrorcarriestransactionIndex,phase,assertionIndex, a copy of the caller'sassertion(to,data, andcheck), and rawreturnData;SimulationLogAssertErrorcarriestransactionIndex,assertionIndex, a copy of the caller's assertion, and the lowercase log values passed to its check, which receives them in a frozen copy.Verification vocabulary:
SIMULATION_MODES/SimulationMode,OPERATION_TYPES/OperationType,BLUE_MARKET_OPERATION_TYPES/BlueMarketOperationType,VAULT_OPERATION_TYPES/VaultOperationType,SimulationOperationSubjectand its membersBlueMarketOperationSubject,BlueRefinanceSubject,VaultOperationSubject,VaultV1MigrateToV2Subject,MidnightBuySubject,MidnightBlueParkingSubject,MidnightCollateralOperationSubject,MidnightRedeemSubject,MidnightSellSubject, andMidnightRepayWithdrawCollateralSubject(operation groups and the operation-keyed subject union used by verification results).
Assertions
Each transaction may declare caller-supplied assertBefore and assertAfter
assertions, plus assertLogs checks over that transaction's frozen log copy,
whose hex address, topics, and data are lowercase. Log
assertions run after the transaction succeeds and before its assertAfter
assertions; they do not add calls to the simulation plan. The erc20BalanceAssertion, nativeBalanceAssertion, and
maxDepositAssertion helpers construct common calls; their optional min and
max bounds are inclusive and require at least one bound. Values are in token
base units, wei, or vault asset units, respectively.
Assertions run as ordinary calls from the bundle owner (msg.sender is the
owner), so they must be view calls: a call that writes state acts with the
owner's authority, and later calls see its writes.
An assertion can also call any view function and inspect its return data:
const allowanceIsZero: Assertion = {
to: USDC,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "allowance",
args: [owner, spender],
}),
check: (returnData) =>
hexToBigInt(returnData) === 0n
? undefined
: `Allowance is "${hexToBigInt(returnData)}", expected "0".`,
};Every assertLogs check for a transaction receives the same recursively frozen
copy of its logs. The address, topics, and data are lowercase hex, so the event
signature can be compared directly with viem's toEventSelector. Use viem to
decode a log from the transaction's RawLog[]. decodeEventLog requires a
non-empty topics tuple, so this example narrows the signature topic and builds
that tuple without changing the RawLog shape or casting:
import type { LogAssertion } from "@morpho-org/evm-simulation";
import {
decodeEventLog,
erc20Abi,
isAddressEqual,
toEventSelector,
type Address,
type Hex,
} from "viem";
const USDC: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const approvalTopic = toEventSelector("Approval(address,address,uint256)");
const approvalMatches = (
owner: Address,
spender: Address,
amount: bigint,
): LogAssertion => ({
check: (logs) => {
const log = logs.find(
(entry) =>
isAddressEqual(entry.address, USDC) &&
entry.topics[0] === approvalTopic,
);
if (!log) return "Approval event is missing.";
const [signature, ...indexedTopics] = log.topics;
if (!signature) return "Approval event is missing.";
const topics: [Hex, ...Hex[]] = [signature, ...indexedTopics];
const { args } = decodeEventLog({
abi: erc20Abi,
eventName: "Approval",
data: log.data,
topics,
});
return isAddressEqual(args.owner, owner) &&
isAddressEqual(args.spender, spender) &&
args.value === amount
? undefined
: "Approval event has unexpected values.";
},
});assertBefore and assertAfter assertions run as ordinary calls inside the
same eth_simulateV1 request, not as static calls. They must be view calls: any
state they write is visible to later calls. Morpho Vault V2's maxDeposit is
pure and always returns 0, so
min > 0 always fails there; the helper is meaningful for Vault V1/MetaMorpho.
A failed assertBefore or assertAfter check throws SimulationAssertError
(SIMULATION_ASSERT_FAILED) with
transactionIndex, phase, assertionIndex, a copy of the caller's
assertion (to, data, and check), and raw returnData. A failing assertBefore takes precedence
over its transaction's own revert. Reverted assertion reads also throw
SimulationAssertError; the check decides whether empty or malformed return
data passes. Assertion declarations are omitted from returned simulationTxs.
An assertLogs failure throws SimulationLogAssertError with the same
SIMULATION_ASSERT_FAILED code, the transaction and assertion indexes, a copy
of the log assertion, and the lowercase log values passed to its check, which
receives them in a frozen copy. A transaction
revert takes precedence over its log assertions, and a failing log assertion
takes precedence over its assertAfter assertions.
Limits
limits is required on every call: pass quote-only SimulationLimits,
SimulationInputLimits for typed inputs or mixed operations (via
SimulateInputParams), or the literal "none" to skip limit and approval checks
and their state reads. Final-mode signed-authorization use checks still run.
"none" is
reserved for transactions that no operation type covers (Merkl reward claims,
unsupported Midnight actions and midnightCancelAndMake bundles that combine
a Blue supply with collateral supplies, contain unfunded publications, or
contain cancellations only, and approval/authorization-only bundles), and
every transaction an operation type covers must pass limits. Missing limits
and an empty operations array are rejected as SimulationValidationError —
an implicit
opt-out is never accepted.
New integrations should supply inputs, independently of the transaction builder:
const result = await simulate(config, {
chainId: 1,
transactions: [{ from: owner, to: tx.to, data: tx.data, value: tx.value }],
limits: {
operations: [{ type: "vaultV2Deposit", vault, inputs: { assets } }],
},
});InputOperationLimit accepts typed amounts through inputs: vault deposits and
withdrawals use { assets }, redemptions use { shares }; Blue supply, borrow
and collateral operations use { assets }; Blue withdrawal accepts { assets }
or { shares }, and repayment additionally accepts "full". Combined Blue
operations use { collateralAssets?, borrowAssets? } or
{ repay?, collateralAssets? }, with at least one active leg. Migration names
sourceVault and targetVault, with { assets } or { shares }. The position
owner is account (default: transaction sender); receiver also defaults to
the sender. OperationInputs, AssetsOrShares and RepayInputs describe these
amounts. InputOperationLimit is preferred for the types OperationInputs
covers; OperationLimit remains supported as the quote-only type.
Expectations come from hidden before-state calls in the same simulated block:
vault previews, Morpho accrueInterest followed by market totals and the owner's
position. Migration uses the source preview and the target V2's accrued totals,
fee shares and virtual shares, reproducing its previewDeposit formula. Helper
calls never appear in simulationTxs, transfers or asset changes. Blue accrual
does carry into the bundle: a typed Blue input accrues its market before the
user's transactions, so when the market has not yet accrued in the simulated
block, calls[i].logs omit that market's AccrueInterest event and
calls[i].gasUsed excludes the accrual and interest-rate-model cost. Quote-only
limits do not accrue.
Derived sides use no percentage tolerance. A directly selected amount must match
exactly; converted outputs must be at least the derived amount and converted
costs at most it. Derived debt shares on borrow legs and source shares on vault
withdrawals and migrations must match exactly, preventing amounts split across
subjects. Assets-mode Blue withdrawals additionally require at least the derived
shares minus one share wei per earlier subject-market supply evidenced by a
simulated Blue Supply event (including public-allocator reallocations). Each
supply rounds minted shares down and can lower the withdrawal conversion by one
share wei. Larger conversion changes fail closed. Explicit quote sides override derivation and retain the
existing slippageTolerance behavior (default DEFAULT_SLIPPAGE_TOLERANCE).
slippageTolerance is validated but ignored on derived sides, even if supplied.
Checked quotes include the derived values. Input-based checked limits also
report fieldChecks per scalar side: exact means equality, minimum and
maximum are zero-tolerance derived bounds, and quoted uses the reported
slippageTolerance. Internal derivation fields are not returned.
Inactive combined-operation pay-in legs receive automatic 1n caps. An inactive
repay leg permits no debt-share growth; an inactive collateral-supply leg permits
no collateral loss. These subject checks survive explicit quote overrides.
For combined Blue operations with both legs active, matching wallet tokens and
matching sender/receiver, the derived wallet expectations net the pay-in against
the payout. assetsReceived is signed (zero or a debit is possible), and
assetsPaid is the nonnegative net debit. Subject collateral and share checks
remain independent of this netting, including on-behalf positions. Different
tokens or wallet holders do not net.
Wallet measurements cover the whole bundle, not individual operations. If two
operation limits measure the same wallet token balance (including native tokens
and vault-share balances), and either uses typed inputs, simulation rejects them
with SimulationValidationError before execution. Split the bundle or use
quote-only limits for every overlapping operation, with bundle-wide wallet quotes.
Multiple sides within one operation may share a wallet measurement; disjoint
wallet measurements and existing quote-only overlaps remain supported.
Migration target shares retain a strict minimum: if an earlier source withdrawal
changes the target's conversion, quote sharesMinted explicitly rather than
assuming a percentage or fixed rounding allowance.
Declare an integrator's WAD-scaled percentage as referralFee (default zero,
strictly below 1e18); it is not a user amount. Gross funding/proceeds fees
round down; Blue repay fees are floor(repaidAssets * pct / (1e18 - pct)).
Collateral-only Blue operations have no fee-bearing leg. For blueBorrow,
blueWithdraw (assets or shares mode), and an active borrow leg of
blueSupplyCollateralBorrow, also declare reallocationPenalty: the aggregate
allocator charge in absolute loan-token base units, default zero, not a
WAD-scaled rate. Sum ceil(allocation.assets * allocation.penalty / 1e18) across
allocations. Net proceeds are (assets - penalty) - floor((assets - penalty) *
referralFee / 1e18); position shares still derive from the gross assets.
The penalty must leave positive proceeds. Other input types do not accept it;
refinance still requires a quote including its penalty. Undeclared fees and
penalties are not deducted from expectations and fail the derived check.
In-kind redeems, refinance, V2 force exits and Midnight operations
still require explicit quotes; inputs is rejected for these types.
Quote-only limits and toSimulationLimits keep their v8 behavior and are not
deprecated. They remain the only form for refinance, in-kind redeems, force
exits and Midnight operations. For quote-only callers, the caller chooses the
action, subject, quote, and percentage tolerance. Every
operation type requires the quote sides of the required-minimums and
required-caps lists below — the per-operation table lives in
docs/migrations/evm-simulation-v7-to-v8.md:
- Required minimums: callback-position
sharesMintedonmidnightSupplyBlueMakeLend;assetsReceivedon payout ops (blueWithdraw,blueBorrow,blueWithdrawCollateral,midnightRedeem,midnightTakeBorrow,midnightTakeWithdraw, and every vault withdraw, redeem and force-exit); supply/vaultsharesMinted(blueSupply,vaultV1Deposit,vaultV2Deposit,vaultV1MigrateToV2); debtsharesBurned(blueRepay,blueRefinance);collateralSuppliedonblueSupplyCollateral,midnightSupplyCollateral, andmidnightSupplyCollateralMakeBorrow;unitsBoughtonmidnightTakeLend;debtRepaidwhen quoted andcollateralWithdrawalsdelivery onmidnightRepayWithdrawCollateralandmidnightTakeRepayWithdrawCollateral;collateralSupplies,unitsSold, andassetsReceivedonmidnightSupplyCollateralTakeBorrow; and either ofcollateralSupplied/assetsReceivedorsharesBurned/assetsReceivedon the combined ops. - For
midnightSupplyCollateralTakeBorrow, eachcollateralSuppliesamount is both the exact minimum credited at that index and the exact sender pull cap source; percentage tolerance does not widen either bound. The helper derives these amounts from the action and rejectsexpected.collateralSuppliesoverrides. Owner native value from all transactions in the bundle, including unrelated ones, is subtracted from the owner's wNative pull cap, which is the quoted wNative collateral. If native value exceeds that cap, simulation fails closed withSlippageCheckUnavailableError. This includes a0ncap: any positive native value fails closed, for example when the owner receives wNative loan proceeds but no quoted collateral is wNative. Native value sent on a chain without registered wNative fails closed withSlippageCheckUnavailableError; otherwise, if the operation has no owner wNative bound, native value is not checked. The combined operation throwsSimulationValidationErrorwhen its wallet bounds overlap another operation's wallet balance reads or wallet bounds, or when its Midnight collateral, credit, or debt reads overlap another operation's Midnight position reads. - Required caps:
assetsPaidon every pay-in op (blueSupply,blueSupplyCollateral,midnightSupplyCollateral,midnightSupplyCollateralMakeBorrow,midnightSupplyBlueMakeLend,midnightTakeLend,blueRepay,vaultV1Deposit,vaultV2Deposit, and unconditionally on both combined ops, on an active repay leg ofmidnightRepayWithdrawCollateral, and onmidnightTakeRepayWithdrawCollateral); supply/vaultsharesBurned(includingvaultV1MigrateToV2's source-vault shares); debtsharesMinted(blueBorrow,blueRefinance's target market,blueSupplyCollateralBorrow);collateralWithdrawnonblueWithdrawCollateralandblueRepayWithdrawCollateral; the position- decrease side ofcollateralWithdrawalsonmidnightRepayWithdrawCollateralandmidnightTakeRepayWithdrawCollateral;creditRedeemedonmidnightRedeemandmidnightTakeWithdraw;unitsSoldonmidnightTakeBorrowandmidnightSupplyCollateralTakeBorrow.
A single-leg combined use quotes 1n on each inactive leg's cap
(sharesMinted, collateralWithdrawn, assetsPaid, since 0 is rejected);
an active leg on either side requires that leg's other side: sharesMinted
above 1n requires assetsReceived on blueSupplyCollateralBorrow, and
collateralWithdrawn above 1n requires it on
blueRepayWithdrawCollateral; symmetrically, assetsPaid above 1n
requires collateralSupplied on blueSupplyCollateralBorrow and
sharesBurned on blueRepayWithdrawCollateral. In-kind redeems must quote
positions — the Blue supply shares delivered to the receiver per market —
or assetsReceived for the idle wallet portion, on top of the sharesBurned
cap.
The collateral a refinance moves between markets is not verified.
midnightSupplyCollateral and midnightSupplyCollateralMakeBorrow limits
name the Midnight market (marketId, from morpho-sdk's
MidnightMarketUtils.toId) and the collateralIndex they supply; collateralSupplied
is the Midnight.collateral(marketId, account, collateralIndex) delta, and
assetsPaid is the sender's balance delta in the collateral token, resolved
from Midnight.toMarket(marketId) or the supplied market struct at that index.
Two supplies to the same slot in one bundle are checked against one aggregate
quote. The chain must have a registered midnight address; otherwise
UnsupportedChainError is thrown. A market that does not exist yet at the
state block has no toMarket entry. For a first midnightTakeLend,
midnightTakeBorrow,
midnightSupplyCollateralTakeBorrow, midnightSupplyCollateral, or
midnightSupplyCollateralMakeBorrow into a
market created in the bundle, pass its struct via market; the Midnight
MarketUtils.toId(market) must equal marketId. For a first
midnightSupplyCollateral or midnightSupplyCollateralMakeBorrow,
assetPaid is a fallback when no market is passed: set it to
collateralParams[collateralIndex].token. The override is
not checked, and a wrong address lets the assetsPaid cap pass without
measuring anything. It is also the token the approval check allows to Midnight
and MidnightBundlesV2.
midnightSupplyBlueMakeLend checks both the sender's exact debit of the full
parked amount and the BlueBuyCallback's supply-share increase over the whole
bundle. For wNative loan tokens, the debit measurement adds ETH sent to the net
wNative balance change, so native funding needs no quote adjustment. The caller
must quote callback-position sharesMinted; slippageTolerance applies to
that minimum but does not widen the exact assetsPaid comparison. It uses
loanToken directly without token metadata reads. The limit names the Blue
marketId (MarketUtils.getMarketId(blueSupply.market)) and the
callbackSalt; simulate() computes the callback address from the bundle
sender and that salt. Combining Blue parking with any collateral supplies is
unsupported and throws UnsupportedLimitActionError.
On chains with no registered wNative, sending native value with Blue parking
throws SlippageCheckUnavailableError.
toSimulationLimits also supports a funded maker publication produced by
supplyCollateralMakeBorrow: tx.action.type must be midnightCancelAndMake,
collateralSupplies must be non-empty, and blueSupply must be undefined.
It emits one operation per distinct collateral index, preserving first-seen
order and summing repeated indices: a non-zero root uses
midnightSupplyCollateralMakeBorrow, while a zero root uses
midnightSupplyCollateral. MidnightBundlesV2 acts for
msg.sender, so each operation uses owner as its collateral account;
onBehalf cannot change that. If the maker publishes supplies at more than one
index, omit expected because its quote cannot be assigned to an individual
operation; a defined expected field throws AmbiguousExpectedQuoteError.
If the maker's supply creates the market, the toMarket rule above applies:
pass the struct via market to toSimulationLimits, or, when no market is
passed, set assetPaid to collateralParams[collateralIndex].token on each
returned operation.
Otherwise, simulate() throws SlippageCheckUnavailableError.
Other Midnight actions—including midnightCancelAndMake bundles that combine
Blue parking with collateral supplies, contain unfunded publications, or
contain cancellations only—remain unsupported; pass limits: "none" only
when no covered operation is present.
When supplied collateral is wNative, the check counts ETH sent plus the
sender's net wNative change against assetsPaid, so native funding needs no
quote adjustment. Sending value when no supplied collateral is wNative, or on
a chain without registered wNative, throws SlippageCheckUnavailableError.
For Blue parking, the helper quotes the full amount parked. When the loan token
is wNative, the check adds the sender's ETH sent to the net wNative change, so
native funding needs no quote adjustment.
After fulfilling the maker output's token-approval and Midnight-authorization requirements, pass its action through the helper before simulation:
const output = await client.morpho.midnight(base.id).supplyCollateralMakeBorrow({
accountAddress: user,
offers: rateTree,
collateral: {
market,
supplies: [{ collateralIndex: 0n, assets: collateralAssets }],
},
deadline: maxUint256,
});
const tx = output.buildTx();
const result = await simulate(config, {
chainId: base.id,
transactions: [{ from: user, to: tx.to, data: tx.data, value: tx.value }],
limits: toSimulationLimits(tx.action, { owner: user }),
});midnightRedeem limits name the Midnight market (marketId, from morpho-sdk's
MidnightMarketUtils.toId). account is the credit owner (onBehalf) and
receiver the address paid. creditRedeemed caps the drop in account's
credit as returned by Midnight.updatePositionView(market, marketId, account),
read before and after the bundle: that is the credit after realized losses and
accrued continuous fees, so fees that accrue are not counted as redeemed credit.
assetsReceived is the receiver's balance delta in the market's loan token.
position.maxCredit caps the same credit after the bundle; 0n requires the
bundle to close the position and is not loosened by slippageTolerance.
Several redeems from one market in one bundle are checked against one
aggregate quote. midnightTakeBorrow, midnightTakeWithdraw and
midnightSupplyCollateralTakeBorrow are whole-bundle
sell limits that always measure the sender and take no account field; any
supplied account is rejected. Proceeds are measured on receiver in the
market's loan token.
midnightTakeBorrow.unitsSold and
midnightSupplyCollateralTakeBorrow.unitsSold cap the
net position decrease, computed from updatePositionView credit minus debt
before and after the bundle. midnightTakeWithdraw.creditRedeemed caps accrued
credit reduction and the entire bundle must leave debt unchanged. None of these sells
supports position or sharePrice bounds. The default
slippageTolerance also applies to the sell target's bounds; pass
slippageTolerance: 0n for exact bounds.
midnightTakeLend limits are whole-bundle limits for the sender too:
assetsPaid caps the loan-token balance decrease, and unitsBought is the
minimum increase in the account's net position (credit minus debt). Assets
targets derive assetsPaid from target.assets and unitsBought from
target.minUnits; units targets derive them from target.maxBuyerAssets and
target.units. On wNative loan markets, assetsPaid counts ETH sent plus the
sender's net wNative change against the full cap, whether native funding covers
part or all of it. Sending native value on a chain without registered wNative
also fails closed; send no ETH. Multiple takes in one market share one aggregate limit, with
the summed quote passed through expected. The default slippageTolerance
also applies to the buy target; pass slippageTolerance: 0n for exact bounds.
takeLend supports neither position nor sharePrice bounds.
Direct repay-withdraw limits always measure the sender and reject any explicit
account; they measure repayment, collateral withdrawal, and unchanged credit
across the whole bundle; without a debtRepaid quote (withdraw-only or full
repay), debt must not increase. A quoted repayment fails closed if loan-token
collateral is delivered to the payer; use another receiver or split repayment
and withdrawal into separate bundles. If native value is sent while wNative
collateral is delivered to the sender, use another receiver or send no ETH,
regardless of the loan market. Sending native value on a chain without
registered wNative also fails closed; send no ETH. debtRepaid is the
minimum debt reduction; quoted collateral is checked against both position
reduction and delivery to receiver. position.maxDebt and
position.maxCollateral are inclusive final-state bounds and are not scaled by
tolerance. A zero bound requires debt closure or an emptied collateral index.
midnightTakeRepayWithdrawCollateral (repayment through offers, with the
optional direct fallback) uses the same checks: it always measures the sender
and rejects any explicit account. An assets target quotes
assetsPaid: target.assets and debtRepaid: minUnits; a units target quotes
assetsPaid: maxBuyerAssets and debtRepaid: units; the maxUint256
full-debt target quotes assetsPaid: maxBuyerAssets and requires
position.maxDebt: 0n. The default slippageTolerance scales both quote
fields; pass slippageTolerance: 0n for exact bounds. position.maxDebt: 0n
is never scaled.
import { midnightRedeem } from "@morpho-org/morpho-sdk";
import { simulate, toSimulationLimits } from "@morpho-org/evm-simulation";
// An authorized operator redeems `units` of `owner`'s credit to `receiver`.
const tx = midnightRedeem({ chainId, market, units, onBehalf: owner, receiver });
const result = await simulate(config, {
chainId,
transactions: [{ from: operator, to: tx.to, data: tx.data, value: tx.value }],
limits: toSimulationLimits(tx.action, { owner: operator, onBehalf: owner, receiver }),
});For a sell, the target provides receiver proceeds and the position cap. Use a zero tolerance to enforce those contract bounds exactly:
import { midnightTakeBorrow } from "@morpho-org/morpho-sdk";
import { simulate, toSimulationLimits } from "@morpho-org/evm-simulation";
const tx = midnightTakeBorrow({
chainId, market, target, receiver, takeableOffers, deadline,
referralFeePct, referralFeeRecipient,
});
const result = await simulate(config, {
chainId,
transactions: [{ from: owner, to: tx.to, data: tx.data, value: tx.value }],
limits: toSimulationLimits(tx.action, {
owner, receiver, slippageTolerance: 0n,
}),
});All quote amounts must be > 0.
slippageTolerance is optional and defaults to blue-sdk's
DEFAULT_SLIPPAGE_TOLERANCE (0.03%); when supplied it must be below 1e18
(100%). Unknown input keys are rejected as SimulationValidationError rather
than silently ignored. The simulator never decodes calldata or fetches a quote
to infer constraints — set account/receiver from the connected user, never
from the transaction's own onBehalf. For midnightRedeem, set account to
the intended credit owner and receiver to the intended payee.
Optional extras keep their own rules: sharePrice bounds the realized
assets-per-share rate of an operation's priced quote pair, position bounds
the subject's post-bundle Blue position (or its Midnight credit, maxCredit,
on midnightRedeem), positions bounds the supply shares
an in-kind redeem delivers per market, and penalty/deallocations bound a
Vault V2 force exit's realized economics — penalty and deallocations are
checked only when quoted and never zero-rejected.
Preview authorizations, final-mode Blue authorization signatures, and every operation type require a Morpho Blue address
registered in blue-sdk's getChainAddresses; otherwise
UnsupportedChainError is thrown. Blue parking also requires registered
Midnight and midnightBlueBuyCallbackFactory addresses.
midnightAuthorizationSignature additionally requires registered Midnight and
EcrecoverAuthorizer addresses.
A missing required approval-registry address throws
ApprovalCheckUnavailableError. Bundler spenders (vaultBundlesV1,
vaultExitBundlesV1, blueBundlesV1) are allowed only where the registry lists
them; on other chains, grants to them need a limits.approvals entry. Preview authorizations and final-mode Blue signatures with limits: "none"
still require a registered Morpho Blue address and otherwise throw
UnsupportedChainError.
Approval allowlist
With SimulationLimits or SimulationInputLimits, the simulator derives allowed approvals from each
declared operation, its real vault or market token, the simulation sender, and
spender addresses in the chain registry. limits.approvals adds entries to
that set; it does not replace the derived entries. For example:
limits: {
operations: [operationLimit],
approvals: [
{
type: "token",
token: USDC,
owner: user,
spender: trustedContract,
},
{
type: "morphoAuthorization",
authorizer: user,
authorized: trustedContract,
},
{
type: "midnightAuthorization",
onBehalf: user,
authorized: trustedContract,
},
],
}The check rejects sender-granted ERC-20 allowances, ERC-721 token/operator
approvals, Permit2 approvals and permits, Morpho authorizations, and Midnight
setIsAuthorized grants on the registered Midnight contract that are outside the derived-plus-explicit set. It checks events across preparation and
user calls, and checks pending preview authorization requests before
eth_simulateV1, including permits that may be used outside this bundle.
Permit2 is optional: chains without a registered address do not derive Permit2
spenders, but Permit2-topic events are still decoded from any emitter and
checked against the allowlist.
Approvals of zero amount and revocations are allowed. The simulator does not
bound approval amounts, restrict call targets or recipients, or infer allowed
spenders from transaction calldata. Vault deposits additionally allow the
subject token directly to the vault; Blue pay-in legs additionally allow it
directly to Morpho Blue. Midnight pay-ins allow the paid token, read from
the pinned Midnight market or, for Blue parking (midnightSupplyBlueMakeLend),
from its declared loanToken, to Midnight and MidnightBundlesV2. For a first
midnightSupplyCollateral or midnightSupplyCollateralMakeBorrow into a
market the bundle creates, with no market struct, the explicit assetPaid is
the allowed token. Sender ERC-721
Approval and ApprovalForAll grants are always rejected and cannot be
allowlisted. Every Midnight operation allows a grant to midnightBundlesV2;
midnightSupplyCollateralMakeBorrow (emitted for
supplyCollateralMakeBorrow) and midnightSupplyBlueMakeLend also allow
priceRatifierV1 and rateRatifierV1; direct midnightSupplyCollateral
allows only midnightBundlesV2 and its token approvals.
Grants are checked whenever the sender is either onBehalf or caller,
whichever contract calls Midnight, including MidnightBundlesV2 and
EcrecoverAuthorizer; a sender-called grant for another account needs an
explicit matching midnightAuthorization entry.
Ratifier root ratification (SetIsRootRatified* on priceRatifierV1 /
rateRatifierV1) is not checked by limits.approvals.
ERC-777 operator grants (authorizeOperator) are not checked.
limits: "none" opts out of this check as
well as all limit checks.
Unexpected grants throw UnexpectedTokenApprovalError with token, owner,
spender, source ("event" or "authorization"), and allowlistable, or
UnexpectedMorphoAuthorizationError with authorizer, authorized, source,
and allowlistable, or UnexpectedMidnightAuthorizationError with onBehalf,
authorized, and allowlistable. These errors instruct the consumer not to
sign the request. A Midnight grant, or a token or Morpho grant whose
allowlistable is true, can be added to limits.approvals; ERC-721 grants,
Permit2 or Morpho signatures that name another verifying contract, and
EcrecoverAuthorizer signatures that name another verifying contract are never
allowlistable. If the subject token or
required chain registry address cannot be resolved, the RPC response omits logs
for a preparation or user call, or an approval-topic event cannot be decoded,
ApprovalCheckUnavailableError fails the simulation closed.
A missing or zero midnightBundlesV2 address throws ApprovalCheckUnavailableError, while
missing Morpho Blue or Midnight addresses still throw UnsupportedChainError.
Missing priceRatifierV1 or rateRatifierV1 addresses required by an operation
also throw ApprovalCheckUnavailableError.
Minimums — assetsReceived, supply/vault sharesMinted, debt sharesBurned,
collateralSupplied, debtRepaid, unitsBought (net Midnight position units,
on midnightTakeLend) and the collateralWithdrawals delivery — must be at
least ceil(quote * (1 - tolerance)); caps — assetsPaid,
supply/vault sharesBurned, debt sharesMinted, collateralWithdrawn,
creditRedeemed (Midnight credit units), unitsSold (net Midnight position
units, on midnightTakeBorrow) and the collateralWithdrawals position
decrease — must be at most floor(quote * (1 + tolerance)).
collateralSupplied and collateralWithdrawn are measured on the Blue
position's collateral, except on midnightSupplyCollateral and
midnightSupplyCollateralMakeBorrow, where
collateralSupplied is the Midnight collateral slot delta. Equality and
favorable movement pass.
const result = await simulate(config, {
chainId: 1,
transactions,
limits: {
operations: [{
type: "vaultV1Deposit",
vault,
account: user, // Share/position owner; defaults to transaction sender.
receiver: user, // Asset recipient; defaults to transaction sender.
quote: { sharesMinted: 1_000n, assetsPaid: 1_000n },
slippageTolerance: 10_000000000000000n, // 1%
}],
},
});
// result.verification.operations[0].checkedLimits contains this quote and tolerance.Typed-input asset overrides must match the canonical subject token. A native
ethAddress alias is allowed only for the chain's registered wNative; wallet
measurements then net ETH and wNative together, including extra wrapped-token
pulls or receipts. Quote-only overrides remain trusted as before.
Typed wNative measurements on the sender also include native ETH whenever the
request sends native value, even without an explicit native alias.
Requests that send native value with typed inputs must have that value covered
by a measured native or wNative side; otherwise simulation throws
SlippageCheckUnavailableError.
For combined operations with the same token and sender/receiver wallet, either
native alias applies to the shared ETH-plus-wNative net measurement on both sides.
With distinct wallets, each side uses only its own asset override.
Typed single-leg Blue operations also protect the subject's counter-side: supply and withdrawal forbid debt-share growth and collateral loss; borrow and repay forbid collateral loss and supply-share loss; collateral operations forbid debt-share growth and supply-share loss. Both combined Blue operations also forbid undeclared supply-share loss. These are separate position-delta guards, not percentage bounds or scalar share checks, and explicit quotes do not disable them. Multi-operation input bundles that move the undeclared counter-side in these adverse directions are rejected; quote-only behavior is unchanged. Use a supported combined operation, or quote-only limits with explicit position bounds for the intended bundle-wide effects.
Measurements cover the named subject over the whole bundle. Entries do not
attribute aggregate state changes to individual transactions. Use separate
simulations for per-transaction limits. Asset bounds use net wallet balance
changes (gas excluded). For native ETH, set assetPaid or assetReceived to
viem's ethAddress (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); other
sentinels such as zeroAddress are read as ERC-20 tokens. Share bounds use the
selected account's vault shares or Blue position shares. Borrow/repay entries
use debt shares; refinance selects
source shares burned and target shares minted; migration selects source vault
shares burned and target vault shares minted. Combined collateral/borrow actions
measure collateral paid and loan assets received; repay/collateral-withdraw actions
measure loan assets paid and collateral received.
For vaultV1InKindRedeem and vaultV2InKindRedeem, assetsReceived measures
only the receiver's wallet balance of the vault asset (the idle portion). These
operations must quote positions, the expected Blue
supply shares delivered to the receiver per market ({ marketId, supplyShares }),
or assetsReceived (an idle-only redeem delivers no position); each entry is
checked as a minimum output under the same tolerance. Quote supply
shares, not assets: convert a preview's delivered assets with the accrued
market's toSupplyShares(assets, "Down") at the simulated block, since interest
accrued between quote and execution lowers the shares delivered. positions is
rejected on every other operation type.
An operation may also carry a sharePrice bound { expected, tolerance } on
the realized assets-per-share rate, computed from the two quoted amounts the
operation prices — assetsPaid/sharesMinted for deposits and blueSupply,
assetsReceived/sharesBurned for withdrawals, redeems and blueWithdraw,
assetsReceived/sharesMinted for borrows, and assetsPaid/sharesBurned for
repays. expected is RAY-scaled (raw assets per raw share × 1e27, the scaling
VaultBundlesV1 maxSharePrice and VaultExitBundlesV1 minSharePriceE27 use)
and tolerance a WAD fraction from 0 to 1e18 inclusive. The realized rate is
measured on what moves in and out of the wallet. Referral fees, a flat share
of the amount on every route that takes referralFeePct, must be folded into
expected: price * WAD / (WAD - feePct) when a higher price is adverse
(deposits, supply, repays), price * (WAD - feePct) / WAD when a lower one is
(withdrawals, redeems, force exits, borrows). A force exit's minSharePriceE27
already nets the force-deallocation penalty — only its referral fee is
deducted after the check, so it needs only the referral-fee adjustment; never
apply the percentage formula to a penalty. For any other charge that isn't a
flat share of the amount, derive expected from the quoted delivered amount
(e.g. previewVaultV2ForceWithdraw) instead of a percentage. Both amounts
must be
quoted; a higher realized price is adverse for
deposits/supplies and repays, a lower one for withdrawals/redeems and borrows.
sharePrice is rejected on collateral-only operations, in-kind redeems,
blueRefinance, vaultV1MigrateToV2, midnightTakeBorrow,
midnightTakeWithdraw, midnightTakeLend,
midnightRepayWithdrawCollateral, midnightTakeRepayWithdrawCollateral,
midnightSupplyCollateralTakeBorrow and midnightSupplyBlueMakeLend, whose
deltas price no single conversion.
Blue operations may also carry a position bound on the subject's Blue
position after the whole bundle: { maxBorrowShares, minSupplyShares,
minCollateral }, any subset. These are absolute final values compared
inclusively, not deltas, and are not scaled by slippageTolerance. For
blueRefinance the bound reads the target market's position, where the debt
lands. position is rejected on vault operations. LTV and health bounds are
not covered: they need oracle and market reads the simulation does not take.
Vault V2 force exits (vaultV2ForceWithdraw, vaultV2ForceRedeem) may quote the execution-time economics no delta covers: penalty is the expected total penalty assets charged to the account across the bundle's force deallocations, and deallocations lists the expected assets withdrawn from each Blue market ({ marketId, assets }). Both are read from the simulated calls' logs: the penalty sums penaltyAssets over the vault's ForceDeallocate events for the account, and each of those events is paired, within the same call, with the nearest earlier Blue Withdraw from its adapter for the same amount, so liquidity-adapter withdrawals are not counted. A higher realized penalty or deallocated amount than quoted is adverse; a realized deallocation in a market not quoted violates.
Slippage and the optional sharePrice/position bounds are checked; refunds are
not checked.
sharesMinted and sharesBurned quotes for blueSupplyCollateral and
blueWithdrawCollateral are invalid and throw SimulationValidationError
during request parsing, as do collateralSupplied/collateralWithdrawn quotes
on any other operation. SlippageCheckUnavailableError reports runtime
evidence gaps: a failed state read, native outgoing traces that do not cover
value sent, a metadata read that reverts or returns empty/invalid data, a
quoted force exit with no vault Withdraw for the account, or a
ForceDeallocate that cannot be paired with a Blue Withdraw while
deallocations is quoted (for example a non-Blue adapter), or a
ForceDeallocate that charges a penalty to another account, or repay-withdraw
checks when a quote returns loan-token collateral to the payer, native value
is sent without registered wNative, or native value is sent while wNative
collateral is delivered to the sender, or a midnightTakeLend that sends
native value without registered wNative. A quoted
balance/position read whose non-empty return data cannot be decoded throws
InvalidSimulationResponseError, as does a matching ForceDeallocate/Withdraw
event log that fails to decode. Excessive slippage, a violated
sharePrice/position bound, a midnightTakeWithdraw bundle that
changes the sender's Midnight debt, or a midnightRepayWithdrawCollateral or
midnightTakeRepayWithdrawCollateral bundle that changes the sender's Midnight credit, increases debt on an
action without a debtRepaid quote, or under-delivers quoted collateral to
the receiver throws ConsumerLimitViolationError.
Only quoted amounts, position bounds and the sender's Midnight debt through
debt(id, account) for midnightTakeBorrow, midnightTakeWithdraw,
midnightSupplyCollateralTakeBorrow, midnightRepayWithdrawCollateral,
midnightTakeRepayWithdrawCollateral, and midnightTakeLend are observed.
midnightSupplyCollateralTakeBorrow also reads the sender's Midnight credit,
each quoted collateral index's position, and balanceOf for each wallet bound.
midnightTakeLend also reads the sender's
Midnight credit for unitsBought. The repay-withdraw operations also read the
sender's Midnight credit, each quoted collateral index's position, and the
receiver's balanceOf for that collateral token. ERC-20 assets and
vault shares use balanceOf; Blue shares use position, checked against the
after-bundle position for position bounds. Each distinct observation runs before
and after the bundle. Input-based derivation reads (vault previews, Blue accrual
and market/position reads, migration target totals) run only before the bundle;
these helpers never appear in simulationTxs, transfers or assetChanges.
Blue accrual still affects the bundle's per-call logs and gas, as described
under input-based limits.
Native amounts use the existing transfer traces. With limits: "none" —
reserved for transactions no operation type covers — there are no slippage
reads and verification.operations is empty. Quotes with an asset amount resolve asset() or market parameters only
when an explicit assetPaid or assetReceived was not supplied; derived asset
sides also resolve metadata, and Blue inputs always resolve market parameters.
Share-only quotes need no metadata reads. No vault factories, full entities, allocations, risk metrics, allowances,
or nonces are fetched for slippage.
For example, a Vault V2 deposit quoting assets paid and shares minted makes four
in-bundle view calls — the sender's asset balance and the account's share balance, each before
and after execution — plus one asset() read unless assetPaid is supplied. verification.operations[].checkedLimits records the quote
and tolerance checked, plus sharePrice and position when set, and input-based
fieldChecks identifying exact, minimum, maximum and quoted sides. The result does not include broad state snapshots or diffs;
transfers and assetChanges remain available.
The result's historical VerifiedSimulationResult name does not imply that
unchecked outcomes are economically verified.
Building quote-based limits with toSimulationLimits
toSimulationLimits(action, { owner, expected }) produces the
SimulationLimits of a transaction built by a morpho-sdk action (tx.action),
without touching the network. For operations that carry account, it is
owner, except on blueWithdrawBuyCallback, midnightSupplyCollateral and midnightRedeem, where it is
options.onBehalf ?? owner. Midnight sells always measure the sender and reject
any explicit account. midnightRepayWithdrawCollateral and
midnightTakeRepayWithdrawCollateral also always measure the sender and reject
any explicit account. Encoded args.onBehalf must
match the selected account. redeem.recipient on vaultV2ForceRedeem and
encoded receivers on midnightRedeem, midnightTakeBorrow,
midnightSupplyCollateralTakeBorrow and midnightTakeWithdraw must match
receiver ?? owner. On collateral-withdrawing
midnightRepayWithdrawCollateral and midnightTakeRepayWithdrawCollateral,
args.collateralReceiver must match the
same value. Mismatches throw LimitAccountMismatchError.
blueWithdrawBuyCallback maps to the existing blueWithdraw operation. Pass the
intended callback as onBehalf and its payout quote as expected.assetsReceived:
const limits = toSimulationLimits(tx.action, {
owner: maker,
onBehalf: callbackAddress,
expected: { assetsReceived: expectedAssets },
});This checks the callback's supply-share burn separately from the maker's wallet
proceeds. The action's callback must match onBehalf, and its receiver must match
owner; omitting onBehalf does not infer the callback from the action. Callback
registration, ownership and authorization checks remain in the SDK action's
getRequirements(). Offer cancellation and confirmation still happen separately.
For midnightSupplyBlueMakeLend, the helper sets account to owner;
simulate() rejects a raw limit whose account is not the bundle sender with
SlippageCheckUnavailableError, because MidnightBundlesV2 creates the
BlueBuyCallback for msg.sender.
The same applies to midnightSupplyCollateralMakeBorrow: its helper account
is owner, and simulate() rejects a raw limit whose account is not the
bundle sender with SlippageCheckUnavailableError (omit it or use the sender),
because MidnightBundlesV2 cancelAndMake supplies collateral for msg.sender.
Derived limits catch on-chain divergence from the encoded calldata, not wrong
inputs; pass the UI's quote through expected.
| Operation | Derived from args | Required via expected (when not derivable) | Not quotable |
| --- | --- | --- | --- |
| vaultV1Deposit / vaultV2Deposit | assetsPaid, sharesMinted (netAssets vs maxSharePrice) | — | — |
| vaultV1Withdraw / vaultV2Withdraw | assetsReceived (netAssets, net of fee) | sharesBurned | — |
| vaultV1Redeem / vaultV2Redeem | sharesBurned | assetsReceived | — |
| vaultV2ForceWithdraw | sharesBurned cap (exitAssets vs minSharePriceE27) | assetsReceived; sharesBurned when minSharePriceE27 is 0n | — |
| vaultV2ForceRedeem | — | sharesBurned (total burn including penalty shares — forceDeallocate burns extra shares from onBehalf), assetsReceived | — |
| vaultV1InKindRedeem / vaultV2InKindRedeem | — | sharesBurned and positions or assetsReceived (per-market Blue supply shares delivered to the receiver, or the idle wallet portion) | — |
| vaultV1MigrateToV2 | sharesBurned (shares mode), sharesMinted (when netAssets known) | either when not derivable | — |
| blueSupply | assetsPaid (gross; fee deducted inside) | sharesMinted | — |
| blueWithdraw | assetsReceived net of penalty and fee (assets mode); sharesBurned in shares mode | sharesBurned (assets mode), assetsReceived (shares mode) | — |
| blueWithdrawBuyCallback (emits blueWithdraw) | sharesBurned; account is the intended callback in onBehalf, receiver is owner | assetsReceived | Callback ownership and offer cancellation |
| blueBorrow | assetsReceived (borrowAssets − penalty − fee) | sharesMinted (debt-share cap) | — |
| blueSupplyCollateral | assetsPaid, collateralSupplied | — | — |
| midnightSupplyCollateral | assetsPaid, collateralSupplied (assets); account is options.onBehalf ?? owner; for wNative collateral, assetsPaid counts ETH sent plus the sender's net wNative change; native funding needs no quote adjustment, and sending value without wNative collateral or a registered wNative throws SlippageCheckUnavailableError | — | — |
| midnightCancelAndMake with non-empty collateralSupplies and no blueSupply | One operation per distinct collateral index; repeated indices are summed in first-seen order; non-zero roots emit midnightSupplyCollateralMakeBorrow, zero roots emit midnightSupplyCollateral; quote assetsPaid and collateralSupplied; account is owner; for wNative collateral, assetsPaid counts ETH sent plus the sender's net wNative change; native funding needs no quote adjustment, and sending value without wNative collateral or a registered wNative throws SlippageCheckUnavailableError | — | — |
| midnightCancelAndMake with blueSupply and no collateralSupplies (emits midnightSupplyBlueMakeLend) | exact assetsPaid (blueSupply.assets): sender debit of the full parked amount; for wNative, counts ETH sent plus net wNative change; combining with collateral supplies is unsupported | sharesMinted (the BlueBuyCallback's expected Blue supply-share increase) | Offer fillability |
| midnightRedeem | assetsReceived, creditRedeemed (units); account is options.onBehalf ?? owner, receiver is options.receiver ?? owner | — | — |
| midnightTakeLend | assetsPaid and unitsBought; always measures the sender, rejects any explicit account or receiver; a non-owner options.receiver throws LimitAccountMismatchError | — | — |
| midnightTakeBorrow | assetsReceived and unitsSold; always measures the sender, rejects any explicit account, a non-owner options.onBehalf throws LimitAccountMismatchError, receiver is options.receiver ?? owner | — | — |
| midnightTakeWithdraw | assetsReceived and creditRedeemed; always measures the sender, rejects any explicit account, a non-owner options.onBehalf throws LimitAccountMismatchError, receiver is options.receiver ?? owner; debt must remain unchanged | — | — |
| midnightRepayWithdrawCollateral | assetsPaid when repayment is active; debtRepaid for a positive asset repayment; finite collateralWithdrawals (summed per index) for any repay type; position.maxDebt: 0n on full repay; position.maxCollateral zero for withdraw-all; always measures sender and rejects any explicit account; receiver is options.receiver ?? owner; credit must remain unchanged; without a debtRepaid quote (withdraw-only or full repay), debt must not increase | collateralWithdrawals for each maxUint256 index | — |
| midnightTakeRepayWithdrawCollateral | assetsPaid (target.assets or maxBuyerAssets); debtRepaid (minUnits or finite units); position.maxDebt: 0n for units: maxUint256; collateral sides as midnightRepayWithdrawCollateral; always measures sender and rejects any explicit account; receiver is options.receiver ?? owner; credit must remain unchanged; without a debtRepaid quote (full debt), debt must not increase | collateralWithdrawals for each maxUint256 index | — |
| midnightSupplyCollateralTakeBorrow | collateralSupplies per-index amounts are exact minimum credit and pull-cap sources; unitsSold and assetsReceived; wallet spending and proceeds are checked together; owner native value is subtracted from the owner's wNative pull cap (the quoted wNative collateral), and excess fails closed even at 0n (e.g. owner receives wNative loan proceeds without quoted wNative collateral); native value on a chain without registered wNative fails closed with SlippageCheckUnavailableError; otherwise, without an owner wNative bound, native value is not checked; always measures the sender, rejects any explicit account, a non-owner options.onBehalf throws LimitAccountMismatchError, receiver is options.receiver ?? owner | — | — |
|
