Skip to main content
The PolicyVault contract provides a secure treasury for AI agents with onchain spending policies, daily limits, allowlists, and emergency controls.

Contract Overview

Location: src/PolicyVault.sol:13 Purpose: Agent treasury management with automated policy enforcement Asset Type: Native ETH (Base Sepolia) Security: ReentrancyGuard, daily limits, allowlists, freeze mechanism

State Variables

owner
address
Contract owner with administrative privileges
agent
address
Authorized agent wallet that can withdraw funds
frozen
bool
Emergency freeze flag (kills all withdrawals when true)
dailyLimit
uint256
Maximum spending allowed per day (in wei)
spentToday
uint256
Amount spent in current day period (in wei)
lastResetDay
uint256
Day counter for daily limit resets (block.timestamp / 1 days)
allowlist
mapping(address => bool)
Addresses approved to receive withdrawals

Functions

Withdraw Funds

Agent withdraws native ETH within policy limits.
function withdraw(address to, uint256 amount) 
    external onlyAgent notFrozen nonReentrant
to
address
required
Recipient address (must be allowlisted and non-zero)
amount
uint256
required
Amount of native ETH to transfer (in wei)
Access Control: Only callable by the configured agent address Policy Checks:
  1. Vault must not be frozen
  2. Recipient must be allowlisted
  3. Amount must not exceed remaining daily limit
Daily Limit Logic:
  • Resets automatically at midnight UTC (every 86400 seconds)
  • Tracks cumulative spending across multiple withdrawals
  • Prevents exceeding dailyLimit in any 24-hour period
Events Emitted:
event Withdrawal(address indexed to, uint256 amount);
Errors:
  • OnlyAgent() - Caller is not the authorized agent
  • VaultFrozen() - Vault is frozen
  • ZeroAddress() - Recipient is zero address
  • NotAllowlisted() - Recipient not on allowlist
  • ExceedsDailyLimit() - Would exceed daily spending limit
  • TransferFailed() - ETH transfer failed
Example:
// Agent withdraws 1 ETH to escrow contract
const tx = await policyVault.withdraw(
  ESCROW_ADDRESS,
  ethers.parseEther("1")
);
await tx.wait();

console.log("Withdrawal successful");
Daily limit resets are based on UTC days (block.timestamp / 86400). The counter increments at midnight UTC, not from the time of first withdrawal.

Deposit Funds

Anyone can deposit native ETH to the vault.
receive() external payable
Events Emitted:
event Deposit(address indexed from, uint256 amount);
Example:
// Send ETH to vault
const tx = await signer.sendTransaction({
  to: POLICY_VAULT_ADDRESS,
  value: ethers.parseEther("10")
});
await tx.wait();

Owner Functions

Set Daily Limit

Update the daily spending limit.
function setDailyLimit(uint256 _limit) external onlyOwner
_limit
uint256
required
New daily limit in weiExample: ethers.parseEther("100") = 100 ETH per day
Events Emitted:
event PolicyUpdated(uint256 newDailyLimit);
Example:
// Set daily limit to 50 ETH
await policyVault.setDailyLimit(ethers.parseEther("50"));

Set Allowlist

Add or remove addresses from the allowlist.
function setAllowlist(address _target, bool _allowed) external onlyOwner
_target
address
required
Address to add or remove from allowlist
_allowed
bool
required
true to allow, false to deny
Events Emitted:
event AllowlistUpdated(address indexed target, bool allowed);
Errors:
  • ZeroAddress() - Target is zero address
Example:
// Allow escrow contract to receive funds
await policyVault.setAllowlist(ESCROW_ADDRESS, true);

// Remove address from allowlist
await policyVault.setAllowlist(OLD_ESCROW_ADDRESS, false);

Set Frozen Status

Freeze or unfreeze the vault (emergency kill switch).
function setFrozen(bool _frozen) external onlyOwner
_frozen
bool
required
true to freeze, false to unfreeze
Events Emitted:
event Frozen(bool status);
Example:
// Emergency freeze
await policyVault.setFrozen(true);

// Resume operations
await policyVault.setFrozen(false);
When frozen, the agent cannot withdraw ANY funds, even within daily limits. Use this as an emergency kill switch.

Set Agent Address

Update the authorized agent wallet.
function setAgent(address _agent) external onlyOwner
_agent
address
required
New agent wallet address
Events Emitted:
event AgentUpdated(address indexed newAgent);
Errors:
  • ZeroAddress() - Agent is zero address

Transfer Ownership

Transfer contract ownership to a new address.
function transferOwnership(address newOwner) external onlyOwner
newOwner
address
required
New owner address
Events Emitted:
event OwnershipTransferred(
    address indexed oldOwner,
    address indexed newOwner
);
Errors:
  • ZeroAddress() - New owner is zero address

Emergency Withdraw

