ShinobiCashPool
Extended Privacy Pool with cross-chain withdrawal support and refund handling.
Overview
ShinobiCashPool is an abstract contract that extends the base PrivacyPool from privacy-pools-core. It adds:
- Cross-chain withdrawal with 9-signal proofs
- Refund commitment handling for failed intents
Inheritance
PrivacyPool (privacy-pools-core)
└── ShinobiCashPool (abstract)
└── ShinobiCashPoolSimple (native ETH implementation)State Variables
// Cross-chain proof verifier
ICrossChainWithdrawalProofVerifier public immutable CROSS_CHAIN_WITHDRAWAL_VERIFIER;The pool also inherits state from PrivacyPool:
- Merkle tree for commitments
- Nullifier registry (spent nullifiers)
- Root history for proof validation
- ASP (Association Set Provider) root
Key Functions
crosschainWithdraw
Process a cross-chain withdrawal with an enhanced 9-signal proof.
function crosschainWithdraw(
Withdrawal memory _withdrawal,
CrossChainProofLib.CrossChainWithdrawProof memory _proof
) external_withdrawal— Withdrawal request (recipient, amount, processooor, data)_proof— 9-signal Groth16 proof
- Validate processooor (msg.sender matches expected)
- Validate context matches
hash(withdrawal, SCOPE) % SNARK_SCALAR_FIELD - Validate tree depths within bounds
- Validate state root is in recent history (
ROOT_HISTORY_SIZE) - Validate ASP root is the latest
- Verify Groth16 proof via
CROSS_CHAIN_WITHDRAWAL_VERIFIER - Spend nullifier (mark as used)
- Insert new commitment (change note)
- Transfer withdrawn value to processooor
- Emit
CrosschainWithdrawnevent
handleRefund
Insert a refund commitment for a failed cross-chain withdrawal.
function handleRefund(
uint256 _refundCommitmentHash,
uint256 _amount
) external payable onlyEntrypoint_refundCommitmentHash— The refund commitment (9th proof signal)_amount— Amount being refunded
- Validate caller is the entrypoint
- Validate
msg.valuematches_amount - Insert
_refundCommitmentHashinto the Merkle tree - User can later withdraw this commitment normally
9-Signal Proof Structure
Cross-chain withdrawals use an enhanced proof with 9 public signals:
struct CrossChainWithdrawProof {
uint256[2] pA;
uint256[2][2] pB;
uint256[2] pC;
uint256[9] pubSignals;
}| Index | Signal | Description |
|---|---|---|
| 0 | newCommitmentHash | Change note commitment |
| 1 | existingNullifierHash | Spent deposit nullifier |
| 2 | refundCommitmentHash | Fallback if intent fails |
| 3 | withdrawnValue | Amount being withdrawn |
| 4 | stateRoot | Merkle root of deposits |
| 5 | stateTreeDepth | Depth of state tree |
| 6 | ASPRoot | Merkle root of ASP set |
| 7 | ASPTreeDepth | Depth of ASP tree |
| 8 | context | Binding context (prevents replay) |
The 9th signal (refundCommitmentHash) is the key addition for cross-chain — it enables fund recovery if the intent expires without being filled.
Standard vs Cross-Chain Proofs
| Feature | Standard (8 signals) | Cross-Chain (9 signals) |
|---|---|---|
| Change note | ✅ | ✅ |
| Nullifier | ✅ | ✅ |
| Refund commitment | ❌ | ✅ |
| Used for | Same-chain withdrawals | Cross-chain withdrawals |
ShinobiCashPoolSimple
The concrete implementation for native ETH:
contract ShinobiCashPoolSimple is ShinobiCashPool {
function _pull(address, uint256 _amount) internal override {
// Pull ETH from msg.sender (via msg.value)
}
function _push(address _to, uint256 _amount) internal override {
// Push ETH to recipient
(bool success, ) = _to.call{value: _amount}("");
require(success, "ETH transfer failed");
}
}Context Validation
The context binds the proof to specific withdrawal parameters:
uint256 expectedContext = uint256(
keccak256(abi.encode(_withdrawal, SCOPE))
) % SNARK_SCALAR_FIELD;
if (proof.context() != expectedContext) revert ContextMismatch();This prevents proof replay across different withdrawals or pools.
Root History
The pool maintains a history of recent Merkle roots:
uint32 public constant ROOT_HISTORY_SIZE = 30;
mapping(uint32 => uint256) public roots;
uint32 public currentRootIndex;Proofs can use any root from the last 30 insertions, allowing for concurrent proof generation.
Security Considerations
- Nullifier uniqueness — Each nullifier can only be spent once
- Root validation — State root must be in recent history
- ASP enforcement — ASP root must be the latest (no stale proofs)
- Context binding — Proofs cannot be replayed
- Entrypoint-only refunds — Only the entrypoint can insert refund commitments
Source Code
Related
- Entrypoint — Orchestrator that calls the pool
- Privacy Pools — Cryptographic foundations
- OIF Settlers — Cross-chain settlement