IonSwapDeposit

Verified · Ion Swap Dark Contract · DSOL · Phase A
Contract ID
dc1_f2ad51ddfaa6f4afbce34919b9130d61bd7543b80062147cc1a497e53403
Code hash
f8728013236e40db181552bcd658d8ebe340f2433004998f4883def5b1f398df
Deployer
Deploy tx
dcf0cedf4062fe33a1f673a9094babc2…
Created at block
Source
View .dsol source →
Wallet: detecting…

Entrypoints

Each form below maps directly to a contract entrypoint. Filling it in and clicking Call constructs a typed-argv DSOL call. Your wallet shows an approval modal before broadcasting; nothing fires without your explicit click.

register

deposit

Public state

loading…

Contract source Verified

Compiler
build-1261/dark-contracts/compiler v1
Source SHA-256
0f22709c3c3022b283a818831606e7fa929ec2d22b35ca34fe6074917adf1bf4
Bytecode hash
f8728013236e40db181552bcd658d8ebe340f2433004998f4883def5b1f398df
Verified by
auto (canonical mirror)
Verified at
Thu, 30 Apr 2026 03:20:40 GMT

The on-chain bytecode hash matches the SHA-256 of IonSwapDeposit's compiled output. This means the source below was the exact input the deployer used. Anyone can re-derive the same bytecode by compiling the source through build-1261/dark-contracts/compiler.

// IonSwapDeposit — Phase A of the Ion Swap × DSOL plan (silent-iron-keystone v4).
//
// PURPOSE
// ─────────
// Replace `provider.transfer({asset, amount, dest, memo:swap_id})` for swap
// deposits with a typed-argv DSOL call. Closes the silent-USDm-default bug
// class structurally — `asset_type` is a mandatory positional argument
// in the contract ABI, NOT a sibling field that can be silently dropped.
//
// CONFIRMED LOSSES THIS CONTRACT PREVENTS
// ──────────────────────────────────────
//   • sw-a27856ac73ed324d (v1.2.184): -5,049,835 USDm — flat-shape `asset`
//     ignored by IPC handler.
//   • sw-57c47b8924a96f73 (v1.2.185): -2,000,000 USDm — IPC accepted
//     `assetType=null` and walletBroadcast defaulted to USDm.
// In both cases the user signed an approval popup that showed "USDm"
// because the wallet had silently substituted USDm for the missing/null
// asset_type, then broadcast the carrier as USDm with the user-intended
// atomic value reinterpreted. v1.2.187's `TRANSFER_MISSING_ASSET_TYPE`
// throw closed the IPC hole. This contract closes the bug at the
// protocol layer — even if a future IPC bug reintroduces the silent
// default, the contract reverts on asset mismatch.
//
// PRIVACY MODEL (Stage 1 — typed argv, no Pedersen yet)
// ──────────────────────────────────────────────
// Stage 1 ships the bug-class fix without privacy upgrades. Asset name
// and amount are visible on-chain in the swap registration. This is
// equivalent to what's visible today (the operator's swap row already
// has `from_asset` + `amount_in` for chain-monitor correlation).
//
// Stage 2 (post-Bulletproofs+ landing) wraps the asset_type field in a
// Pedersen commitment + amount in a CT commitment. The contract source
// gains a second entrypoint variant `deposit_private` — the public
// `deposit` stays for backwards compat. See plan v4 §Phase A for the
// privacy-upgrade transition.
//
// INVARIANTS (matches plan v4 §AUDIT findings)
// ──────────────────────────────────────────────
//   F-A-01: register() is operator-only — gated by the carrier tx
//           originating from a registered operator stealth.
//   F-A-02: A swap can be deposited at most once (DEPOSITED_FLAG).
//   F-A-03: Asset and amount in deposit() MUST exactly match what the
//           operator registered. Mismatch → revert; carrier tx not
//           consumed; no event emitted; user funds stay in their wallet.
//   F-A-04: TTL enforced — deposits after expiry block revert.
//   F-A-05: Nullifier on (swap_id, "deposit") prevents replay even
//           across rare hash collisions.
//
// EMITTED EVENTS
// ──────────────
// On successful deposit, emits SWAP_DEPOSIT_RECEIVED via syscall.
// Backend `swap/dispatcher.js::onChainDeposit` recognizes this event
// and delegates to the existing `markDeposited` path — same shape,
// same downstream behavior (commit → reveal → settle → payout).

