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 ETH 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

ETH 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 ETH 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 ethers v6. Every move is simulated with staticCall first, so a refused move returns the wall and reason without spending gas.

import { JsonRpcProvider, Wallet, parseEther } from "ethers";
import { WardenAgent, ROBINHOOD_CHAIN } from "./warden.js";

const provider = new JsonRpcProvider(ROBINHOOD_CHAIN.rpc);
const agentKey = new Wallet(process.env.AGENT_KEY, provider); // holds gas only
const vault = new WardenAgent("0xYourVault", agentKey);

// plain ETH send
await vault.send("0xPayee", parseEther("0.05"));

// contract call
const r = await vault.call(router, routerAbi, "swapExactETHForTokens", [min, path, vault.address, deadline], parseEther("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);
MethodWhat 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 ETH for gas. If it leaks, the attacker gets the same walls your agent has, and you can swap it with setAgent.

Errors

ErrorWallMeaning
Halted()HaltThe owner froze the vault.
NotAllowlisted(target, selector)FilterThat contract and function pair isn't allowed.
InfiniteApproval()FilterUnlimited approvals are never allowed.
OverTxCap(value, cap)LimitSingle move above the per-tx cap.
OverHourCap(wouldBe, cap)LimitWould pass the hourly total.
OverDayCap(wouldBe, cap)LimitWould pass the daily total.
OverTokenCap(token, wouldBe, cap)LimitWould pass the token's daily cap.
StillHeld(id, eta)HoldRelease time not reached.
NotPending(id)HoldAlready released or cancelled.
NotAgent() / NotOwner()AccessWrong caller.
CallFailed(reason)TargetThe target contract itself reverted.

Owner controls

Everything here is available in the console with your owner wallet.

FunctionEffect
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 / withdrawTokenTake funds out, even while halted.
ownerCall(target, value, data)Any call from the vault, no walls.
transferOwnership / acceptOwnershipTwo-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 ETH 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.

Factory address: published at deployment.

Network

ChainRobinhood Chain (Arbitrum Orbit L2)
Chain ID4663
RPChttps://rpc.mainnet.chain.robinhood.com
Explorerrobinhoodchain.blockscout.com
GasETH