Skip to content

Security model

The trust boundaries, what each key can do, and the threats each layer stops.

UNRLZD is non-custodial. Your money sits in a trading account on NEAR that only your keys can act on, and the contract on that account checks every call against fixed rules. We can’t move or freeze your money.

This page maps who holds which key, lists the attacks we design against, and shows what our pauses can and can’t do. Risks covers what is left.

Trust boundaries: who holds which key

Four zones. In your browser, the terminal holds no key; the signer frame on its own origin holds the trading keys; the wallet bridge on its own origin carries owner calls. Your wallet holds the owner's full-access key. On NEAR, the trading account checks every call, swaps only on allowlisted venues with the output returned to itself, and was created by the factory. On our servers, the engine serves data and quotes, the order service holds the 24/7 key and the watcher checks every order it sends and can only pause it. Your browser Your wallet NEAR Our servers Signer frame trading keys Terminal holds no key Wallet bridge owner calls Owner wallet full-access key Venues Rhea, DCL, Plach Trading account checks every call Factory trade.unrlzd.near Engine data and quotes Order service holds the 24/7 key Watcher can only pause 1 1 The terminal asks the frame to sign, by postMessage. 2 execute 2 The signed call goes through an RPC node to your account. 3 3 Owner calls go to the wallet bridge, which checks each one. 4 4 Your wallet shows the call and signs it with your own key. 5 owner_* · 1 yocto 5 owner_* calls arrive with exactly 1 yoctoNEAR. 6 6 Swaps go to allowlisted venues; the output comes back to the account. 7 7 The factory created the account; its later changes reach new accounts. 8 execute_order 8 The order service sends stored orders with the 24/7 key. 9 9 The watcher checks every order sent and can only pause the service.

Trust boundaries: who holds which key

Four zones. In your browser, the terminal holds no key; the signer frame on its own origin holds the trading keys; the wallet bridge on its own origin carries owner calls. Your wallet holds the owner's full-access key. On NEAR, the trading account checks every call, swaps only on allowlisted venues with the output returned to itself, and was created by the factory. On our servers, the engine serves data and quotes, the order service holds the 24/7 key and the watcher checks every order it sends and can only pause it. Your browser Signer frame trading keys Terminal holds no key Wallet bridge owner calls Your wallet Owner wallet full-access key NEAR Venues Rhea, DCL, Plach Trading account checks every call Factory trade.unrlzd.near Our servers Engine data and quotes Order service holds the 24/7 key Watcher can only pause 1 The terminal asks the frame to sign, by postMessage. 2 The signed call goes through an RPC node to your account. 3 Owner calls go to the wallet bridge, which checks each one. 4 Your wallet shows the call and signs it with your own key. 5 owner_* calls arrive with exactly 1 yoctoNEAR. 6 Swaps go to allowlisted venues; the output comes back to the account. 7 The factory created the account; its later changes reach new accounts. 8 The order service sends stored orders with the 24/7 key. 9 The watcher checks every order sent and can only pause the service.
Figure 1.The four trust zones and the nine paths between them. Blue is a trade reaching your account and the swap it makes; amber is the watcher, which can only pause.

The terminal page holds no key. It asks the signer frame for every signature. The frame holds your trading keys, which it created as non-exportable WebCrypto keys, and it checks each request before it signs. Owner calls go to the wallet bridge, another separate site that checks the call before your wallet sees it. Keys and signing has the details.

Your wallet keeps the owner’s own key. Every owner_* method needs it and exactly 1 yoctoNEAR (trading-account/lib.rs:2850-2863). A function call key can’t attach a deposit, so a trading key or a 24/7 key can’t call them.

The trading account runs the same contract code as every other account, named by its hash. It swaps only on allowlisted venues and only for output that comes back to itself. The factory created it. The factory’s admin is the DAO (unrlzd.sputnik-dao.near). Its changes reach new accounts only, after a 24-hour wait (factory/lib.rs:431-445).

The engine serves market data and quotes; nothing it says is trusted for anything signed. The order service holds the 24/7 key, which can only send orders you stored. The watcher checks every order the service sends and can only pause it.

