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
Contract owner with administrative privileges
Authorized agent wallet that can withdraw funds
Emergency freeze flag (kills all withdrawals when true)
Maximum spending allowed per day (in wei)
Amount spent in current day period (in wei)
Day counter for daily limit resets (block.timestamp / 1 days)
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
Recipient address (must be allowlisted and non-zero)
Amount of native ETH to transfer (in wei)
Access Control : Only callable by the configured agent address
Policy Checks :
Vault must not be frozen
Recipient must be allowlisted
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
New daily limit in wei Example: 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
Address to add or remove from allowlist
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
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
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
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
Recipient address (no allowlist check)
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
event Deposit ( address indexed from , uint256 amount );
Emitted when ETH is deposited to the vault.
event Withdrawal ( address indexed to , uint256 amount );
Emitted when agent withdraws funds.
event PolicyUpdated ( uint256 newDailyLimit );
Emitted when daily limit is updated.
event AllowlistUpdated ( address indexed target , bool allowed );
Emitted when allowlist is modified.
event Frozen ( bool status );
Emitted when vault freeze status changes.
event AgentUpdated ( address indexed newAgent );
Emitted when agent address is updated.
event EmergencyWithdrawal ( address indexed to , uint256 amount );
Emitted during emergency withdrawals.
event OwnershipTransferred (
address indexed oldOwner ,
address indexed newOwner
);
Emitted when ownership is transferred.
Errors
Function restricted to contract owner.
Function restricted to authorized agent.
Vault is frozen, withdrawals disabled.
Withdrawal would exceed daily spending limit.
Recipient address not on allowlist.
Invalid zero address provided.
Usage Example
Setup Vault
Agent Withdrawal
Monitor Activity
Daily Limit Tracking
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:
Owner adds Escrow to allowlist : setAllowlist(escrowAddress, true)
Agent creates escrow : Agent calls escrow.createEscrow() from their wallet
PolicyVault funds escrow : Agent withdraws from vault to escrow contract
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 wallet address authorized to withdraw
Initial daily spending limit in wei Example 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