Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
AgentWallet.execute(payee, amount)
├── 1. PolicyEngine.enforce(agentId, amount, payee)
│ checks per-tx, hourly, daily limits
│ reverts if violated
├── 2. DelegationRegistry.checkAndRecordSpend(agentId, amount)
│ checks delegation budget up the whole tree
│ reverts if any ancestor is over-budget
│ no-op if this is a root agent
└── 3. USDC.transfer(payee, amount)
money only moves if both checks passed
146 changes: 146 additions & 0 deletions packages/contracts/src/core/AgentWallet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../interfaces/IAgentWallet.sol";
import "../interfaces/IPolicyEngine.sol";

/// @title AgentWallet
/// @notice One smart account per AI agent.
/// Holds USDC. Before every payment, asks PolicyEngine
/// for permission. If PolicyEngine reverts, no money moves.
///
/// @dev Deployed by AgentWalletFactory via CREATE2.
/// Initialised once — constructor is empty because CREATE2
/// minimal proxies can't take constructor args.
/// Call initialise() right after deployment.
contract AgentWallet is IAgentWallet, ReentrancyGuard {
using SafeERC20 for IERC20;

// ─── State ────────────────────────────────────────────────────
bytes32 private _agentId;
address private _owner;
address private _policyEngine;
address private _usdcToken;
bool private _initialised;

// ─── Modifiers ────────────────────────────────────────────────
modifier onlyOwner() {
if (msg.sender != _owner) revert NotOwner();
_;
}

modifier initialised() {
if (!_initialised) revert NotInitialised();
_;
}

// ─── Initialisation ───────────────────────────────────────────

/// @notice Called once by AgentWalletFactory after CREATE2 deploy.
/// Sets the agent's identity and connects it to PolicyEngine.
/// @param agentId_ keccak256(abi.encodePacked(address(this), chainId))
/// @param owner_ human who controls this agent
/// @param policyEngine_ deployed PolicyEngine address
/// @param usdcToken_ USDC contract address on this chain
function initialise(
bytes32 agentId_,
address owner_,
address policyEngine_,
address usdcToken_
) external {
// Can only be called once
if (_initialised) revert NotInitialised();
if (owner_ == address(0)) revert NotOwner();
if (policyEngine_ == address(0)) revert PolicyBlocked();
if (usdcToken_ == address(0)) revert TransferFailed();

_agentId = agentId_;
_owner = owner_;
_policyEngine = policyEngine_;
_usdcToken = usdcToken_;
_initialised = true;

emit AgentIdSet(agentId_);
}

// ─── Core: execute payment ────────────────────────────────────

/// @notice Pay `amount` USDC to `payee`.
/// Step 1: ask PolicyEngine if this is allowed.
/// Step 2: transfer USDC if yes.
/// Step 3: emit receipt.
/// @dev Only owner (or in future: authorised agent runner) calls this.
/// nonReentrant guards against re-entry on the USDC transfer.
function execute(
address payee,
uint128 amount
)
external
override
onlyOwner
initialised
nonReentrant
returns (bool approvalRequired)
{
if (amount == 0) revert ZeroAmount();
if (payee == address(0)) revert ZeroPayee();

uint128 bal = balance();
if (bal < amount) revert InsufficientBalance(bal, amount);

// ── Step 1: policy check ──────────────────────────────────
// enforce() reverts if any rule is violated.
// If it reverts, this whole transaction reverts — no money moves.
try IPolicyEngine(_policyEngine).enforce(_agentId, amount, payee)
returns (bool _approvalRequired)
{
approvalRequired = _approvalRequired;
} catch {
revert PolicyBlocked();
}

// ── Step 2: move USDC ─────────────────────────────────────
// SafeERC20 handles tokens that return false instead of reverting
IERC20(_usdcToken).safeTransfer(payee, uint256(amount));

// ── Step 3: emit receipt ──────────────────────────────────
emit PaymentExecuted(payee, amount, _agentId, approvalRequired);
}

// ─── Funding ──────────────────────────────────────────────────

/// @notice Anyone can deposit USDC to fund this agent.
/// Caller must have approved this contract first.
function deposit(uint128 amount) external override nonReentrant {
if (amount == 0) revert ZeroAmount();
IERC20(_usdcToken).safeTransferFrom(msg.sender, address(this), uint256(amount));
emit FundsDeposited(msg.sender, amount);
}

/// @notice Owner withdraws USDC back to any address.
function withdraw(
address to,
uint128 amount
) external override onlyOwner nonReentrant {
if (amount == 0) revert ZeroAmount();
if (to == address(0)) revert ZeroPayee();
uint128 bal = balance();
if (bal < amount) revert InsufficientBalance(bal, amount);
IERC20(_usdcToken).safeTransfer(to, uint256(amount));
emit FundsWithdrawn(to, amount);
}

// ─── Views ────────────────────────────────────────────────────

function agentId() external view override returns (bytes32) { return _agentId; }
function owner() external view override returns (address) { return _owner; }
function policyEngine() external view override returns (address) { return _policyEngine; }
function usdcToken() external view override returns (address) { return _usdcToken; }

function balance() public view override returns (uint128) {
return uint128(IERC20(_usdcToken).balanceOf(address(this)));
}
}
143 changes: 143 additions & 0 deletions packages/contracts/src/core/AgentWalletFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./AgentWallet.sol";
import "../interfaces/IPolicyEngine.sol";

