# Common Patterns

### Integration Patterns

#### Pattern 1: Same-Chain Deposit (Hub Only)

Fastest and cheapest—no LayerZero fees.

```solidity
// Direct deposit on hub chain
uint256 usdtAmount = 1000 * 1e6;
uint256 minShares = 990 * 1e18;

IERC20(usdt).approve(depositPipe, usdtAmount);
uint256 shares = IDepositPipe(depositPipe).deposit(
    usdtAmount,
    msg.sender,
    msg.sender,
    minShares
);

// Shares minted instantly
```

**Note:** The `deposit` function signatures are:

* `deposit(uint256 assets, address receiver)` - ERC4626 standard
* `deposit(uint256 assets, address receiver, address controller)` - With controller
* `deposit(uint256 assets, address receiver, uint256 minShares)` - With slippage protection
* `deposit(uint256 assets, address receiver, address controller, uint256 minShares)` - Full control

The `mint` function signatures are:

* `mint(uint256 shares, address receiver)` - ERC4626 standard
* `mint(uint256 shares, address receiver, address controller)` - With controller
* `mint(uint256 shares, address receiver, uint256 maxAssets)` - With slippage protection
* `mint(uint256 shares, address receiver, address controller, uint256 maxAssets)` - Full control

#### Pattern 2: Cross-Chain Deposit (Spoke → Hub)

Deposit from any spoke, receive shares on hub.

```solidity
// On Arbitrum (spoke), send USDT0 to hub for shares
uint256 usdtAmount = 1000 * 1e6;
uint32 hubEid = 30167; // HyperEVM (example)

// Build compose message for deposit
// ACTION_DEPOSIT_ASSET params: (address targetAsset, bytes32 receiver, SendParam, uint256 minMsgValue, bytes32 feeRefundRecipient, uint32 originEid)
bytes memory composeMsg = abi.encode(
    uint8(1), // ACTION_DEPOSIT_ASSET
    abi.encode(
        usdtAddressOnHub,                    // targetAsset
        addressToBytes32(msg.sender),        // receiver (shares recipient if same chain)
        SendParam({
            dstEid: 0,                       // 0 = same chain, non-zero = cross-chain
            to: addressToBytes32(msg.sender), // Share recipient if cross-chain
            amountLD: 0,                     // Will be set by composer
            minAmountLD: 990 * 1e18,        // Min shares (slippage protection)
            extraOptions: "",
            composeMsg: "",
            oftCmd: ""
        }),
        uint256(0),                          // minMsgValue (native fee for compose)
        addressToBytes32(msg.sender),        // feeRefundRecipient
        uint32(0)                            // originEid (source chain for multi-hop refunds)
    )
);

SendParam memory sendParam = SendParam({
    dstEid: hubEid,
    to: addressToBytes32(composer),
    amountLD: usdtAmount,
    minAmountLD: usdtAmount,
    extraOptions: OptionsBuilder.newOptions().addExecutorLzComposeOption(0, 700_000, 0),
    composeMsg: composeMsg,
    oftCmd: ""
});

MessagingFee memory fee = IOFT(usdtOFT).quoteSend(sendParam, false);
IERC20(usdt).approve(usdtOFT, usdtAmount);
IOFT(usdtOFT).send{value: fee.nativeFee}(sendParam, fee, msg.sender);

// Shares received on hub in ~1-3 minutes
```

#### Pattern 3: Cross-Chain Deposit (Spoke → Hub → Different Spoke)

Deposit on one spoke, receive shares on another spoke.

```solidity
// Deposit USDT on Arbitrum, receive shares on Ethereum
uint256 usdtAmount = 1000 * 1e6;
uint32 hubEid = 30167; // HyperEVM
uint32 ethereumEid = 30101; // Ethereum

// Build compose message with cross-chain share distribution
bytes memory composeMsg = abi.encode(
    uint8(1), // ACTION_DEPOSIT_ASSET
    abi.encode(
        usdtAddressOnHub,
        addressToBytes32(address(msg.sender)),
        SendParam({
            dstEid: ethereumEid,                  // Send shares to Ethereum
            to: addressToBytes32(msg.sender), // Share recipient on Ethereum
            amountLD: 0,
            minAmountLD: 990 * 1e18,
            extraOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0),
            composeMsg: "",
            oftCmd: ""
        }),
        uint256(0),                          // minMsgValue for compose
        addressToBytes32(msg.sender),        // feeRefundRecipient
        getSourceEid()                       // originEid (Arbitrum)
    )
);

SendParam memory sendParam = SendParam({
    dstEid: hubEid,
    to: addressToBytes32(composer),
    amountLD: usdtAmount,
    minAmountLD: usdtAmount,
    extraOptions: OptionsBuilder.newOptions().addExecutorLzComposeOption(0, 700_000, 0),
    composeMsg: composeMsg,
    oftCmd: ""
});

MessagingFee memory fee = IOFT(usdtOFT).quoteSend(sendParam, false);
IERC20(usdt).approve(usdtOFT, usdtAmount);
IOFT(usdtOFT).send{value: fee.nativeFee}(sendParam, fee, msg.sender);

// Shares received on Ethereum in ~1-3 minutes
```

