How it works
A WARDEN vault is a contract you own that holds your agent's funds. The agent keeps its own key, but that key holds nothing of value: the only thing it can do is call execute(target, value, data) on the vault. The vault runs the request through four walls and either makes the call, queues it, or reverts with an error that names the wall.
Rather see it than read it? The sandbox runs these exact rules in your browser.
Your owner wallet is never given to the agent. From it you can halt the agent, cancel held moves, change limits and the allowlist, and withdraw everything at any time.
The walls, in order
I. Halt
When the owner calls halt(), every execute and executeHeld reverts with Halted() until resume(). The agent has no way to call either.
II. Filter
Each move is matched on its target contract and its 4-byte function selector. Only pairs set with setAllowed pass. A plain SOL send has selector 0x00000000 and must be allowed per address like any other call. Calls to approve or increaseAllowance for type(uint256).max, and setApprovalForAll(…, true), are always refused.
III. Limit
SOL value is checked against a per-transaction cap, then counted into an hourly and a daily window (clock hour, UTC day). Token transfer and transferFrom calls are counted against a daily cap per token; a token with no cap can't be moved at all.
IV. Hold
A move whose SOL value is at or above holdThreshold isn't sent. It's queued with a release time of now plus holdDelay. After that, the agent or owner calls executeHeld(id), and every wall is checked again at that moment. The owner can cancelHeld(id) any time before.
Agent SDK
One file, warden.js, on @solana/web3.js. Every move is simulated with staticCall first, so a refused move returns the wall and reason without spending a fee.
import { Connection, Keypair } from "@solana/web3.js"; import { WardenAgent, sol } from "./warden.js"; const connection = new Connection(RPC_URL); const agentKey = Keypair.fromSecretKey(AGENT_KEY); // holds fees only const vault = new WardenAgent(connection, "YourVault…", agentKey); // plain SOL send await vault.send("Payee…", sol("0.05")); // contract call const r = await vault.call(router, routerAbi, "swapExactSOLForTokens", [min, path, vault.address, deadline], sol("0.2")); if (!r.ok) console.log(r.wall, r.reason); // "Limit" "OverHourCap(…)" if (r.held) console.log("queued", r.heldId, r.eta); // later for (const h of await vault.pending()) if (h.eta <= Date.now() / 1000) await vault.release(h.id);
| Method | What it does |
|---|---|
check(target, value, data) | Dry run. Returns { ok, held } or { ok:false, wall, reason }. |
send(target, value, data) | Check, then execute. Returns the tx hash, or the held id and release time. |
call(target, abi, fn, args, value) | Encodes the call and sends it. |
release(id) | Releases a held move after its delay. |
pending() | Lists held moves still waiting. |
status() | Owner, agent, halt state, balance, limits and spend. |
The agent's own key only needs a little SOL for fees. If it leaks, the attacker gets the same walls your agent has, and you can swap it with setAgent.
Errors
| Error | Wall | Meaning |
|---|---|---|
Halted() | Halt | The owner froze the vault. |
NotAllowlisted(target, selector) | Filter | That contract and function pair isn't allowed. |
InfiniteApproval() | Filter | Unlimited approvals are never allowed. |
OverTxCap(value, cap) | Limit | Single move above the per-tx cap. |
OverHourCap(wouldBe, cap) | Limit | Would pass the hourly total. |
OverDayCap(wouldBe, cap) | Limit | Would pass the daily total. |
OverTokenCap(token, wouldBe, cap) | Limit | Would pass the token's daily cap. |
StillHeld(id, eta) | Hold | Release time not reached. |
NotPending(id) | Hold | Already released or cancelled. |
NotAgent() / NotOwner() | Access | Wrong caller. |
CallFailed(reason) | Target | The target contract itself reverted. |
Owner controls
Everything here is available in the console with your owner wallet.
| Function | Effect |
|---|---|
halt() / resume() | Freeze or reopen the agent. |
cancelHeld(id) | Drop a queued move. |
setPolicy(policy) | Change caps, hold line and delay. |
setAllowed(targets, selectors, ok) | Add or remove allowlist pairs. |
setTokenCap(token, cap) | Daily cap for a token, in its own units. |
setAgent(agent) | Swap the agent key. |
withdraw / withdrawToken | Take funds out, even while halted. |
ownerCall(target, value, data) | Any call from the vault, no walls. |
transferOwnership / acceptOwnership | Two-step owner change. |
Contracts
WardenFactory.createVault(agent, policy, targets, selectors) deploys a vault owned by the caller, with its allowlist set in the same transaction and an optional SOL deposit. vaultsOf(owner) lists an owner's vaults.
Source: WardenVault.sol · WardenFactory.sol · ABI. Covered by 20 tests including fuzzing on the daily cap and on unlisted targets, and a reentrancy test.
Network
| Token | $WARDEN on Solana |
|---|---|
| Contract | Soon |
| Explorer | solscan.io |