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
2 changes: 2 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 2 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 2 additions & 0 deletions apps/examples/basic-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 2 additions & 0 deletions apps/examples/langchain-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 2 additions & 0 deletions apps/examples/mcp-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 2 additions & 0 deletions apps/examples/multi-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 2 additions & 0 deletions apps/facilitator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 2 additions & 0 deletions apps/indexer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
14 changes: 14 additions & 0 deletions packages/contracts/foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"lib/forge-std": {
"tag": {
"name": "v1.16.1",
"rev": "620536fa5277db4e3fd46772d5cbc1ea0696fb43"
}
},
"lib/openzeppelin-contracts": {
"tag": {
"name": "v5.6.1",
"rev": "5fd1781b1454fd1ef8e722282f86f9293cacf256"
}
}
}
3 changes: 2 additions & 1 deletion packages/contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
src = "src"
out = "out"
libs = ["lib"]
remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"]
test = "test"
script = "script"
optimizer = true
Expand All @@ -22,4 +23,4 @@
[rpc_endpoints]
base-sepolia = "${BASE_SEPOLIA_RPC_URL}"
base = "${BASE_RPC_URL}"
localhost = "http://127.0.0.1:8545"
localhost = "http://127.0.0.1:8545"
2 changes: 1 addition & 1 deletion packages/contracts/lib/openzeppelin-contracts
6 changes: 6 additions & 0 deletions packages/contracts/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
278 changes: 234 additions & 44 deletions packages/contracts/src/core/PolicyEngine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,269 @@
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import "../interfaces/IPolicyEngine.sol";
import "../libraries/PolicyLib.sol";