#### Pattern 4: Same-Chain Instant Redemption

Fastest redemption with highest fee.

```solidity
// Redeem shares for USDT on hub
uint256 shares = 1000 * 1e18;

IShareManager(shareManager).approve(redemptionPipe, shares);
uint256 usdtReceived = IRedemptionPipe(redemptionPipe).redeem(
    shares,
    msg.sender,
    msg.sender
);

// USDT received instantly (after fee)
```

**Alternative: Withdraw specific asset amount**

```solidity
// Withdraw specific amount of assets (net, after fees)
uint256 desiredAssets = 1000 * 1e6; // Want 1000 USDT net

IShareManager(shareManager).approve(redemptionPipe, type(uint256).max);
uint256 sharesBurned = IRedemptionPipe(redemptionPipe).withdraw(
    desiredAssets,
    msg.sender,
    msg.sender
);

// Shares burned, assets received (after fee)
```

**Note:**

* Instant redemption (`redeem`) applies a fee (`instantRedeemFeeBps`) which is deducted from the assets received. The fee stays with the liquidity provider.
* `withdraw` calculates the required shares based on the net asset amount (after fees), while `redeem` burns a specific number of shares.

#### Pattern 5: Cross-Chain Redemption (Spoke → Hub → Spoke)

Redeem shares on spoke for assets on hub or different spoke.

```solidity
// On Ethereum, redeem shares for USDT on hub
uint256 shares = 1000 * 1e18;
uint32 hubEid = 30167; // HyperEVM

// Build compose message for redemption
// ACTION_REDEEM_SHARES params: (address receiver, SendParam, uint256 minMsgValue, uint256 minAssets, bytes32 feeRefundRecipient, uint32 originEid)
bytes memory composeMsg = abi.encode(
    uint8(2), // ACTION_REDEEM_SHARES
    abi.encode(
        msg.sender,                          // receiver (USDT recipient if same chain)
        SendParam({
            dstEid: 0,                       // 0 = same chain, non-zero = cross-chain
            to: addressToBytes32(msg.sender), // Asset recipient if cross-chain
            amountLD: 0,                     // Will be set by composer
            minAmountLD: 990 * 1e6,          // Min USDT (slippage protection)
            extraOptions: "",
            composeMsg: "",
            oftCmd: ""
        }),
        uint256(0),                          // minMsgValue (native fee for compose)
        990 * 1e6,                           // minAssets (slippage protection)
        addressToBytes32(msg.sender),        // feeRefundRecipient
        getSourceEid()                       // originEid (Ethereum
        
        )
    )
);

SendParam memory sendParam = SendParam({
    dstEid: hubEid,
    to: addressToBytes32(composer),
    amountLD: shares,
    minAmountLD: shares,
    extraOptions: OptionsBuilder.newOptions().addExecutorLzComposeOption(0, 700_000, 0),
    composeMsg: composeMsg,
    oftCmd: ""
});

MessagingFee memory fee = IOFT(shareOFT).quoteSend(sendParam, false);
IShareManager(shareERC20).approve(shareOFT, shares);
IOFT(shareOFT).send{value: fee.nativeFee}(sendParam, fee, msg.sender);

// USDT received on hub in ~1-3 minutes
```

#### Pattern 6: Transfer Shares Between Spokes

Simple cross-chain share transfer (no vault operation).

```solidity
// Transfer shares from Arbitrum to Ethereum (no deposit/redeem)
uint256 shares = 1000 * 1e18;
uint32 ethereumEid = 30101;

SendParam memory sendParam = SendParam({
    dstEid: ethereumEid,
    to: addressToBytes32(msg.sender),
    amountLD: shares,
    minAmountLD: shares,
    extraOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0),
    composeMsg: "", // No compose message
    oftCmd: ""
});

MessagingFee memory fee = IOFT(shareOFT).quoteSend(sendParam, false);
IShareManager(shareERC20).approve(shareOFT, shares);
IOFT(shareOFT).send{value: fee.nativeFee}(sendParam, fee, msg.sender);

// Shares arrive on Ethereum in ~1-3 minutes
```

