Diffusal

On-Chain Guide

A narrative walkthrough of the full trading lifecycle on Diffusal — from funding your wallet to settlement at expiry

This guide walks you through a complete on-chain trade on Diffusal: fund a wallet, deposit collateral, place an order, verify your fill, withdraw funds, and understand what happens at settlement and expiry. It is intended for developers integrating directly with Diffusal contracts using Foundry's Cast CLI — no TypeScript SDK required.

Prerequisites

  • Install Foundry: curl -L https://foundry.paradigm.xyz | bash && foundryup
  • Have MON for gas on Monad Testnet (chain ID 10143). You can bridge or request MON from community faucets.
  • Export your private key as an environment variable:
    export PRIVATE_KEY=0xYOUR_PRIVATE_KEY
    export WALLET_ADDRESS=0xYOUR_WALLET_ADDRESS
  • All commands use the Monad Testnet RPC: https://testnet-rpc.monad.xyz

Never commit or share your $PRIVATE_KEY. Use a dedicated testnet wallet with no mainnet funds.


Your First Trade

Step 1: Fund Your Wallet

Diffusal's testnet uses TestnetUSDT as the collateral token (6 decimals). You can claim 100,000 TestnetUSDT from the built-in faucet once every 24 hours.

cast send 0xf75610dbe4aff5c2c3b2fd5a840b2e92e7b413bc \
  "faucet()" \
  --rpc-url https://testnet-rpc.monad.xyz \
  --private-key $PRIVATE_KEY
# Expected output
blockHash            0x4a7c1e...
blockNumber          22053900
contractAddress
cumulativeGasUsed    46823
effectiveGasPrice    100000000000
gasUsed              46823
status               1 (success)
transactionHash      0xd8f3a1...

The faucet mints 100,000 USDT (100000000000 in 6-decimal units) to your wallet. Call cast call 0xf75610dbe4aff5c2c3b2fd5a840b2e92e7b413bc "balanceOf(address)(uint256)" $WALLET_ADDRESS --rpc-url https://testnet-rpc.monad.xyz to confirm.


Step 2: Deposit Collateral

Depositing to Diffusal requires two transactions: first approve the DiffusalCollateralVault to spend your USDT, then call depositToPortfolio.

Step 2a: Approve the vault

cast send 0xf75610dbe4aff5c2c3b2fd5a840b2e92e7b413bc \
  "approve(address,uint256)" \
  0x2322eec37c016df82e7a009db58d20c376cee780 \
  10000000000 \
  --rpc-url https://testnet-rpc.monad.xyz \
  --private-key $PRIVATE_KEY
# Expected output
blockHash            0x9c2d4e...
blockNumber          22053910
gasUsed              46321
status               1 (success)
transactionHash      0xa1b2c3...

This approves 10,000 USDT (10000000000 in 6-decimal units) for the vault to pull.

Step 2b: Deposit to your default portfolio

Portfolio 0 is the default portfolio for most integrations. You do not need to call createPortfolio first — portfolio 0 is provisioned automatically on first deposit.

cast send 0x2322eec37c016df82e7a009db58d20c376cee780 \
  "depositToPortfolio(uint256,uint256)" \
  0 10000000000 \
  --rpc-url https://testnet-rpc.monad.xyz \
  --private-key $PRIVATE_KEY
# Expected output
blockHash            0xf3e2d1...
blockNumber          22053920
gasUsed              89450
status               1 (success)
transactionHash      0xe5f6a7...

Step 3: Check Your Balance

After depositing, verify your collateral balance using DiffusalMarginCalculator.

cast call 0x8b8e8163d1e9faf7bf7195bc0e40965db10d9ddf \
  "getPortfolioMarginInfo(address,uint256)(uint256,int256,int256,uint256,uint256,uint256,bool)" \
  $WALLET_ADDRESS 0 \
  --rpc-url https://testnet-rpc.monad.xyz
# Expected output (tuple: deposit, unrealizedPnl, equity, initMargin, maintMargin, maxWithdrawable, isHealthy)
10000000000
0
10000000000
0
0
10000000000
true

The first value is your deposited collateral in 6-decimal USDT. The final true confirms the portfolio is healthy.


Step 4: Explore Available Markets

