Olinea documentation

Olinea is a USDC vault on Arc whose releases are authorized by SLH-DSA-SHA2-128s (FIPS 205) signatures verified on-chain by Arc's post-quantum precompile. There is no owner, no admin key, no pause and no upgrade path.

Overview

The vault holds USDC through the ERC-20 interface at 0x3600000000000000000000000000000000000000 and releases it only for a 32-byte hash-based key fixed at deployment. That key is not an elliptic-curve key: breaking ECDSA does not forge a release.

Two details carry the design:

  1. The precompile returns false for a forged signature without reverting. A contract that checks only whether the call succeeded accepts every forgery. See Security model.
  2. The key signs exactly keccak256(abi.encode(chainId, vault, to, amount, nonce)), so an authorization cannot be replayed on another chain, in another vault, for another recipient or amount, or twice in the same vault. See Authorization digest.
The browser derives an SLH-DSA key from 24 words in a Web Worker and signs a 32-byte digest that binds the chain id, the vault, the recipient, the amount and a nonce. The signature is relayed to an OlineaVault on Arc, which asks the post-quantum precompile for a boolean and only then transfers USDC.
Left of the dashed line is your machine — no account, no server. The digest is the whole authorization: change any one of its five fields and the signature is worthless elsewhere. The vault asks the precompile and requires true before it moves anything. Full size

Status

ComponentStatus
PQ precompile ABIMapped from Circle's public arc-node source, then verified against Arc mainnet: real signature true; tampered signature, wrong key and wrong message all false
OlineaVault.sol19 Foundry tests green; its exact authorization digest verified against the live mainnet precompile (3/3)
OlineaFactory.sol12 Foundry tests green; one transaction per vault, no owner and nothing to administer
Vault consoleLive against Arc mainnet — key derivation, the precompile proof and every read; the create, deposit and release path is verified end to end on a local chain
Arc mainnet deploymentFactory deployed — runbook
Independent auditNone

Quickstart

Verifying is a read call — free, no wallet. You can check the whole authorization path against Arc mainnet before deploying anything.

From the browser

The vault console does the same without a terminal: 24 words become an SLH-DSA key in the browser, the key is proven free against Arc's precompile, and every release is checked on-chain before it is sent. Nothing is uploaded.

The console has one rule the CLI does not. Its 24 words go through PBKDF2 then HKDF — salt olinea/slh-dsa/v1, info vault/<index> — to make the 48-byte seed slh_dsa_sha2_128s.keygen takes.

noble calls that keygen a library hook, not a FIPS 205 derivation. So the version is pinned and the salt is versioned: a backup made under a later salt will not open a vault made under this one. A different index gives an unrelated vault from the same backup.

# the vault and its test suite
git clone https://github.com/thesithunyein/olinea
cd olinea/contracts && forge test            # 31 tests

# post-quantum tooling
cd ../scripts && npm install
node pq.mjs keygen                            # SLH-DSA keypair -> scripts/keys/pq-key.json
node pq.mjs conformance                       # real mainnet precompile, no wallet

Keep the key. keygen writes it to scripts/keys/pq-key.json (gitignored). Lose it and the funds in that vault are unrecoverable — there is no admin escape hatch.

OlineaVault reference

contracts/src/OlineaVault.sol

Constructor

constructor(IUSDC usdc_, bytes memory verifyingKey_, address verifier_)

// usdc_          Arc USDC ERC-20 (6 decimals)
// verifyingKey_  32-byte SLH-DSA-SHA2-128s verifying key; any other length reverts
// verifier_      address(0) -> the canonical Arc precompile 0x1800..0004

verifier_ exists only so tests can inject a fake verifier. Production passes address(0).

Read

FunctionReturns
usdc()The USDC ERC-20 address the vault holds
pqVerifier()The address used for verification — the canonical precompile unless overridden
verifyingKey()The 32-byte key that authorizes releases
balance()USDC held, in the 6-decimal ERC-20 unit
nonceUsed(uint256)Whether a nonce has been consumed
authorizationDigest(address to, uint256 amount, uint256 nonce)The exact 32 bytes a release must be signed over

Write

FunctionEffect
deposit(uint256 amount)Pulls USDC from the caller. Open to anyone; a deposit grants no rights over the balance
release(address to, uint256 amount, uint256 nonce, bytes signature)Transfers USDC for a valid authorization. Anyone may relay; the relayer gains nothing
// canonical signatures, for cast, viem or ethers
deposit(uint256)
release(address,uint256,uint256,bytes)

Events and errors

event Deposited(address indexed from, uint256 amount);
event Released(address indexed to, uint256 amount, uint256 nonce, bytes32 digest);

error InvalidVerifyingKeyLength(uint256 length);
error InvalidRecipient();
error ZeroAmount();
error InsufficientBalance(uint256 requested, uint256 available);
error NonceAlreadyUsed(uint256 nonce);
error PrecompileCallFailed();
error InvalidPostQuantumSignature();
error TransferFailed();

InvalidPostQuantumSignature is the one you will see when an authorization is wrong. PrecompileCallFailed means the call itself reverted — malformed input, not a bad signature.