#### Pattern 7: Direct Composer Methods (Hub Only)

For same-chain operations on the hub, you can use direct composer methods:

```solidity
// Deposit and send shares cross-chain in one call
uint256 usdtAmount = 1000 * 1e6;
uint32 ethereumEid = 30101;

SendParam memory shareSendParam = SendParam({
    dstEid: ethereumEid,
    to: addressToBytes32(msg.sender),
    amountLD: 0, // Will be set by composer
    minAmountLD: 990 * 1e18,
    extraOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0),
    composeMsg: "",
    oftCmd: ""
});

IERC20(usdt).approve(composer, usdtAmount);
MessagingFee memory fee = IOFT(shareOFT).quoteSend(shareSendParam, false);
IOVaultComposerMulti(composer).depositAssetAndSend{value: fee.nativeFee}(
    usdt,
    usdtAmount,
    shareSendParam,
    msg.sender // refund address
);

// Redeem and send assets cross-chain in one call
uint256 shares = 1000 * 1e18;

SendParam memory assetSendParam = SendParam({
    dstEid: ethereumEid,
    to: addressToBytes32(msg.sender),
    amountLD: 0, // Will be set by composer
    minAmountLD: 990 * 1e6,
    extraOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0),
    composeMsg: "",
    oftCmd: ""
});

IShareManager(shareERC20).approve(composer, shares);
MessagingFee memory fee = IOFT(underlyingAssetOFT).quoteSend(assetSendParam, false);
IOVaultComposerMulti(composer).redeemAndSend{value: fee.nativeFee}(
    shares,
    assetSendParam,
    msg.sender // refund address
);
```

#### Pattern 8: Standard Redemption Request (Hub Only)

Standard redemption requests are fulfilled later by the protocol.

```solidity
// Request standard redemption
uint256 shares = 1000 * 1e18;

// Transfer shares to redemption pipe (they will be held in custody)
IShareManager(shareManager).transfer(redemptionPipe, shares);

uint256 requestId = IRedemptionPipe(redemptionPipe).requestRedeem(
    shares,
    msg.sender,  // receiver
    msg.sender,  // controller
    msg.sender   // owner
);

// Later, a FULFILL_MANAGER_ROLE will fulfill the request
// Users receive assets after fulfillment (no fees)
```

### Developer Guide

#### 1. Reading NAV and Share Prices

```solidity
// Get current NAV (18 decimals)
uint256 nav = INAVOracle(navOracle).getNAV();

// Get total share supply (18 decimals)
uint256 supply = IShareManager(shareManager).totalSupply();

// Calculate share price (18 decimals)
uint256 sharePrice = (nav * 1e18) / supply;

// Calculate user's USD value
uint256 userShares = IShareManager(shareManager).balanceOf(user);
uint256 userValue = (userShares * nav) / supply;
```

#### 2. Handling Different Asset Decimals

All internal calculations use 18 decimals, but deposit assets may vary.

```solidity
// Normalize asset amount to 18 decimals
function normalizeToDecimals18(uint256 amount, uint8 decimals)
    internal
    pure
    returns (uint256)
{
    if (decimals == 18) return amount;
    if (decimals < 18) {
        return amount * (10 ** (18 - decimals));
    } else {
        return amount / (10 ** (decimals - 18));
    }
}

// Denormalize from 18 decimals to asset decimals
function normalizeFromDecimals18(uint256 amount18, uint8 decimals)
    internal
    pure
    returns (uint256)
{
    if (decimals == 18) return amount18;
    if (decimals < 18) {
        return amount18 / (10 ** (18 - decimals));
    } else {
        return amount18 * (10 ** (decimals - 18));
    }
}
```

**Example:**

```solidity
// User wants to deposit $1000 worth of USDT (6 decimals)
uint256 usdtAmount = 1000 * 1e6; // 1000 USDT

// Preview shares (returns 18 decimals)
uint256 expectedShares = IDepositPipe(depositPipe).previewDeposit(usdtAmount);
// expectedShares might be 1000 * 1e18 (if 1:1 ratio)

// User wants 1000 shares (18 decimals)
uint256 desiredShares = 1000 * 1e18;

// Preview required USDT (returns 6 decimals)
uint256 requiredUSDT = IDepositPipe(depositPipe).previewMint(desiredShares);
// requiredUSDT might be 1000 * 1e6
```

