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
trueThe 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.
| Pair | Pair ID |
|---|---|
| BTC-USDT | 0xa92bcb5bc51aa5535ed0cc3f522992dd9a6fb2e8dd6dcf484705d93eb3cd167a |
| ETH-USDT | 0x7020b52841bb268cbc78137a54d4bf1f5305eed1039fb5d003ba95b8ededc46c |
| CMD:GC-USDT | 0x90a207505592982ae8c0e7e1e70db1425626867cbeff46d35798869d8e21b675 |
| CMD:SI-USDT | 0x47a8fc916623fb2126ee1fbce6cdd5c3e8531b6af09a0891dbc96a2846b19904 |
| CMD:BZ-USDT | 0x5220d7074b195c7853e6efcc6cfd0193ae04bc142eb9d1c815d4c90bed18fc96 |
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)
0x4a8b2c1d3e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b01Step 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
trueThe 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:
- The settlement engine reads the oracle price at expiry
- All open positions in the expired series are settled at the mark price
- P&L is credited or debited to each trader's portfolio collateral balance
- 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.xyzAfter 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)
| Function | Description |
|---|---|
createPortfolio() | Create an additional portfolio (beyond portfolio 0) |
deletePortfolio(uint256 portfolioId) | Delete an empty portfolio |
DiffusalCollateralVault (0x2322eec37c016df82e7a009db58d20c376cee780)
| Function | Description |
|---|---|
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)
| Function | Description |
|---|---|
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)
| Function | Description |
|---|---|
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)
| Function | Description |
|---|---|
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
- Authentication — SIWE wallet sign-in for API access
- Market Maker On-Chain Guide — Market maker quote submission and RFQ flows
- Backend API Quickstart — REST and WebSocket integration
- Testnet Guide — Faucets, setup, and testnet-specific notes
- Contract Addresses — All deployed contract addresses