Diffusal supports five trading pairs on testnet. Below are their on-chain pair IDs (keccak256 hashes of the pair name), which you will use when placing orders.

PairPair ID
BTC-USDT0xa92bcb5bc51aa5535ed0cc3f522992dd9a6fb2e8dd6dcf484705d93eb3cd167a
ETH-USDT0x7020b52841bb268cbc78137a54d4bf1f5305eed1039fb5d003ba95b8ededc46c
CMD:GC-USDT0x90a207505592982ae8c0e7e1e70db1425626867cbeff46d35798869d8e21b675
CMD:SI-USDT0x47a8fc916623fb2126ee1fbce6cdd5c3e8531b6af09a0891dbc96a2846b19904
CMD:BZ-USDT0x5220d7074b195c7853e6efcc6cfd0193ae04bc142eb9d1c815d4c90bed18fc96

For available option series (strike, expiry combinations) within each pair, call the REST API:

curl -s "https://api.testnet.diffusal.xyz/api/markets/pairs" | jq .
// Expected output (abbreviated)
{
  "pairs": [
    {
      "name": "BTC-USDT",
      "pairId": "0xa92bcb5bc5...",
      "baseAsset": "BTC",
      "quoteAsset": "USDT"
    },
    {
      "name": "ETH-USDT",
      "pairId": "0x7020b52841...",
      "baseAsset": "ETH",
      "quoteAsset": "USDT"
    }
  ]
}

Step 5: Place an Order (API-Assisted Flow)

Placing an order requires an EIP-712 signature. The recommended path is to let the API build the typed data for you, then sign it with Cast, and relay the resulting calldata on-chain.

This is the API-assisted flow — the server computes tick, nonce, deadline, and seriesId from a human-readable price. You only sign and relay. See the Contract Reference below if you want to build the EIP-712 payload manually.

Step 5a: Request typed order data from the API

Authenticate first if you have not done so (see Authentication). Then call the order placement endpoint without a signature to get the typed data:

curl -s -X POST https://api.testnet.diffusal.xyz/api/mm/orders/place \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -d '{
    "seriesId": "0xACTIVE_SERIES_ID",
    "isBuy": true,
    "price": "0.05",
    "size": "0.1"
  }' | jq .
// Expected output
{
  "mode": "typed_data",
  "typedData": {
    "domain": {
      "name": "DiffusalOptionsOrderBook",
      "version": "1",
      "chainId": 10143,
      "verifyingContract": "0x7b0990d6ed456eacf04043063ede7c2ae6dd74aa"
    },
    "types": {
      "PlaceOrder": [
        { "name": "seriesId", "type": "bytes32" },
        { "name": "portfolioId", "type": "uint256" },
        { "name": "isBuy", "type": "bool" },
        { "name": "tick", "type": "uint32" },
        { "name": "size", "type": "uint128" },
        { "name": "expiry", "type": "uint32" },
        { "name": "nonce", "type": "uint256" },
        { "name": "deadline", "type": "uint256" }
      ]
    },
    "message": {
      "seriesId": "0xACTIVE_SERIES_ID",
      "portfolioId": "0",
      "isBuy": true,
      "tick": "150",
      "size": "100000000000000000",
      "expiry": "1780000000",
      "nonce": "1776045156600",
      "deadline": "1776045456"
    }
  }
}

For MMM wallets, this unsigned request can omit portfolioId. The API normalizes to the MMM cash convention and returns the right typed data while internal trading portfolio routing stays automatic.

Step 5b: Sign the typed data with Cast

Save the typedData object to a file, then sign it:

# Save typedData from the API response to typed-data.json
echo '$TYPED_DATA_JSON' > typed-data.json

cast wallet sign --data "$(cat typed-data.json)" \
  --private-key $PRIVATE_KEY
# Expected output (EIP-712 signature)
0x4a8b2c1d3e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b01

Step 5c: Submit signature and get relay calldata

Post back to the same endpoint with the signature:

curl -s -X POST https://api.testnet.diffusal.xyz/api/mm/orders/place \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -d '{
    "seriesId": "0xACTIVE_SERIES_ID",
    "portfolioId": 0,
    "isBuy": true,
    "tick": 150,
    "size": "100000000000000000",
    "expiry": 1780000000,
    "nonce": "1776045156600",
    "deadline": "1776045456",
    "signature": "0x4a8b2c1d..."
  }' | jq .
