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
Trust boundaries: who holds which key
Trust boundaries
Section titled “Trust boundaries”In your browser
Section titled “In your browser”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
Section titled “Your wallet”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.
On NEAR
Section titled “On NEAR”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).
On our servers
Section titled “On our servers”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).
What each key can call
Section titled “What each key can call”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).
Trading keys
Section titled “Trading keys”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.
The 24/7 key
Section titled “The 24/7 key”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.
Threats and what stops them
Section titled “Threats and what stops them”| 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 |
Code changes
Section titled “Code changes”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.
Withdrawal addresses
Section titled “Withdrawal addresses”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.
What a pause can and can’t do
Section titled “What a pause can and can’t do”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.
Check it yourself
Section titled “Check it yourself”The factory’s state is public. This reads its pending changes and its timelock:
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
Last verified 15 October 2026 against R4.