Skip to content
WARDEN

Contracts and events

Two contracts. WardenFactory deploys one WardenAccount per owner and salt, as an ERC-1167 clone at a predictable address. Each account holds its owner's funds and every agent's policy. Solidity 0.8, OpenZeppelin 5, built and tested with Foundry.

Addresses#

ContractBase Sepolia (84532)
WardenFactory0x1a4aA3C45E9fdF8845dBdfcAb6428f5f7D0272F6
WardenAccount (implementation)0x2d8eFc03262f4cc905d344569a3f541B0233CA92
Demo account0x6498509548D66e7b9F4CE1ae0a7463Ccd0dcDce4
USDC (Circle test token)0x036CbD53842c5426634e7929541eC2318f3dCF7e

Deployed at block 48,317,558.

ABIs#

These are the files the dashboard itself uses, generated from the compiled contracts.

WardenFactory#

solidity
function createAccount(address owner, bytes32 salt) external returns (address account);
function predictAddress(address owner, bytes32 salt) external view returns (address);
function implementation() external view returns (address);

event AccountCreated(address indexed owner, address indexed account, bytes32 salt);

The CREATE2 salt is keccak256(abi.encode(owner, salt)), so nobody else can occupy an owner's address with an account they control. createAccount returns the existing account instead of reverting, so a front-run deployment cannot make the owner's transaction fail. The dashboard and @warden/sdk use the salt "main", as left-aligned ASCII bytes32, for an owner's first account, then "account-2", "account-3" and so on, so both predict the same address.

To find an owner's accounts, filter AccountCreated by the indexed owner topic.

WardenAccount#

Agent functions#

solidity
function pay(address token, address to, uint256 amount, bytes32 memo) external returns (uint256 requestId);
function call(address target, uint256 value, bytes calldata data, bytes32 memo) external returns (uint256 requestId);
function check(address agent, address token, address to, uint256 amount, address target, bytes calldata data)
    external view returns (Decision decision, bytes32 reason);

requestId is 0 when the action executed, or the id of a new approval request. check evaluates pay(token, to, amount) when target is address(0), and otherwise call(target, amount, data) with amount as the native value. It never reverts. Decision is 0 Allowed, 1 NeedsApproval, 2 Denied.

Owner functions#

solidity
function setAgent(address agent, Policy calldata policy, address[] calldata targets,
    Selector[] calldata selectors, Threshold[] calldata thresholds) external;
function revokeAgent(address agent) external;
function approve(uint256 requestId) external;
function reject(uint256 requestId, bytes32 reason) external;
function setPaused(bool paused) external;
function setRequestTtl(uint64 ttl) external;
function withdraw(address token, address to, uint256 amount) external;
function transferOwnership(address next) external;
function acceptOwnership() external;

Deposits: send ETH to the account, or call depositToken(token, amount) after approving the account. Both emit Deposited.

Views#

solidity
function owner() external view returns (address);
function pendingOwner() external view returns (address);
function factory() external view returns (address);
function paused() external view returns (bool);
function requestTtl() external view returns (uint64);
function nextRequestId() external view returns (uint256);
function isAgent(address agent) external view returns (bool);
function policyHash(address agent) external view returns (bytes32);
function getAgent(address agent) external view returns (Policy, address[], Selector[], Threshold[]);
function getRequest(uint256 requestId) external view returns (Request memory);
function spending(address agent, address token) external view returns (uint64 windowStart, uint128 spent, uint256 remaining);
function isAllowedTarget(address agent, address target) external view returns (bool);
function isAllowedSelector(address agent, address target, bytes4 selector) external view returns (bool);
function approvalThreshold(address agent, address token) external view returns (uint256);

Events#

EventFields
Spentagent (indexed), token (indexed), to (indexed), amount, memo, requestId, policyHash
Calledagent (indexed), target (indexed), selector, value, memo, requestId, policyHash
ApprovalRequestedrequestId (indexed), agent (indexed), token, to, amount, memo, expiresAt
ApprovedrequestId (indexed)
RejectedrequestId (indexed), reason
AgentSetagent (indexed), policyHash
AgentRevokedagent (indexed)
Pausedpaused
Depositedfrom (indexed), token (indexed), amount
Withdrawntoken (indexed), to (indexed), amount
RequestTtlSetttl
OwnershipTransferStarted, OwnershipTransferredpreviousOwner (indexed), newOwner (indexed)

A call emits one Spent per amount it counted (native value, and the token amount of ERC-20 calldata) before its Called, so summing Spent gives an agent's full outflow. For a call request, ApprovalRequested describes the spend that crossed the threshold; getRequest returns the full calldata.

Errors#

ErrorMeaning
Denied(bytes32 reason)The action breaks the policy. See denial reasons.
NotOwner()Only the owner can call this.
NotPendingOwner()Only the nominated address can accept ownership.
ZeroAddress()An address argument was address(0).
InvalidAgent(address)The agent is address(0), the account or the factory.
InvalidValidity(uint64, uint64)validUntil is not later than validAfter.
DuplicateLimit(address)The same token appears twice in limits.
InvalidTtl(uint64)requestTtl must be between 1 second and 30 days.
RequestNotPending(uint256)The request was already approved or rejected, or never existed.
RequestExpired(uint256, uint64)The request is past its expiry.
NativeTransferFailed(address, uint256)The recipient refused ETH.
CallFailed(address, bytes)The target reverted; its revert data is passed through.

Reading the history#

The account's events are the whole audit trail. A few practical notes from building the dashboard:

  • sepolia.base.org limits eth_getLogs to 200 blocks per request. Base produces a block every two seconds, so use a provider that allows wide ranges (the dashboard uses publicnode and Tenderly's public gateways) or index the events yourself.
  • Start from the factory's deployment block, not from zero.
  • Block timestamps come from getBlock; cache them, they never change.