#### 3. Slippage Protection Best Practices

Always use slippage protection for better UX.

```solidity
// For deposits: calculate minimum shares
uint256 depositAmount = 1000 * 1e6; // USDT
uint256 expectedShares = IDepositPipe(depositPipe).previewDeposit(depositAmount);
uint256 minShares = (expectedShares * 99) / 100; // 1% slippage
IDepositPipe(depositPipe).deposit(depositAmount, receiver, controller, minShares);

// For mints: calculate maximum assets
uint256 desiredShares = 1000 * 1e18;
uint256 expectedAssets = IDepositPipe(depositPipe).previewMint(desiredShares);
uint256 maxAssets = (expectedAssets * 101) / 100; // 1% slippage
// Note: Use mint(shares, receiver, maxAssets) or mint(shares, receiver, controller, maxAssets)
IDepositPipe(depositPipe).mint(desiredShares, receiver, maxAssets);

// For redemptions: calculate minimum assets
uint256 sharesToRedeem = 1000 * 1e18;
uint256 expectedAssets = IRedemptionPipe(redemptionPipe).previewRedeem(sharesToRedeem);
uint256 minAssets = (expectedAssets * 99) / 100; // 1% slippage

// Note: For instant redemptions, slippage is implicit in fee
// For cross-chain redemptions, include minAssets in compose message
```

#### 4. Gas Estimation for LayerZero Operations

Always get fee quotes before cross-chain operations.

```solidity
// Quote fee for cross-chain send
SendParam memory sendParam = /* ... */;
MessagingFee memory fee = IOFT(oft).quoteSend(sendParam, false);

// Total gas needed
uint256 totalGas = fee.nativeFee;

// For compose operations, add compose gas
if (sendParam.composeMsg.length > 0) {
    totalGas += estimateComposeGas(); // 0.01-0.05 ETH typical
}

// Execute with proper value
IOFT(oft).send{value: totalGas}(sendParam, fee, refundAddress);    
```

#### 5. Error Handling

Common errors and how to handle them:

```solidity
// DepositPipe errors
try IDepositPipe(depositPipe).deposit(amount, receiver, controller, minShares)
    returns (uint256 shares) {
    // Success
} catch Error(string memory reason) {
    if (keccak256(bytes(reason)) == keccak256("DepositPipe: slippage exceeded")) {
        // Handle slippage - increase minShares or retry
    }
    if (keccak256(bytes(reason)) == keccak256("DepositPipe: shares below minimum")) {
        // Amount too small - increase deposit
    }
    if (keccak256(bytes(reason)) == keccak256("ShareManager: max deposit exceeded")) {
        // User or global deposit limit exceeded
    }
    if (keccak256(bytes(reason)) == keccak256("DepositPipe: max supply exceeded")) {
        // Global supply limit exceeded
    }
}

// RedemptionPipe errors
try IRedemptionPipe(redemptionPipe).redeem(shares, receiver, controller)
    returns (uint256 assets) {
    // Success
} catch Error(string memory reason) {
    if (keccak256(bytes(reason)) == keccak256("RedemptionPipe: insufficient liquidity")) {
        // Instant redemption unavailable - use fast/standard
    }
    if (keccak256(bytes(reason)) == keccak256("RedemptionPipe: insufficient shares")) {
        // User doesn't have enough shares
    }
    if (keccak256(bytes(reason)) == keccak256("RedemptionPipe: shares below minimum")) {
        // Amount too small
    }
    if (keccak256(bytes(reason)) == keccak256("RedemptionPipe: maximum redeem per user exceeded")) {
        // User redemption limit exceeded
    }
}

// LayerZero errors
try IOFT(oft).send{value: fee.nativeFee}(sendParam, fee, refund)
    returns (MessagingReceipt memory receipt) {
    // Success - track receipt.guid on LayerZero scanner
} catch {
    // Insufficient gas, invalid parameters, or OFT paused
}
```

#### 6. Monitoring Cross-Chain Transactions

Track LayerZero messages using the GUID.

```solidity
// After sending cross-chain
MessagingReceipt memory receipt = IOFT(oft).send{value: fee}(sendParam, fee, refund);
bytes32 guid = receipt.guid;

// Emit event for frontend tracking
emit CrossChainOperationInitiated(guid, sendParam.dstEid, user);

// Users can track on: https://layerzeroscan.com/tx/{guid}
```

