Docs
The complete reference for trading on, making on, and integrating with Arcdesk. The desk is a non-custodial tool: these documents plus the contract source are the entire trust model — there is no fine print anywhere else.
Network: loading…
Networks · Order lifecycle · Taker guide · Maker guide · Fees & timeouts · Contract reference · REST API · Arc specifics · Security & trust · Security review · FAQ
Networks & contracts
Addresses below are read live from the desk API, so
they are always the pair this deployment actually settles on. Verify independently before sending
funds: call usdc() and operator() on each escrow and check they match this page.
| Leg | Chain | Chain ID | Contract |
|---|---|---|---|
| Payment | — | — | — |
| Liquidity | — | — | — |
| USDC (payment side) | — | — | — |
| USDC (Arc) | Arc | — | 0x3600000000000000000000000000000000000000 |
Both legs run the same bytecode
(ArcdeskEscrow.sol). Nothing configures a contract into a role; the chain it sits on
decides which half of the interface gets used. One audit therefore covers both deployments.
Order lifecycle
quoted ──▶ reserved ──▶ paid ──▶ maker_paid ──▶ delivered
│ │
│ └── payment deadline passes ──▶ refunded (your money back)
└── reservation deadline passes ──▶ liquidity returns to the offer
| State | On-chain meaning | Who acted |
|---|---|---|
| quoted | Order priced; nothing on-chain yet | API |
| reserved | reserve() confirmed on Arc: maker liquidity is locked to your address under a hashlock, for ≥90 min | Desk keeper |
| paid | lockPayment() confirmed on the payment chain: your funds escrowed under the same hashlock, ≤30 min | You (your only transaction) |
| maker_paid | claimPayment() confirmed: maker took the payment, and doing so published the secret on-chain | Maker |
| delivered | claimReservation() confirmed on Arc: your USDC left escrow to your address | Anyone (the desk submits it for you) |
| refunded | refundPayment() after the deadline: full escrow returned to you | Anyone (button on Orders, or cast) |
Taker guide — buying USDC on Arc
- Pick an offer on the Trade page. The price you compare is the premium; the 2% desk fee is the same everywhere.
- Enter amount + your Arc address. Any address you control on Arc; it does not need gas, history, or a balance.
- Get quote & reserve. Before you pay anything, the desk locks your USDC out of the maker's offer, addressed to you. You can verify this yourself: read
reservations(orderId)on the Arc escrow and checkrecipientis your address anddeadlineis comfortably after your payment deadline. - Approve + pay — two wallet transactions on the payment chain. The second escrows
proceeds + feeunder the hashlock. This is the last thing you ever have to do. - Wait ~a minute. The maker claims your payment (publishing the secret) and the desk delivers your USDC on Arc. You pay no Arc gas at any point.
If nothing is delivered: your money is not gone, it is locked with a countdown.
After the payment deadline (max 30 minutes) the Refund button on Orders returns it — or from
any wallet: refundPayment(orderId, payer) on the payment escrow. The desk cannot block this.
Maker guide — selling USDC on Arc
You escrow USDC on Arc behind an offer priced at your premium. When a buyer's payment is locked on the payment chain, claiming that payment is what releases your escrowed USDC to them — you can never lose the USDC without simultaneously receiving the payment, and the buyer can never take your USDC without you being paid.
| Action | Call | Notes |
|---|---|---|
| Post an offer | approve() + postOffer(offerId, amount, premiumBps, expiry) | offerId: any unused 32-byte id (hash something unique). premiumBps: 800 = 8%. expiry: unix timestamp, or 0 for open-ended. |
| Top up | fundOffer(offerId, amount) | Adds liquidity to a live offer. |
| Close & withdraw | cancelOffer(offerId) | Instant for unreserved liquidity. Reserved slices come back automatically when their reservations expire unclaimed. |
| Sweep an expired offer | expireOffer(offerId) | Permissionless housekeeping for time-limited offers; open-ended offers only close via cancel. |
# post 100 USDC at 8%, open-ended, from your Arc wallet
cast send $ARC_USDC "approve(address,uint256)" $ARC_ESCROW 100000000 \
--rpc-url $ARC_RPC --private-key $KEY --legacy
cast send $ARC_ESCROW "postOffer(bytes32,uint256,uint16,uint64)" \
$(cast keccak "my-offer-$(date +%s)") 100000000 800 0 \
--rpc-url $ARC_RPC --private-key $KEY --legacy
Maker accounts pay Arc gas (which is USDC), so keep a little slack beyond your offer size. Today the settlement secret is generated by the desk when it quotes an order against your offer; a maker API where you bring your own hashlock (so the desk never holds the secret) is the next milestone and will be required for third-party makers on mainnet.
Fees, premiums, timeouts
| Parameter | Value | Where enforced |
|---|---|---|
| Desk fee | 2% | Quoted into lockPayment; paid by the taker to the treasury at settlement |
| Maker premium | 0 – 655.35% (uint16 bps) | Set per offer; the market decides what clears |
| Payment window | ≤ 30 min — MAX_PAYMENT_WINDOW | Contract rejects longer locks; after the deadline refundPayment is open to anyone |
| Reservation window | ≥ 90 min — MIN_RESERVATION_WINDOW | Contract rejects shorter reservations, so delivery always outlives payment by ≥1h |
| Offer expiry | maker's choice or 0 (open-ended) | Expired offers stop reserving instantly; sweep is permissionless |
Quote math for buying A USDC at premium
p bps: proceeds = A × (10000 + p) / 10000, fee = proceeds × feeBps / 10000,
you pay proceeds + fee. All amounts are 6-decimal USDC integers; division truncates.
Contract reference — ArcdeskEscrow
Solidity ^0.8.24, OpenZeppelin SafeERC20 + ReentrancyGuard. All state-changing functions are nonReentrant. No proxy, no upgradability, no pause on refund paths.
Liquidity leg (Arc)
| Function | Access | Reverts with |
|---|---|---|
| postOffer(bytes32,uint256,uint16,uint64) | anyone | offer exists · zero amount · bad expiry |
| fundOffer(bytes32,uint256) | maker | not maker · inactive · zero amount |
| cancelOffer(bytes32) | maker | not maker |
| expireOffer(bytes32) | anyone | no offer · open-ended · not expired · inactive |
| reserve(bytes32,bytes32,address,uint256,bytes32,uint64) | operator | not operator · order exists · bad recipient · bad hashlock · reservation too short · inactive · offer expired · insufficient |
| claimReservation(bytes32,bytes32) | anyone | not reserved · bad preimage |
| refundReservation(bytes32) | anyone | not reserved · not yet |
Payment leg (source chain)
| Function | Access | Reverts with |
|---|---|---|
| lockPayment(bytes32,address,address,uint256,uint256,bytes32,uint64) | anyone (the taker) | order exists · bad payee · bad hashlock · deadline passed · payment window too long |
| claimPayment(bytes32,address,bytes32) | anyone with the preimage | not locked · bad preimage |
| refundPayment(bytes32,address) | anyone | not locked · not yet |
Admin surface (deliberately small)
| Function | Access | Blast radius |
|---|---|---|
| setOperator(address) | owner | Changes who may reserve. Cannot touch escrowed funds or refund paths. |
| transferOwnership(address) | owner | Hands over the above. Nothing else. |
Events
OfferPosted(offerId, maker, amount, premiumBps, expiry)
OfferFunded(offerId, amount) OfferCancelled(offerId, returned)
OfferExpired(offerId, returned)
Reserved(orderId, offerId, recipient, amount, hashlock, deadline)
ReservationClaimed(orderId, preimage) ReservationRefunded(orderId, amount)
PaymentLocked(orderId, payer, maker, proceeds, fee, hashlock, deadline)
PaymentClaimed(orderId, preimage) PaymentRefunded(orderId)
OperatorChanged(operator) OwnerChanged(owner)
Hashlock scheme: hashlock = keccak256(abi.encodePacked(secret))
where secret is 32 bytes generated by the maker side. Revealing it in either
claim* makes it public for the other chain — that is the entire cross-chain binding;
there is no bridge, oracle, or messaging layer to trust.
REST API
Base URL: the origin serving this page (configurable under Profile → Settings). JSON in/out. Amounts are 6-decimal integer strings. No authentication — the API can only do things that are safe for anyone to do; your funds are guarded by the contracts, not by this server.
| Endpoint | Returns |
|---|---|
| GET /health | { ok, keeper, sourceEscrow, arcEscrow, feeBps } |
| GET /offers | { offers: [{ offerId, maker, remaining, premiumBps, expiry }] } — expiry 0 = open-ended |
| GET /stats | { trades, volume6, fees6 } |
| POST /orders | Reserves on Arc first, then returns exact lockPayment args (below) |
| GET /orders/:id | Order row incl. status and settlement tx hashes |
| GET /address/:addr/orders | History for a taker address (latest 100) |
POST /orders
{ "offerId": "0x…", "amount6": "10000000",
"takerSource": "0xYourPaymentChainAddr", "recipientArc": "0xYourArcAddr" }
200 →
{ "orderId": "0x…", "escrow": "0x…", "usdc": "0x…",
"lockPayment": { "orderId", "maker", "treasury", "proceeds", "fee", "hashlock", "deadline" },
"totalPay": "…", "payDeadline": …, "resvDeadline": … }
errors: 404 offer not found · 409 not enough liquidity · 410 offer expired · 400 bad address
Integration contract: send exactly one
approve(escrow, proceeds+fee) then lockPayment(…) with the returned args,
then poll GET /orders/:id until delivered. If you see refunded
or your deadline passes, call refundPayment(orderId, payer) yourself — never resend a payment
for the same orderId (the contract rejects duplicates).
Arc-specific behavior
- USDC is the gas token. The ERC-20 view at
0x3600…0000is the account's native balance ÷ 10¹² (6-decimal surface over an 18-decimal core). One pool of funds, two interfaces. - Transfers run through chain precompiles (
0x1800…0000balances,0x1800…0001compliance). A new Arc account needs native USDC to pay gas. Keeper-assisted delivery lets it receive its first USDC without submitting an Arc transaction. - Circle operates a blocklist at the chain level. A blocklisted address cannot move USDC, and that includes escrow contracts. This is an external risk the desk cannot remove; it is disclosed here because you should size positions knowing it exists.
- Timestamps can stall on Arc when blocks are sparse. The contract windows are generous (90 min floor) partly for this reason.
Current availability: only Base → Arc matching is enabled, subject to desk liquidity. New Arc → Base fills are paused pending settlement accounting and recovery tests.
Security & trust model
| Party | Can do | Cannot do |
|---|---|---|
| Desk / keeper | Reserve maker liquidity and submit claims. A malicious operator can drain third-party maker offers; public market making is disabled. | Change an existing reservation recipient or the payees of an existing payment lock. Eligible refunds are permissionless but require a transaction. |
| Maker | Decline to claim (trade times out, you get refunded) | Take your payment without releasing your USDC — claiming publishes the secret that unlocks your delivery, and the contract forces the delivery window to outlive the payment window by ≥1h. |
| Contract owner | Replace the operator, including with an operator that can misuse maker liquidity. | Upgrade the deployed code (there is no proxy) or change the payees of existing payment locks. |
| Circle (Arc) | Blocklist any Arc address, including the escrow | — (external, disclosed above) |
Audit status: unaudited. The 49-test suite covers unit, attack, fuzz, invariant and testnet-fork cases. Passing tests do not resolve the known maker/operator trust issue or constitute an independent audit. Third-party market making is disabled in this application; the existing contract does not enforce that restriction.
Security review (adversarial)
The system was tested from an attacker's seat, on contracts and the live API. Findings and their status:
| Finding | Severity | Status |
|---|---|---|
| Order-id front-run: lock dust under a public orderId to block the real taker (DoS) | Medium | FIXED — payments keyed by (orderId, payer); slots are independent |
| Keeper claims any hashlock-matching payment, so a forged lock could reveal the secret early | High | FIXED — keeper only claims the exact quoted payment (payer, maker, amounts) |
| Unauthenticated POST /orders reserves maker liquidity for free (griefing) | High (practical) | MITIGATED — per-offer cap of 3 unpaid reservations, per-IP rate limit, auto-refund sweeper at the reservation deadline |
| Raw SQLite error string returned on bad input | Low (info leak) | FIXED — strict input validation, clean errors |
| Operator can drain any maker's offer (compromised/rogue keeper) | High (design) | KNOWN — inherent keeper trust for makers; the fix is maker-signed reservations (EIP-712), a prerequisite before third-party makers on mainnet. The application only accepts desk-owned liquidity. This is an application restriction, not an on-chain access control: direct third-party deposits remain unsafe. |
Attacks the design already refuses: redirecting a delivery or payment, refunding to a non-payer, forging or replaying a claim, refunding a reservation early, seizing admin, and the owner touching escrowed funds — all covered by the automated suite.
FAQ
Why does USDC cost more than $1 here?
Makers choose their quotes based on available liquidity. Arc mainnet is public and Arcdesk is an independent OTC route. Compare the full cost with other available routes; a premium is neither required nor guaranteed.
Do I need anything on Arc first?
No. An empty address you control is enough. You never send an Arc transaction; delivery comes to you.
What if the site disappears mid-trade?
Your payment sits in a contract with a ≤30-minute deadline. After it passes, refundPayment(orderId, payer) from any wallet returns everything. The orderId is on your Orders page and in the PaymentLocked event your own wallet emitted.
Can I trade the other direction (Arc → out)?
Select Buy or Sell in the order book. Both directions are implemented; execution requires a matching desk-owned offer or bid.
Is there an official token, points, or airdrop?
No. Anyone claiming otherwise in our name is scamming you.