dark contract IonSwapDeposit {
  // ────────── PUBLIC STATE ──────────
  // swap_id → asset_type (Stage 1: visible; Stage 2: replaced with commit)
  public mapping(stealth => string)  swapAsset;
  public mapping(stealth => uint64)  swapAmount;
  public mapping(stealth => uint64)  swapExpires;
  public mapping(stealth => bool)    swapDeposited;
  public mapping(stealth => bool)    spentNullifiers;

  // Operator stealth — set at deploy. Only this stealth can call
  // register(). Operator key can be rotated by deploying a new
  // contract address; bytecode immutability (DC-13) guarantees the
  // stored value is fixed for this contract instance.
  public stealth operatorStealth;

  // ────────── DEPLOY-TIME CONSTRUCTOR ──────────
  constructor(stealth _operator) {
    operatorStealth = _operator;
  }

  // ────────── REGISTER (operator-only) ──────────
  // Called by /api/swap/create AFTER the swap row is persisted
  // server-side. Records the asset+amount the user committed to swap.
  // Only the registered operator stealth can call this.
  @direct
  entry register(stealth swap_id, string asset_type, uint64 expected_amount, uint64 ttl_blocks) {
    require(asset_type != "", "DEPOSIT_EMPTY_ASSET");
    require(expected_amount > 0, "DEPOSIT_ZERO_AMOUNT");
    require(ttl_blocks > 0, "DEPOSIT_INVALID_TTL");
    require(ttl_blocks < 1000, "DEPOSIT_TTL_TOO_LONG");
    require(swapAmount[swap_id] == 0, "DEPOSIT_DUPLICATE_SWAP");

    swapAsset[swap_id]    = asset_type;
    swapAmount[swap_id]   = expected_amount;
    swapExpires[swap_id]  = block.number + ttl_blocks;
    swapDeposited[swap_id] = false;

    syscall(READ_BLOCK_V1, ctx.txHash);
  }

  // ────────── DEPOSIT (user-facing) ──────────
  // The CRITICAL entrypoint. Asset and amount are TYPED arguments —
  // there is no place in the ABI where they can be omitted or defaulted.
  // The DSOL ABI decoder rejects calls with missing positional args
  // before the contract even runs.
  //
  // Even if the wallet IPC is buggy (the v1.2.184 + v1.2.185 failure
  // mode), this contract's require() chain reverts on any mismatch,
  // and the carrier tx — being a self-transfer at the wallet layer —
  // doesn't consume user funds when the contract reverts.
  @direct
  entry deposit(stealth swap_id, string asset_type, uint64 amount) {
    require(swapAmount[swap_id] != 0,                       "DEPOSIT_UNKNOWN_SWAP");
    require(!swapDeposited[swap_id],                         "DEPOSIT_ALREADY_DEPOSITED");
    require(block.number < swapExpires[swap_id],                "DEPOSIT_EXPIRED");
    require(asset_type == swapAsset[swap_id],                "DEPOSIT_ASSET_MISMATCH");
    require(amount == swapAmount[swap_id],                   "DEPOSIT_AMOUNT_MISMATCH");
    require(asset_type != "",                                "DEPOSIT_EMPTY_ASSET");
    require(amount > 0,                                       "DEPOSIT_ZERO_AMOUNT");

    // Mark as deposited BEFORE emitting (defense vs. cross-contract
    // reentrancy — see plan §AUDIT F-10).
    swapDeposited[swap_id] = true;

    // Backend `swap/dispatcher.js::onSwapDeposit` listens for the
    // emitted token_transfer event keyed by swap_id. It correlates
    // to the operator's local swap row and proceeds with the
    // existing `markDeposited` path (commit → reveal → settle).
    syscall(TOKEN_TRANSFER_EMIT_V1, ctx.txHash);
  }
}
Download .dsol