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#
| Contract | Base Sepolia (84532) |
|---|---|
| WardenFactory | 0x1a4aA3C45E9fdF8845dBdfcAb6428f5f7D0272F6 |
| WardenAccount (implementation) | 0x2d8eFc03262f4cc905d344569a3f541B0233CA92 |
| Demo account | 0x6498509548D66e7b9F4CE1ae0a7463Ccd0dcDce4 |
| 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#
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#
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#
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#
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#
| Event | Fields |
|---|---|
Spent | agent (indexed), token (indexed), to (indexed), amount, memo, requestId, policyHash |
Called | agent (indexed), target (indexed), selector, value, memo, requestId, policyHash |
ApprovalRequested | requestId (indexed), agent (indexed), token, to, amount, memo, expiresAt |
Approved | requestId (indexed) |
Rejected | requestId (indexed), reason |
AgentSet | agent (indexed), policyHash |
AgentRevoked | agent (indexed) |
Paused | paused |
Deposited | from (indexed), token (indexed), amount |
Withdrawn | token (indexed), to (indexed), amount |
RequestTtlSet | ttl |
OwnershipTransferStarted, OwnershipTransferred | previousOwner (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#
| Error | Meaning |
|---|---|
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.orglimitseth_getLogsto 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.