Authorization digest

The signing key never touches the chain. It signs one 32-byte digest per release:

digest = keccak256(abi.encode(block.chainid, address(vault), to, amount, nonce))

All five values are inside the digest, so a signature is valid for exactly one release: another chain changes chainid, another vault changes the address, and a replay in the same vault fails on the consumed nonce. Nonces are per-vault and need not be sequential — only unique.

The digest is the message, with no extra hashing by the caller: the vault passes abi.encodePacked(digest), 32 bytes.

PQ precompile reference

PropertyValue
Address0x1800000000000000000000000000000000000004 (genesis-fixed)
Selector0xbf4db8ba — verifySlhDsaSha2128s(bytes,bytes,bytes)
Arguments(bytes verifyingKey /*32 B*/, bytes message /*any length*/, bytes signature /*7856 B*/)
Returnsbool — false for an invalid signature
Gas444,122–448,502 gas per release, measured on two releases
Cost0.00888–0.00897 USDC at Arc's base fee of about 20 Gwei

When it reverts

Only malformed input reverts: Input too short, Invalid selector, Invalid verifying key length, Invalid signature length. An invalid signature is a value, not an exception — that asymmetry is the whole footgun.

Calling it correctly

// WRONG — true for every forgery
(bool ok, ) = PQ.staticcall(data);
require(ok, "verify failed");

// CORRECT — decode the boolean the precompile returned
(bool ok, bytes memory ret) = PQ.staticcall(data);
require(ok && ret.length == 32 && abi.decode(ret, (bool)), "invalid PQ signature");

The precompile is in Arc's execution layer, not upstream EVM, so a local test suite needs Arc Foundry (arc-anvil --network arc) — prebuilt for Linux and macOS (Apple Silicon) only. Olinea's suite injects the verifier and proves the real precompile against mainnet instead.

Units on Arc. USDC appears twice on one balance: natively with 18 decimals for gas, and as ERC-20 at 0x3600000000000000000000000000000000000000 with 6 for transfers. Mixing them is a 1012 error. Olinea only uses the 6-decimal view; gas is 2 × 10⁻⁸ USDC per unit.

CLI reference

node pq.mjs keygen
node pq.mjs authorize --vault 0x.. --to 0x.. --amount 1000000 --nonce 1 [--check]
node pq.mjs conformance [--vault 0x..]
CommandWhat it does
keygenGenerates an SLH-DSA-SHA2-128s keypair and writes it to scripts/keys/pq-key.json
authorizePrints the digest, the signature for one release and a ready-to-run cast send line. --check verifies it against the live mainnet precompile
conformanceRuns three cases — valid digest, tampered signature, different digest — against the real precompile, and exits non-zero on failure, so it works as a pre-deploy gate

Set ARC_RPC for a different endpoint. The default is https://rpc.drpc.mainnet.arc.io, because Arc's own RPC hosts are not reachable from every jurisdiction.

Security model

What the contract guarantees:

What it does not do:

Limitations

Unaudited. The contracts are new code. A test suite and mainnet evidence are not a substitute for review. Do not put amounts you cannot lose into an unaudited vault.

What happens when it goes wrong

The questions an auditor asks first, each one settled by a test rather than an adjective. The long version, with the files, is in SECURITY.md.

The site is part of this: every page, module, font and geometry it loads is served from one origin, so a cold load names no host that could be slow, wrong or gone. That is asserted in scripts/check-site.mjs, not promised.

Deploying a vault

Step 1 is free: generate the key and check its digest against the live precompile before anything is on chain. Steps 2 onwards need a funded Arc account — gas is paid in USDC.

export ARC_RPC=https://rpc.drpc.mainnet.arc.io
export ARC_PRIVATE_KEY=0x..                    # a few USDC for gas
USDC=0x3600000000000000000000000000000000000000

# 1. key + conformance (free, no wallet)
cd scripts && node pq.mjs keygen && node pq.mjs conformance

# 2. deploy (verifier = address(0) -> the real precompile)
cd ../contracts && forge create src/OlineaVault.sol:OlineaVault \
  --rpc-url $ARC_RPC --private-key $ARC_PRIVATE_KEY --broadcast \
  --constructor-args $USDC 0x<your-32-byte-vk> 0x0000000000000000000000000000000000000000

# 3. fund it
cast send $USDC "approve(address,uint256)" $VAULT 2000000 --rpc-url $ARC_RPC --private-key $ARC_PRIVATE_KEY
cast send $VAULT "deposit(uint256)" 1000000 --rpc-url $ARC_RPC --private-key $ARC_PRIVATE_KEY

# 4. authorize a release (prints the signature and a cast send line)
node ../scripts/pq.mjs authorize --vault $VAULT --to $RECIPIENT --amount 250000 --nonce 1 --check

# 5. verify the vault's balance afterwards
cast call $VAULT "balance()(uint256)" --rpc-url $ARC_RPC

The verifying key is immutable — to change keys, deploy a new vault. Treat the deployed key as the whole security boundary from the first USDC that lands.