// Expected output
{
  "mode": "relay",
  "relay": {
    "to": "0x7b0990d6ed456eacf04043063ede7c2ae6dd74aa",
    "calldata": "0xa3b4c5d6...",
    "value": "0",
    "chainId": 10143
  }
}

Step 5d: Send the relay transaction on-chain

The API never submits transactions — you relay the calldata from your wallet:

cast send 0x7b0990d6ed456eacf04043063ede7c2ae6dd74aa \
  "$RELAY_CALLDATA" \
  --rpc-url https://testnet-rpc.monad.xyz \
  --private-key $PRIVATE_KEY
# Expected output
blockHash            0xb4c5d6...
blockNumber          22053950
gasUsed              312000
status               1 (success)
transactionHash      0xf7e8d9...

To send raw calldata with Cast, use cast send <to> <calldata>. The calldata field from the relay response is the full ABI-encoded function call.


Step 6: Verify Your Fill

After your order is matched and settled, check your portfolio again to see the new position reflected in margin requirements:

cast call 0x8b8e8163d1e9faf7bf7195bc0e40965db10d9ddf \
  "getPortfolioMarginInfo(address,uint256)(uint256,int256,int256,uint256,uint256,uint256,bool)" \
  $WALLET_ADDRESS 0 \
  --rpc-url https://testnet-rpc.monad.xyz
# Expected output (after a fill — initial margin requirement is now non-zero)
10000000000
50000000
10050000000
2000000000
1000000000
8000000000
true

The third field (equity) is now above your deposit due to unrealized P&L. The fourth field (initial margin) reflects your new option position.

You can also query your open positions via the REST API:

curl -s "https://api.testnet.diffusal.xyz/api/portfolio/positions" \
  -H "Authorization: Bearer $SESSION_TOKEN" | jq .

Step 7: Withdraw Collateral

Once you have freed up margin (by closing positions or if the portfolio has excess collateral), call withdrawFromPortfolio on the DiffusalCollateralVault:

cast send 0x2322eec37c016df82e7a009db58d20c376cee780 \
  "withdrawFromPortfolio(uint256,uint256)" \
  0 5000000000 \
  --rpc-url https://testnet-rpc.monad.xyz \
  --private-key $PRIVATE_KEY
# Expected output
blockHash            0xc5d6e7...
blockNumber          22053990
gasUsed              78320
status               1 (success)
transactionHash      0xa9b0c1...

Withdrawals are rejected if they would make the portfolio unhealthy. Check isPortfolioHealthy or maxWithdrawable from getPortfolioMarginInfo before withdrawing.


Settlement and Expiry

When an option series reaches its expiry timestamp, Diffusal's settlement engine closes all positions at the final settlement price. You do not need to take any action — settlement is performed on-chain by the keeper.

What happens:

  1. The settlement engine reads the oracle price at expiry
  2. All open positions in the expired series are settled at the mark price
  3. P&L is credited or debited to each trader's portfolio collateral balance
  4. Settled positions are removed from margin calculations

Check if your positions have been settled:

curl -s "https://api.testnet.diffusal.xyz/api/portfolio/positions?status=settled" \
  -H "Authorization: Bearer $SESSION_TOKEN" | jq .

Check portfolio balance after settlement:

cast call 0x8b8e8163d1e9faf7bf7195bc0e40965db10d9ddf \
  "getPortfolioMarginInfo(address,uint256)(uint256,int256,int256,uint256,uint256,uint256,bool)" \
  $WALLET_ADDRESS 0 \
  --rpc-url https://testnet-rpc.monad.xyz

After settlement, the initial and maintenance margin requirements for the expired series will drop to zero, and your equity will reflect the settled P&L.

If settlement results in a loss larger than your collateral balance, the portfolio enters liquidation. The DiffusalLiquidationEngine handles this automatically — there is no action required from traders.


Contract Reference

The table below lists all user-facing contract functions grouped by contract. Use this as a quick reference after completing the walkthrough above.

DiffusalPortfolioManager (0x6eaefa8a084a9b453c455a33ca993dcd9ef53f18)

FunctionDescription
createPortfolio()Create an additional portfolio (beyond portfolio 0)
deletePortfolio(uint256 portfolioId)Delete an empty portfolio

