OIF Settlers
Open Intent Framework settlers manage cross-chain intent creation, escrow, and settlement.
Overview
Shinobi Cash uses three settler contracts:
| Contract | Chain | Purpose |
|---|---|---|
ShinobiInputSettler | Origin | Escrows funds, handles claims/refunds |
ShinobiDepositOutputSettler | Pool (Arbitrum) | Fills deposit intents |
ShinobiWithdrawalOutputSettler | Destination | Fills withdrawal intents |
Intent Structure
struct ShinobiIntent {
// Base OIF StandardOrder Fields
address user; // Intent creator (verified on origin)
uint256 nonce; // Unique identifier component
uint256 originChainId; // Chain where intent was created
uint32 expires; // Expiry timestamp for refunds
uint32 fillDeadline; // Deadline for filling
address fillOracle; // Oracle for fill proof validation
uint256[2][] inputs; // Input tokens [tokenId, amount][]
MandateOutput[] outputs;// Outputs to fill on destination
// Shinobi Extensions
address intentOracle; // Oracle for intent proof validation
bytes refundCalldata; // Custom refund logic
}ShinobiInputSettler
Manages intent creation and escrow on origin chains.
State
address public immutable entrypoint; // Only caller for open()
mapping(bytes32 => OrderStatus) public orderStatus;
enum OrderStatus {
None, // Order doesn't exist
Deposited, // Funds escrowed, awaiting fill or expiry
Claimed, // Solver filled and claimed
Refunded // Intent expired and refunded
}Functions
open
Create an intent and escrow funds. Only callable by the entrypoint.
function open(ShinobiIntent calldata intent) external payablefinalise
Release escrowed funds to the solver after proving the fill.
function finalise(
ShinobiIntent calldata intent,
SolveParams[] calldata solveParams,
bytes32 destination
) externalFor each output:
1. Check fill timestamp <= fillDeadline
2. Build payloadHash = keccak256(solver | orderId | timestamp | output)
3. Pack into proofSeries: [chainId, oracle, settler, payloadHash]
4. Call fillOracle.efficientRequireProven(proofSeries)refund
Refund expired intent. Permissionless — anyone can trigger for expired intents.
function refund(ShinobiIntent calldata intent) externalState Machine
┌─────────────┐
│ None │
└──────┬──────┘
│ open()
┌──────▼──────┐
│ Deposited │
└──────┬──────┘
┌──────────┴──────────┐
│ │
finalise() refund()
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Claimed │ │ Refunded │
└─────────────┘ └─────────────┘ShinobiDepositOutputSettler
Handles deposit fills on the pool chain.
Key Difference: Mandatory Intent Validation
Deposits require intentOracle validation to prevent depositor address spoofing:
address public immutable intentOracle; // MUST be used for all deposits
function fill(ShinobiIntent calldata intent) external payable nonReentrant {
// 1. Validate deposits have exactly one output
if (intent.outputs.length != 1) revert InvalidOutput();
// 2. Validate correct destination chain
if (intent.outputs[0].chainId != block.chainid) revert InvalidChain();
// 3. Validate fill deadline
if (block.timestamp > intent.fillDeadline) revert FillDeadlinePassed();
// 4. CRITICAL: Validate intent uses configured oracle
if (intent.intentOracle != intentOracle) revert IntentOracleMismatch();
// 5. Compute unique order identifier
bytes32 orderId = intent.orderIdentifier();
// 6. CRITICAL: Validate intent proof via oracle
if (!IInputOracle(intentOracle).isProven(
intent.originChainId,
bytes32(uint256(uint160(intentOracle))),
bytes32(uint256(uint160(address(this)))),
orderId
)) revert IntentNotProven();
// 7. Fill output with callback to crosschainDeposit()
_fillOutput(orderId, intent.outputs[0], msg.sender);
}Why intentOracle is required: Without this, an attacker could create fake intents claiming any depositor address. The oracle proves the intent originated from a legitimate user on the origin chain.
ShinobiWithdrawalOutputSettler
Handles withdrawal fills on destination chains.
Key Difference: Optimistic Settlement
Withdrawals use optimistic settlement — NO intent proof validation:
address public immutable fillOracle; // Validated for consistency only
function fill(ShinobiIntent calldata intent) external payable nonReentrant {
// 1. Validate has outputs
if (intent.outputs.length == 0) revert InvalidOutput();
// 2. Validate correct destination chain
if (intent.outputs[0].chainId != block.chainid) revert InvalidChain();
// 3. Validate fill deadline
if (block.timestamp > intent.fillDeadline) revert FillDeadlinePassed();
// 4. Validate fillOracle for consistency
if (intent.fillOracle != fillOracle) revert FillOracleMismatch();
// 5. Compute unique order identifier
bytes32 orderId = intent.orderIdentifier();
// NO intentOracle validation - ZK proof already validated on origin
// 6. Fill each output with simple ETH transfer
for (uint256 i = 0; i < intent.outputs.length; i++) {
_fillOutput(orderId, intent.outputs[i], msg.sender);
}
}Why intentOracle is NOT required: The ZK proof on the origin chain already validated:
- User owns a valid deposit (nullifier)
- User knows the secret (commitment membership)
- Withdrawal parameters are bound to the proof (context)
The intent was created by the trusted ShinobiCashEntrypoint after proof validation.
Oracle System
Two oracles ensure secure cross-chain settlement:
| Oracle | Direction | Purpose | Required For |
|---|---|---|---|
fillOracle | Dest → Origin | Proves the fill happened | All intents (claim) |
intentOracle | Origin → Dest | Proves intent is legitimate | Deposits only |
Security Comparison
| Feature | Deposits | Withdrawals |
|---|---|---|
| Intent validation | Required (intentOracle) | Not required |
| Why | Depositor address could be spoofed | ZK proof validates user |
| Fill validation | fillOracle | fillOracle |
| Refund mechanism | Standard ETH refund | Refund commitment in pool |
Source Code
Related
- Entrypoint — Creates intents
- Cross-Chain Architecture — How OIF works
- Trust Assumptions — Solver trust model