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 @direct
deposit @direct
Public state
loading…
Contract source Verified
// 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