Owner-only emergency withdrawal bypassing daily limits.
function emergencyWithdraw(address to, uint256 amount) 
    external onlyOwner nonReentrant
to
address
required
Recipient address (no allowlist check)
amount
uint256
required
Amount to withdraw in wei
Events Emitted:
event EmergencyWithdrawal(address indexed to, uint256 amount);
Errors:
  • ZeroAddress() - Recipient is zero address
  • TransferFailed() - ETH transfer failed
Use Only in Emergencies: This function bypasses daily limits but NOT the freeze check. If the vault is frozen, even emergency withdrawals will fail.
Example:
// Emergency withdrawal to recover stuck funds
await policyVault.emergencyWithdraw(
  ownerAddress,
  ethers.parseEther("100")
);

View Functions

Get Balance

function getBalance() external view returns (uint256)
Returns the current ETH balance of the vault in wei.

Get Remaining Daily Limit

function getRemainingDaily() external view returns (uint256)
Returns the remaining spending allowance for the current day in wei. Logic:
  • If current day > lastResetDay: returns full dailyLimit
  • Otherwise: returns dailyLimit - spentToday (or 0 if exceeded)
Example:
const remaining = await policyVault.getRemainingDaily();
console.log(`Remaining today: ${ethers.formatEther(remaining)} ETH`);

Events

Deposit
event Deposit(address indexed from, uint256 amount);
Emitted when ETH is deposited to the vault.
Withdrawal
event Withdrawal(address indexed to, uint256 amount);
Emitted when agent withdraws funds.
PolicyUpdated
event PolicyUpdated(uint256 newDailyLimit);
Emitted when daily limit is updated.
AllowlistUpdated
event AllowlistUpdated(address indexed target, bool allowed);
Emitted when allowlist is modified.
Frozen
event Frozen(bool status);
Emitted when vault freeze status changes.
AgentUpdated
event AgentUpdated(address indexed newAgent);
Emitted when agent address is updated.
EmergencyWithdrawal
event EmergencyWithdrawal(address indexed to, uint256 amount);
Emitted during emergency withdrawals.
OwnershipTransferred
event OwnershipTransferred(
    address indexed oldOwner,
    address indexed newOwner
);
Emitted when ownership is transferred.

Errors

OnlyOwner
Function restricted to contract owner.
OnlyAgent
Function restricted to authorized agent.
VaultFrozen
Vault is frozen, withdrawals disabled.
ExceedsDailyLimit
Withdrawal would exceed daily spending limit.
NotAllowlisted
Recipient address not on allowlist.
TransferFailed
ETH transfer failed.
ZeroAddress
Invalid zero address provided.

Usage Example

import { ethers } from 'ethers';

const policyVault = new ethers.Contract(
  POLICY_VAULT_ADDRESS,
  POLICY_VAULT_ABI,
  signer
);

// 1. Set daily limit to 100 ETH
await policyVault.setDailyLimit(ethers.parseEther("100"));

// 2. Add escrow contract to allowlist
await policyVault.setAllowlist(ESCROW_ADDRESS, true);

// 3. Fund the vault
await signer.sendTransaction({
  to: POLICY_VAULT_ADDRESS,
  value: ethers.parseEther("500")
});

console.log("Vault configured and funded");

Security Considerations

Reentrancy Protection: The withdraw() and emergencyWithdraw() functions use the nonReentrant modifier and CEI pattern to prevent reentrancy attacks.
Daily Limit Resets: Limits reset based on UTC day boundaries (block.timestamp / 86400), not rolling 24-hour windows. Plan agent spending accordingly.
Allowlist Requirement: The agent can ONLY withdraw to allowlisted addresses. Ensure the escrow contract and any other necessary addresses are added before the agent operates.

Integration with Escrow

The PolicyVault is designed to work seamlessly with the Escrow contract:
  1. Owner adds Escrow to allowlist: setAllowlist(escrowAddress, true)
  2. Agent creates escrow: Agent calls escrow.createEscrow() from their wallet
  3. PolicyVault funds escrow: Agent withdraws from vault to escrow contract
  4. Escrow releases to seller: After task completion
This creates a trustless flow where:
  • Agent can only spend within daily limits
  • Agent can only send to approved contracts (escrow)
  • Owner retains emergency controls

Constructor Parameters

When deploying, the contract is initialized with:
constructor(address _agent, uint256 _dailyLimit)
_agent
address
required
Agent wallet address authorized to withdraw
_dailyLimit
uint256
required
Initial daily spending limit in weiExample from deployment script: ethers.parseEther("50")
Deployment Example:
const factory = new ethers.ContractFactory(abi, bytecode, signer);
const policyVault = await factory.deploy(
  agentAddress,
  ethers.parseEther("50") // 50 ETH daily limit
);

Next Steps

Deployment Guide

Deploy contracts to Base Sepolia

Escrow Contract

Integrate with escrow payments

Build docs developers (and LLMs) love