/// @title AgentWalletFactory
/// @notice Deploys one AgentWallet per agent using CREATE2.
/// CREATE2 gives a deterministic address — you can predict
/// where the wallet will live before it is deployed.
///
/// After deploying the wallet, the factory:
/// 1. Calls wallet.initialise() to wire up owner + policy engine
/// 2. Grants the wallet ENFORCER_ROLE on PolicyEngine
/// so it is the only address allowed to call enforce()
contract AgentWalletFactory is Ownable {

// ─── State ────────────────────────────────────────────────────
address public immutable policyEngine;
address public immutable usdcToken;

// agentId => wallet address (registry)
mapping(bytes32 => address) public wallets;

// ─── Events ───────────────────────────────────────────────────
event AgentCreated(
bytes32 indexed agentId,
address indexed wallet,
address indexed owner
);

// ─── Errors ───────────────────────────────────────────────────
error AgentAlreadyExists(bytes32 agentId);
error ZeroAddress();
error DeployFailed();

// ─── Constructor ──────────────────────────────────────────────
constructor(
address policyEngine_,
address usdcToken_,
address factoryOwner_
) Ownable(factoryOwner_) {
if (policyEngine_ == address(0)) revert ZeroAddress();
if (usdcToken_ == address(0)) revert ZeroAddress();
if (factoryOwner_ == address(0)) revert ZeroAddress();

policyEngine = policyEngine_;
usdcToken = usdcToken_;
}

// ─── Core: create agent ───────────────────────────────────────

/// @notice Deploy a new AgentWallet for `agentOwner`.
/// The agentId is deterministic: keccak256(owner + salt).
/// Anyone can create an agent for any owner address.
///
/// @param agentOwner human who will control the agent
/// @param salt unique value — use a counter or UUID hash
/// @return wallet address of the deployed AgentWallet
/// @return agentId the agent's unique bytes32 identifier
function createAgent(
address agentOwner,
bytes32 salt
) external returns (address wallet, bytes32 agentId) {
if (agentOwner == address(0)) revert ZeroAddress();

// ── Derive agentId ────────────────────────────────────────
// Ties agent identity to: owner + salt + this chain
agentId = keccak256(
abi.encodePacked(agentOwner, salt, block.chainid)
);

if (wallets[agentId] != address(0))
revert AgentAlreadyExists(agentId);

// ── Deploy with CREATE2 ───────────────────────────────────
// CREATE2 address is determined by:
// keccak256(0xff ++ factory_address ++ salt ++ keccak256(bytecode))
// This means you can predict the wallet address BEFORE deploying.
bytes32 create2Salt = keccak256(abi.encodePacked(agentId));

wallet = _deploy(create2Salt);
if (wallet == address(0)) revert DeployFailed();

// ── Initialise the wallet ─────────────────────────────────
AgentWallet(wallet).initialise(
agentId,
agentOwner,
policyEngine,
usdcToken
);

// ── Grant wallet ENFORCER_ROLE on PolicyEngine ────────────
// This is the access control that means ONLY this wallet
// can call policyEngine.enforce(). No other address can.
AccessControl(policyEngine).grantRole(
keccak256("ENFORCER_ROLE"),
wallet
);

// ── Register in our local registry ───────────────────────
wallets[agentId] = wallet;

emit AgentCreated(agentId, wallet, agentOwner);
}

// ─── Predict address ─────────────────────────────────────────

/// @notice Returns the address where an AgentWallet WOULD be deployed
/// for these inputs, without actually deploying it.
/// Useful for pre-funding a wallet before it exists.
function predictAddress(
address agentOwner,
bytes32 salt
) external view returns (address) {
bytes32 agentId = keccak256(
abi.encodePacked(agentOwner, salt, block.chainid)
);
bytes32 create2Salt = keccak256(abi.encodePacked(agentId));

return address(uint160(uint256(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
create2Salt,
keccak256(type(AgentWallet).creationCode)
)))));
}

/// @notice Returns true if an agent exists for this agentId
function agentExists(bytes32 agentId) external view returns (bool) {
return wallets[agentId] != address(0);
}

// ─── Internal: CREATE2 deploy ─────────────────────────────────
function _deploy(bytes32 salt) internal returns (address addr) {
bytes memory bytecode = type(AgentWallet).creationCode;
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
}
}
Loading
Loading