Integration Guide
Multyr contracts are deployed on Arbitrum One. The system is currently in validation phase. Deposits are not open to the public. Behavior described on this page reflects the protocol's designed behavior; some mechanisms are active in shadow testing, others become active at public launch. See the Status page for details.
This guide explains how to integrate with Multyr using the actual protocol flows.
Multyr differs from a standard ERC-4626 integration in two important ways:
- deposits are designed to use DepositRouter + Permit2
- withdrawals follow a claim-based exit model
While Multyr exposes an ERC-4626 interface for compatibility, its execution model differs from a pure synchronous vault. Integrators should use the router for deposits and the claim system for withdrawals.
Mental Model
Deposit → Router → Vault → Strategies
Withdraw → Claim → Instant OR Queue → Settlement
Integration Overview
Primary Deposit Flow
User → Permit2 signature → DepositRouter → CoreVault
Primary Withdrawal Flow
User → requestClaim(...) → instant exit or queued settlement
Core Contracts for Integrators
Most frontend, backend, and SDK integrations only need to interact with:
- USDC — input asset
- DepositRouter — primary deposit entry
- CoreVault — balances, reads, and claim-based exits
See:
Address Handling
Contract addresses may vary depending on:
- deployment version
- chain
- system state (pre-seal vs sealed)
- vault instance
Integrators should always use the active deployment listed in the deployment reference.
Deposits
Primary Path — DepositRouter + Permit2
The primary deposit path uses the router and Permit2.
This provides:
- a single transaction deposit flow
- permit-based token transfer
- optional referral binding
Router Functions
Mode A — Per-Deposit Signature (Recommended)
function depositWithPermit2Transfer(
uint256 amount,
address referrer,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external returns (uint256 shares);
Mode B — Allowance Reuse
function depositWithPermit2Allowance(
uint256 amount,
address referrer,
uint160 permitAmount,
uint48 permitExpiration,
uint48 permitNonce,
uint256 permitDeadline,
bytes calldata permitSignature
) external returns (uint256 shares);
Deposit Semantics
Important properties:
- the receiver is always
msg.sender - the router does not allow overriding the receiver
referrer = address(0)skips referral binding- referral binding is best-effort for known safe cases
- unknown binding failures revert the transaction
Permit2 Structures
Mode A — PermitTransferFrom
struct TokenPermissions {
address token;
uint256 amount;
}
struct PermitTransferFrom {
TokenPermissions permitted;
uint256 nonce;
uint256 deadline;
}
Mode B — PermitSingle
struct PermitDetails {
address token;
uint160 amount;
uint48 expiration;
uint48 nonce;
}
struct PermitSingle {
PermitDetails details;
address spender;
uint256 sigDeadline;
}
Permit2 domain uses the standard Uniswap Permit2 domain:
name = "Permit2"chainId = 42161on ArbitrumverifyingContract = 0x000000000022D473030F116dDEE9F6B43aC78BA3
Real Example — Deposit via Permit2 Transfer
This is the primary recommended path.
function test_depositWithPermit2Transfer_signAndDeposit() public {
uint256 depositAmount = 1_000e6; // 1,000 USDC
uint256 nonce = 0;
uint256 deadline = block.timestamp + 1 hours;
// [EIP-712 signing happens here — produces `signature` bytes]
vm.prank(user);
uint256 shares =
router.depositWithPermit2Transfer(depositAmount, referrer, nonce, deadline, signature);
assertGt(shares, 0, "Must receive non-zero shares");
}
Replay Protection
Permit2 signatures cannot be reused with the same nonce.
uint256 shares = router.depositWithPermit2Transfer(
depositAmount,
address(0),
nonce,
deadline,
signature
);
// Second use of the same nonce reverts
vm.expectRevert();
router.depositWithPermit2Transfer(
depositAmount,
address(0),
nonce,
deadline,
signature
);
Mode B — Allowance Reuse
This path is useful when reusing an existing Permit2 allowance.
// First call — sets Permit2 allowance + deposits
router.depositWithPermit2Allowance(
500e6,
referrer,
type(uint160).max,
uint48(block.timestamp + 365 days),
0,
block.timestamp + 1 hours,
signature
);
// Second call — empty signature, reuse allowance
router.depositWithPermit2Allowance(
300e6,
address(0),
0,
0,
0,
0,
""
);
Router Events
event DepositWithReferral(
address indexed user,
address indexed referrer,
uint256 assets,
uint256 shares
);
event ReferralBindingSkipped(
address indexed user,
address indexed referrer,
bytes reason
);
Frontend note:
- the router does not distinguish Permit2 mode in events
- use transaction path context if you need to classify Mode A vs Mode B
Deposit Edge Cases
| Condition | Behavior |
|---|---|
referrer == address(0) | Deposit succeeds, referral binding skipped |
| Referrer already bound | Emits ReferralBindingSkipped, deposit still succeeds |
| Self-referral | Binding skipped, deposit still succeeds |
| Unknown binding error | Deposit reverts |
| Expired Permit2 signature | Reverts |
| Reused nonce | Reverts |
| Deposits paused | Reverts |
| NAV stale | Reverts |
amount == 0 | Reverts |
Direct Vault Deposit (Fallback)
Direct ERC-4626 deposit is supported for advanced integrations.
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
function mint(uint256 shares, address receiver) external returns (uint256 assets);
This path requires standard ERC20 approval first.
It is useful when:
- the integrator cannot sign EIP-712 data
- the caller is a contract
- a simpler non-router flow is preferred
However, it is not the primary user-facing integration path.
Withdrawals
Primary Exit Model — Claim-Based
Withdrawals are not primarily synchronous ERC-4626 operations.
Standard ERC-4626 withdraw() and redeem() exist for interface compatibility, but the primary flow is claim-based.
Important Compatibility Note
These ERC-4626 methods should not be exposed as the primary UX path:
function withdraw(uint256, address, address) external returns (uint256); // reverts
function redeem(uint256, address, address) external returns (uint256); // reverts
In the current architecture, withdrawals are handled via requestClaim(...).
Primary Exit Function
function requestClaim(bool immediate, uint256 shares) external;
This function supports two modes.
Exit Modes
| Mode | Call | Behavior |
|---|---|---|
| Instant | requestClaim(true, shares) | Attempts immediate exit if liquidity and cap allow |
| Standard | requestClaim(false, shares) | Queues claim for later settlement |
If immediate = true but instant liquidity or per-epoch cap is insufficient, the request falls back to queued mode instead of reverting.
Standard Exit (Queued)
await vault.requestClaim(false, shares)
Behavior:
- shares are escrowed
- a claim is created
- claim is placed into the queue
- settlement happens later via keeper execution
The user does not need to send a second transaction to receive assets once the queued claim is settled. Settlement transfers USDC in the settlement transaction itself.
Instant Exit
await vault.requestClaim(true, shares)
Behavior:
- attempts same-transaction exit
- subject to available buffer liquidity
- subject to per-epoch cap
- applies instant fee model if successful
If the instant path cannot be satisfied:
- the request falls through to the queued path
- the user does not lose the exit request
Claim Lifecycle
A claim moves through the following states:
- created —
ClaimRequested - queued —
ClaimQueued - eligible — derived off-chain after lock period
- settled —
ClaimSettled - cancelled —
ClaimCancelled
Instant claims do not persist as queue entries.
Claim Management Functions
function cancelClaim(uint256 claimId) external;
function processQueuedRedemptions(uint256 maxClaims) external;
function settleFeesAndProcessQueue(uint256 maxClaims) external;
function endEpochCrystallize() external;
Useful views:
function nextClaimId() external view returns (uint256);
function queueLength() external view returns (uint256);
function pendingShares() external view returns (uint256);
Real Integration Pattern — Queued Exit
await vault.requestClaim(false, shares)
Then:
- track
ClaimRequested - watch for
ClaimSettled - optionally allow
cancelClaim(claimId)while still pending
No separate claim() transaction is required for queued settlement.
Real Integration Pattern — Instant Exit
await vault.requestClaim(true, shares)
Frontend rule:
- if
InstantExitis emitted → exit settled immediately - otherwise parse
ClaimRequestedand treat it as queued
Withdrawal Events
Canonical events for exit UX:
event ClaimRequested(uint256 indexed claimId, address indexed user, uint256 shares, bool immediate);
event SharesFrozen(address indexed user, uint256 shares, uint256 claimId);
event ClaimQueued(uint256 indexed claimId);
event ClaimDequeued(uint256 indexed claimId);
event ClaimSettled(uint256 indexed claimId, address indexed user, uint256 netAssets);
event ClaimCancelled(uint256 indexed claimId, address indexed user);
event SharesUnfrozen(address indexed user, uint256 shares, uint256 claimId);
event InstantExit(address indexed user, uint256 shares, uint256 netAssets, uint256 feeShares);
event ForceExit(address indexed user, uint256 shares, uint256 netAssets, uint256 feeShares);
event ImmediateExitPenaltyApplied(address indexed user, uint256 penaltyAssets, uint256 penaltyShares);
event WithdrawFeeTaken(address indexed sender, uint256 assetsFee, uint256 sharesToTreasury);
event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);
Frontend guidance:
- use
ClaimRequested,ClaimSettled, andClaimCancelledfor claim-history UX - use
InstantExitto tag same-transaction exits - do not rely only on ERC-4626
Withdraw
Withdrawal Edge Cases
| Condition | Behavior |
|---|---|
| Insufficient instant liquidity | Falls through to queued |
| Epoch cap exhausted | Falls through to queued |
| Claim too small | Reverts |
| Claim cooldown active | Reverts |
| Too many claims in epoch | Reverts |
| Withdrawals paused | Reverts |
Non-owner calls cancelClaim | Reverts |
| Cancel after settlement | Reverts |
| Queue not yet eligible | Remains pending |
| NAV stale during settlement | Claim remains queued until safe settlement |
Read-Only Integration
Common reads for frontends and dashboards:
function asset() external view returns (address);
function totalAssets() external view returns (uint256);
function totalSupply() external view returns (uint256);
function balanceOf(address owner) external view returns (uint256);
function convertToAssets(uint256 shares) external view returns (uint256);
function convertToShares(uint256 assets) external view returns (uint256);
function previewDeposit(uint256 assets) external view returns (uint256);
function previewMint(uint256 shares) external view returns (uint256);
function previewWithdraw(uint256 assets) external view returns (uint256); // informational
function previewRedeem(uint256 shares) external view returns (uint256); // informational
function maxDeposit(address) external view returns (uint256);
function maxMint(address) external view returns (uint256);
function maxWithdraw(address) external view returns (uint256); // returns 0 under async model
function maxRedeem(address) external view returns (uint256); // returns 0 under async model
Important note:
maxWithdraw()andmaxRedeem()are not meaningful as "total withdrawable balance" under the async model- use claim-based UX instead of assuming pure ERC-4626 sync exits
Useful Frontend Metrics
TVL
totalAssets()
User Shares
balanceOf(user)
User Asset Value
convertToAssets(balanceOf(user))
Share Price
pricePerShare = totalAssets / totalSupply
Queue Metrics
queueLength()
pendingShares()
Fees
Fees are applied during deposit, withdrawal, and performance crystallization.
Developers should rely on returned values (shares, assets) rather than assuming fee-free transfers.
See Economics → Fee Flow for the full fee structure and routing.
Integration Best Practices
- use DepositRouter + Permit2 as the primary deposit path
- treat direct
deposit()as an advanced fallback - use claim-based exits as the primary withdrawal model
- do not expose
withdraw()/redeem()as the main UX path - do not hardcode dynamic deployment addresses
- monitor canonical events for UX and indexing
- rely on returned values to compute user amounts (fees are already applied)
Summary
Multyr should be integrated using the actual execution model, not generic vault assumptions.
Primary patterns are:
- deposit → DepositRouter + Permit2
- withdraw → requestClaim(immediate | queued)
This reflects the real protocol behavior and provides the safest and most accurate integration surface.