DiffusalCollateralVault (0x2322eec37c016df82e7a009db58d20c376cee780)

FunctionDescription
depositToPortfolio(uint256 portfolioId, uint256 amount)Deposit USDT collateral (requires prior approval)
depositWithSignature(uint256 portfolioId, uint256 amount, uint256 nonce, uint256 deadline, bytes signature)Gasless deposit via meta-transaction
withdrawFromPortfolio(uint256 portfolioId, uint256 amount)Withdraw USDT collateral
withdrawWithSignature(uint256 portfolioId, uint256 amount, uint256 nonce, uint256 deadline, bytes signature)Gasless withdrawal via meta-transaction
transferCollateralBetweenPortfolios(uint256 from, uint256 to, uint256 amount)Move collateral between your portfolios

DiffusalOptionsOrderBook (0x7b0990d6ed456eacf04043063ede7c2ae6dd74aa)

FunctionDescription
placeOrderSigned(SignedPlaceOrder)Canonical MM signed-order placement
batchPlaceOrders(SignedPlaceOrder[])Atomically place multiple signed MM orders
cancelOrderSigned(SignedCancelOrder)Canonical MM signed-order cancellation
batchCancelOrders(SignedCancelOrder[])Atomically cancel multiple signed MM orders
replaceOrderSigned(SignedReplaceOrder)Atomically cancel and replace one resting order
batchReplaceOrders(SignedReplaceOrder[])Atomically replace multiple resting MM orders
registerOrderInPortfolio(...)Direct registration for maker-signed transactions
registerOrderInPortfolioWithSeriesParams(...)Direct registration with lazy series creation
cancelOrderById(bytes32 orderId)Direct cancel by ID for maker-signed transactions

DiffusalMarginCalculator (0x8b8e8163d1e9faf7bf7195bc0e40965db10d9ddf)

FunctionDescription
getPortfolioMarginInfo(address user, uint256 portfolioId)Returns deposit, unrealized PnL, equity, initial/maintenance margin, max withdrawable, and health status
isPortfolioHealthy(address user, uint256 portfolioId)Returns bool — quick health check

DiffusalOptionsRFQ (0x6f18dc553a5fb66d0081a84d804a608998b784c7)

FunctionDescription
fillRfqQuoteInPortfolio(quote, signature, fillAmount, portfolioId)Fill a signed RFQ quote from a market maker
fillRfqQuoteWithSignature(...)Fill RFQ quote via meta-transaction (taker signed)
fillRfqQuoteBatchInPortfolios(...)Batch fill multiple quotes
getQuoteStatus(quote, signature)Pre-flight validation of a quote

PlaceOrder EIP-712 Schema

If you choose to build the order payload manually instead of using the API-assisted flow:

Domain:

{
  "name": "DiffusalOptionsOrderBook",
  "version": "1",
  "chainId": 10143,
  "verifyingContract": "0x7b0990d6ed456eacf04043063ede7c2ae6dd74aa"
}

Type:

{
  "PlaceOrder": [
    { "name": "seriesId", "type": "bytes32" },
    { "name": "portfolioId", "type": "uint256" },
    { "name": "isBuy", "type": "bool" },
    { "name": "tick", "type": "uint32" },
    { "name": "size", "type": "uint128" },
    { "name": "expiry", "type": "uint32" },
    { "name": "nonce", "type": "uint256" },
    { "name": "deadline", "type": "uint256" }
  ]
}

RfqQuote Struct

The quote signed by a market maker:

struct RfqQuote {
    address mmm;           // Market maker address (signer)
    address taker;         // Authorized taker (address(0) = anyone)
    bytes32 seriesId;      // Option series identifier
    bool takerIsBuying;    // true = taker buys long, false = taker buys short
    uint256 price;         // Premium per contract (WAD, 1e18 = 1.0)
    uint256 size;          // Maximum contracts available
    uint256 nonce;         // Must match MM's current on-chain nonce
    uint256 quoteExpiry;   // Quote validity deadline (unix timestamp)
    bytes32 pairId;        // Trading pair hash
    uint256 strike;        // Strike price (WAD)
    uint256 optionExpiry;  // Option expiration timestamp
    bool isCall;           // true = call, false = put
}

See Also

On this page