/// @title PolicyEngine
/// @notice On-chain enforcement of agent spending rules.
/// Called by AgentWallet before every payment.
contract PolicyEngine is IPolicyEngine, AccessControl, ReentrancyGuard {
/// @title PolicyEngine
/// @notice Enforces spending policies for agent wallets.
/// AgentWallet must call enforce() before every payment.
contract PolicyEngine is IPolicyEngine, AccessControl {
using PolicyLib for PolicyLib.Policy;
using PolicyLib for PolicyLib.SpendRecord;

bytes32 public constant POLICY_ADMIN_ROLE = keccak256("POLICY_ADMIN_ROLE");
bytes32 public constant POLICY_ADMIN_ROLE =
keccak256("POLICY_ADMIN_ROLE");

/// @dev agentId => Policy
mapping(bytes32 => PolicyLib.Policy) private _policies;

/// @dev agentId => day bucket => spent
mapping(bytes32 => mapping(uint256 => uint256)) private _dailySpend;

/// @dev agentId => hour bucket => spent
mapping(bytes32 => mapping(uint256 => uint256)) private _hourlySpend;
mapping(bytes32 => PolicyLib.Policy)
private _policies;

event PolicySet(bytes32 indexed agentId, address indexed owner);
event PolicyEnforced(bytes32 indexed agentId, uint256 amount, address payee);
event PolicyViolation(bytes32 indexed agentId, string reason);

error PolicyNotFound(bytes32 agentId);
error PolicyViolated(bytes32 agentId, string reason);
error Unauthorized();
/// @dev agentId => SpendRecord
mapping(bytes32 => PolicyLib.SpendRecord)
private _spendRecords;

constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(POLICY_ADMIN_ROLE, admin);
}

/// @notice Register or update a spending policy for an agent
// ============================================================
// WRITE FUNCTIONS
// ============================================================

/// @notice Create or update an agent policy
function setPolicy(
bytes32 agentId,
PolicyLib.Policy calldata policy
) external onlyRole(POLICY_ADMIN_ROLE) {
)
external
onlyRole(POLICY_ADMIN_ROLE)
{
// Basic sanity checks

if (
policy.maxPerTransaction >
policy.maxPerHour
) {
revert InvalidPolicy();
}

if (
policy.maxPerHour >
policy.maxPerDay
) {
revert InvalidPolicy();
}

_policies[agentId] = policy;
emit PolicySet(agentId, msg.sender);

emit PolicySet(
agentId,
msg.sender
);
}

/// @notice Enforce policy before a payment. Reverts on violation.
/// @notice Disable a policy
function revokePolicy(
bytes32 agentId
)
external
onlyRole(POLICY_ADMIN_ROLE)
{
PolicyLib.Policy storage p =
_policies[agentId];

if (
p.maxPerTransaction == 0 &&
!p.active
) {
revert PolicyNotFound(agentId);
}

p.active = false;

emit PolicyRevoked(agentId);
}

/// @notice Enforce policy before payment.
/// Reverts if payment violates rules.
function enforce(
bytes32 agentId,
uint256 amount,
uint128 amount,
address payee
) external nonReentrant returns (bool requiresApproval) {
PolicyLib.Policy storage p = _policies[agentId];
if (p.expiresAt == 0) revert PolicyNotFound(agentId);
if (p.expiresAt != type(uint256).max && block.timestamp > p.expiresAt)
revert PolicyViolated(agentId, "policy expired");
if (amount > p.maxPerTransaction)
revert PolicyViolated(agentId, "exceeds per-tx limit");
)
external
returns (bool requiresApproval)
{
PolicyLib.Policy storage p =
_policies[agentId];

PolicyLib.SpendRecord storage r =
_spendRecords[agentId];

// --------------------------------------------------------
// 1. Policy must exist
// --------------------------------------------------------

if (
p.maxPerTransaction == 0 &&
!p.active
) {
revert PolicyNotFound(agentId);
}

// --------------------------------------------------------
// 2. Policy must be active
// --------------------------------------------------------

if (!p.active) {
revert PolicyInactive(agentId);
}

uint256 dayBucket = block.timestamp / 1 days;
uint256 hourBucket = block.timestamp / 1 hours;
// --------------------------------------------------------
// 3. Policy must not be expired
// --------------------------------------------------------

uint256 newDaily = _dailySpend[agentId][dayBucket] + amount;
uint256 newHourly = _hourlySpend[agentId][hourBucket] + amount;
if (p.isExpired()) {
revert PolicyExpired(agentId);
}

if (newDaily > p.maxPerDay) revert PolicyViolated(agentId, "exceeds daily limit");
if (newHourly > p.maxPerHour) revert PolicyViolated(agentId, "exceeds hourly limit");
// --------------------------------------------------------
// 4. Payee must be allowed
// --------------------------------------------------------

_dailySpend[agentId][dayBucket] = newDaily;
_hourlySpend[agentId][hourBucket] = newHourly;
if (
!p.isPayeeAllowed(payee)
) {
revert PayeeNotAllowed(
agentId,
payee
);
}

requiresApproval = amount >= p.requireApprovalAbove;
emit PolicyEnforced(agentId, amount, payee);
// --------------------------------------------------------
// 5. Per transaction limit
// --------------------------------------------------------

if (
amount >
p.maxPerTransaction
) {
revert ExceedsPerTxLimit(
agentId,
amount,
p.maxPerTransaction
);
}

// --------------------------------------------------------
// 6. Reset spend windows if needed
// --------------------------------------------------------

if (r.isNewHour()) {
r.hourlyAmount = 0;
r.hourBucket =
PolicyLib.currentHourBucket();
}

if (r.isNewDay()) {
r.dailyAmount = 0;
r.dayBucket =
PolicyLib.currentDayBucket();
}

// --------------------------------------------------------
// 7. Hourly limit
// --------------------------------------------------------

uint128 newHourly =
r.hourlyAmount + amount;

if (
newHourly >
p.maxPerHour
) {
revert ExceedsHourlyLimit(
agentId,
newHourly,
p.maxPerHour
);
}

// --------------------------------------------------------
// 8. Daily limit
// --------------------------------------------------------

uint128 newDaily =
r.dailyAmount + amount;

if (
newDaily >
p.maxPerDay
) {
revert ExceedsDailyLimit(
agentId,
newDaily,
p.maxPerDay
);
}

// --------------------------------------------------------
// 9. Commit spend
// --------------------------------------------------------

r.hourlyAmount = newHourly;
r.dailyAmount = newDaily;

// --------------------------------------------------------
// 10. Approval threshold
// --------------------------------------------------------

requiresApproval =
amount >=
p.requireApprovalAbove;

if (requiresApproval) {
emit ApprovalRequired(
agentId,
amount,
payee
);
} else {
emit PaymentEnforced(
agentId,
amount,
payee
);
}
}

function getPolicy(bytes32 agentId) external view returns (PolicyLib.Policy memory) {
// ============================================================
// READ FUNCTIONS
// ============================================================

function getPolicy(
bytes32 agentId
)
external
view
returns (
PolicyLib.Policy memory
)
{
return _policies[agentId];
}
}

function getSpendRecord(
bytes32 agentId
)
external
view
returns (
PolicyLib.SpendRecord memory
)
{
return _spendRecords[agentId];
}
}
Loading
Loading