Skip to main content

Integration Guide

Current Phase: Shadow Mainnet Testing

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:

Addresses & Deployments


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

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 = 42161 on Arbitrum
  • verifyingContract = 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

ConditionBehavior
referrer == address(0)Deposit succeeds, referral binding skipped
Referrer already boundEmits ReferralBindingSkipped, deposit still succeeds
Self-referralBinding skipped, deposit still succeeds
Unknown binding errorDeposit reverts
Expired Permit2 signatureReverts
Reused nonceReverts
Deposits pausedReverts
NAV staleReverts
amount == 0Reverts

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

ModeCallBehavior
InstantrequestClaim(true, shares)Attempts immediate exit if liquidity and cap allow
StandardrequestClaim(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:

  1. createdClaimRequested
  2. queuedClaimQueued
  3. eligible — derived off-chain after lock period
  4. settledClaimSettled
  5. cancelledClaimCancelled

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 InstantExit is emitted → exit settled immediately
  • otherwise parse ClaimRequested and 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, and ClaimCancelled for claim-history UX
  • use InstantExit to tag same-transaction exits
  • do not rely only on ERC-4626 Withdraw

Withdrawal Edge Cases

ConditionBehavior
Insufficient instant liquidityFalls through to queued
Epoch cap exhaustedFalls through to queued
Claim too smallReverts
Claim cooldown activeReverts
Too many claims in epochReverts
Withdrawals pausedReverts
Non-owner calls cancelClaimReverts
Cancel after settlementReverts
Queue not yet eligibleRemains pending
NAV stale during settlementClaim 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() and maxRedeem() 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.