None of these servers holds a key that can send your money out of the account. A wrong or missing answer from the engine changes what you see, not what the contract accepts: the frame signs only what passes its own checks, the reads a signed value rests on go to RPC providers other than the engine, and prices are cross-checked against on-chain pools before you sign (Prices and guardrails).

NEAR checks a key before any contract code runs. A function call key names one receiver and a list of methods, and it “cannot be used to transfer $NEAR” (Nomicon). A transaction that calls another contract or another method fails at that check. Your trading account holds keys of this kind, and the contract’s two ways to add a key both give it a fixed method list (trading-account/lib.rs:1732-1747, trading-account/lib.rs:3196-3214).

Each device you set up gets one trading key, created in the signer frame. Its receiver is your trading account and its methods are the ten in DEVICE_METHODS (trading-account/lib.rs:29). Grouped by what they do with money:

  • keep it in the account: execute (a swap), place_order, cancel_order, execute_order, withdraw_from_intents;
  • pay the owner: withdraw_to_owner;
  • pay an address the owner listed: withdraw_cross_chain;
  • lower a limit: lower_caps, remove_withdraw_destination, revoke_automation.

No method on that list raises a cap, adds an address, adds a key or changes code. Those are owner_* methods, and a trading key can’t call them, because it can’t attach the 1 yoctoNEAR they need.

If you turn on 24/7 orders, your owner wallet signs for one more key. Its only method is execute_order (trading-account/lib.rs:31), and its allowance of at least 0.5 NEAR is all the gas it can burn: NEAR takes each fee from the allowance and refuses the key once it runs out (trading-account/lib.rs:1850-1853). The contract checks each call against an order you stored, at the stored terms. A trading key removes it with revoke_automation.

Attack What stops it
A script on the terminal page asks for a token transfer out The frame signs only calls to your trading account, on its method list
The same script asks for a withdrawal withdraw_to_owner has no destination: funds go to the owner
It adds its own withdrawal address Only the owner wallet adds one, and it waits 1 hour
A swap that pays someone else Allowlisted venues only, output to the account
Copying a trading key off a device Keys are created non-exportable; the owner wallet removes a key
A leaked 24/7 key execute_order only, each order as stored, a minimum no lower
The factory’s admin ships bad code 24 h wait, new accounts only; you upgrade by choice
A fee raise Fee set at creation, at most 1%; no method in this code changes it
The engine lies about a price or an order Engine data doesn’t decide what is signed; chain reads go to other RPC providers

Each account’s code is a global contract named by hash, which can’t change after it is deployed. New code reaches your account only through owner_upgrade, signed by you, or through auto-upgrade, which is off until you turn it on. Auto-upgrade waits 72 hours after it sees new approved code, and your owner wallet or a trading key can veto it in that time. Verify the contracts shows how to check every hash.

Outside trades, the one way a trading key can pay anyone but the owner is withdraw_cross_chain, and only to an address the owner listed (trading-account/lib.rs:2453-2543).

The hour gives you time to see an address you didn’t add and remove it. The signer frame adds its own check: you confirm the address in a window the terminal can’t draw over.

We can pause parts of our own service if something goes wrong: new buys and new orders, 24/7 sends, and quotes for deposits from or withdrawals to other networks. A pause only restricts. Our apps and the order service refuse what it covers; it can’t move funds, change a key or touch the contract. It never stops withdrawing to your owner wallet, or any owner wallet call.

Pauses don’t stop chain calls that skip our servers. Your owner wallet can call owner_withdraw from any NEAR wallet or the command line.

The factory’s state is public. This reads its pending changes and its timelock:

Terminal window
curl -s https://rpc.mainnet.near.org -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"query","params":{"request_type":"call_function","finality":"final",
"account_id":"trade.unrlzd.near","method_name":"get_admin_state","args_base64":"e30="}}' \
| jq -r '.result.result | implode'
# {"pending_code":null,"code_timelock_ns":"86400000000000","pending_admin":null, ... "creation_paused":false, ...}

A non-null pending_code is new code waiting out its 24 hours. Use get_config the same way to read the admin and the fee.

Sources

  1. Nomicon, Access keys
  2. NEP-591, Global contracts

Last verified 15 October 2026 against R4.