**Frontend Integration:**

```javascript
// Listen for LayerZero events
const receipt = await oft.send(sendParam, fee, refund, {value: totalGas});
const guid = receipt.guid;

// Poll LayerZero scanner API
const checkStatus = async (guid) => {
  const response = await fetch(`https://api.layerzeroscan.com/tx/${guid}`);
  const data = await response.json();
  return data.status; // "INFLIGHT", "DELIVERED", "FAILED"
};
```

#### 7. Important Limits and Constraints

```solidity
// Check minimum share amounts
uint256 minShares = IDepositPipe(depositPipe).MIN_AMOUNT_SHARES();
uint256 minRedeemShares = IRedemptionPipe(redemptionPipe).MIN_AMOUNT_SHARES();

// Check deposit limits
uint256 maxDeposit = IShareManager(shareManager).maxDeposit();
uint256 maxSupply = IShareManager(shareManager).maxSupply();
uint256 userBalance = IShareManager(shareManager).balanceOf(user);
uint256 remainingCapacity = maxDeposit > userBalance ? maxDeposit - userBalance : 0;

// Check redemption limits
uint256 maxWithdraw = IShareManager(shareManager).maxWithdraw();
uint256 userBalance = IShareManager(shareManager).balanceOf(user);
uint256 maxRedeem = IRedemptionPipe(redemptionPipe).maxRedeem(user);
uint256 maxWithdrawAssets = IRedemptionPipe(redemptionPipe).maxWithdraw(user);
```

#### 8. Fee Information

```solidity
// Get redemption fees
(uint256 instantFeeBps, _) = IRedemptionPipe(redemptionPipe).fees();
// Fees are in basis points (10000 = 100%)

// Calculate instant redemption fee
uint256 shares = 1000 * 1e18;
uint256 assets = IRedemptionPipe(redemptionPipe).convertToAssets(shares);
uint256 fee = (assets * instantFeeBps) / 10000;
uint256 assetsAfterFee = assets - fee;
```

#### 9. Operator Pattern

The ShareManager supports an operator pattern for contract-based integrations:

```solidity
// User sets operator
IShareManager(shareManager).setOperator(operatorContract, true);
// Operator can now act on behalf of user
```

#### 10. Blacklist Handling

Users can be blacklisted, preventing deposits, redemptions, and transfers:

```solidity
// Check if user is blacklisted
bool isBlacklisted = IShareManager(shareManager).isBlacklisted(user);

// If blacklisted, operations will revert with:
// "ShareManager: receiver address is blacklisted"
// "ShareManager: sender address is blacklisted"
```

#### 11. Pause Handling

All contracts support emergency pause:

```solidity
// Check if contract is paused
bool isPaused = Pausable(depositPipe).paused();

// If paused, operations will revert with:
// "Pausable: paused"
```

#### 12. Composer Message Structure Reference

**ACTION\_DEPOSIT\_ASSET (1):**

```solidity
abi.encode(
    address targetAsset,        // Asset to deposit on hub
    bytes32 receiver,           // Share recipient if same chain (bytes32(0) if cross-chain)
    SendParam shareSendParam,   // Share distribution (dstEid=0 for same chain)
    uint256 minMsgValue,        // Minimum native fee for compose operation
    bytes32 feeRefundRecipient, // Fee refund recipient (bytes32(0) = depositor)
    uint32 originEid            // Source chain for multi-hop refunds
)
```

**ACTION\_REDEEM\_SHARES (2):**

```solidity
abi.encode(
    address receiver,           // Asset recipient if same chain
    SendParam assetSendParam,   // Asset distribution (dstEid=0 for same chain)
    uint256 minMsgValue,        // Minimum native fee for compose operation
    uint256 minAssets,          // Minimum assets (slippage protection)
    bytes32 feeRefundRecipient, // Fee refund recipient (bytes32(0) = redeemer)
    uint32 originEid            // Source chain for multi-hop refunds
)
```

#### 13. Helper Functions

```solidity
// Convert address to bytes32 for LayerZero
function addressToBytes32(address _addr) internal pure returns (bytes32) {
    return bytes32(uint256(uint160(_addr)));
}

// Convert bytes32 to address
function bytes32ToAddress(bytes32 _b) internal pure returns (address) {
    return address(uint160(uint256(_b)));
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.liminal.money/tokenized/developers/common-patterns.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
