@agenticprimitives/contracts 1.0.0-alpha.11 → 1.0.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT.md +2 -1
- package/deployments-base-sepolia.json +4 -3
- package/dist/abi/AgentRegistryBase.json +588 -0
- package/dist/abi/IRegistryMembershipPolicy.json +70 -0
- package/dist/abi/PaymentEnforcer.json +30 -1
- package/dist/abi/PaymentEscrow.json +25 -0
- package/dist/abi/index.d.ts +2 -0
- package/dist/abi/index.js +2 -0
- package/dist/deployments/base-sepolia.d.ts +1 -0
- package/dist/deployments/base-sepolia.js +4 -3
- package/package.json +1 -1
- package/src/enforcers/PaymentEnforcer.sol +25 -3
- package/src/payments/PaymentEscrow.sol +47 -20
- package/src/registry/AgentRegistryBase.sol +281 -0
- package/src/registry/IRegistryMembershipPolicy.sol +30 -0
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
{
|
|
3
3
|
"type": "constructor",
|
|
4
4
|
"inputs": [
|
|
5
|
+
{
|
|
6
|
+
"name": "delegationManager_",
|
|
7
|
+
"type": "address",
|
|
8
|
+
"internalType": "address"
|
|
9
|
+
},
|
|
5
10
|
{
|
|
6
11
|
"name": "receiptRegistry_",
|
|
7
12
|
"type": "address",
|
|
@@ -56,7 +61,7 @@
|
|
|
56
61
|
}
|
|
57
62
|
],
|
|
58
63
|
"outputs": [],
|
|
59
|
-
"stateMutability": "
|
|
64
|
+
"stateMutability": "view"
|
|
60
65
|
},
|
|
61
66
|
{
|
|
62
67
|
"type": "function",
|
|
@@ -106,6 +111,19 @@
|
|
|
106
111
|
"outputs": [],
|
|
107
112
|
"stateMutability": "nonpayable"
|
|
108
113
|
},
|
|
114
|
+
{
|
|
115
|
+
"type": "function",
|
|
116
|
+
"name": "delegationManager",
|
|
117
|
+
"inputs": [],
|
|
118
|
+
"outputs": [
|
|
119
|
+
{
|
|
120
|
+
"name": "",
|
|
121
|
+
"type": "address",
|
|
122
|
+
"internalType": "address"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"stateMutability": "view"
|
|
126
|
+
},
|
|
109
127
|
{
|
|
110
128
|
"type": "function",
|
|
111
129
|
"name": "encodeTerms",
|
|
@@ -378,6 +396,17 @@
|
|
|
378
396
|
}
|
|
379
397
|
]
|
|
380
398
|
},
|
|
399
|
+
{
|
|
400
|
+
"type": "error",
|
|
401
|
+
"name": "NotDelegationManager",
|
|
402
|
+
"inputs": [
|
|
403
|
+
{
|
|
404
|
+
"name": "caller",
|
|
405
|
+
"type": "address",
|
|
406
|
+
"internalType": "address"
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
},
|
|
381
410
|
{
|
|
382
411
|
"type": "error",
|
|
383
412
|
"name": "NotTransferCall",
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
"type": "function",
|
|
47
47
|
"name": "getHold",
|
|
48
48
|
"inputs": [
|
|
49
|
+
{
|
|
50
|
+
"name": "payer",
|
|
51
|
+
"type": "address",
|
|
52
|
+
"internalType": "address"
|
|
53
|
+
},
|
|
49
54
|
{
|
|
50
55
|
"name": "orderHash",
|
|
51
56
|
"type": "bytes32",
|
|
@@ -120,6 +125,11 @@
|
|
|
120
125
|
"type": "function",
|
|
121
126
|
"name": "refund",
|
|
122
127
|
"inputs": [
|
|
128
|
+
{
|
|
129
|
+
"name": "payer",
|
|
130
|
+
"type": "address",
|
|
131
|
+
"internalType": "address"
|
|
132
|
+
},
|
|
123
133
|
{
|
|
124
134
|
"name": "orderHash",
|
|
125
135
|
"type": "bytes32",
|
|
@@ -133,6 +143,11 @@
|
|
|
133
143
|
"type": "function",
|
|
134
144
|
"name": "release",
|
|
135
145
|
"inputs": [
|
|
146
|
+
{
|
|
147
|
+
"name": "payer",
|
|
148
|
+
"type": "address",
|
|
149
|
+
"internalType": "address"
|
|
150
|
+
},
|
|
136
151
|
{
|
|
137
152
|
"name": "orderHash",
|
|
138
153
|
"type": "bytes32",
|
|
@@ -146,6 +161,11 @@
|
|
|
146
161
|
"type": "function",
|
|
147
162
|
"name": "statusOf",
|
|
148
163
|
"inputs": [
|
|
164
|
+
{
|
|
165
|
+
"name": "payer",
|
|
166
|
+
"type": "address",
|
|
167
|
+
"internalType": "address"
|
|
168
|
+
},
|
|
149
169
|
{
|
|
150
170
|
"name": "orderHash",
|
|
151
171
|
"type": "bytes32",
|
|
@@ -240,6 +260,11 @@
|
|
|
240
260
|
"name": "BadExpiry",
|
|
241
261
|
"inputs": []
|
|
242
262
|
},
|
|
263
|
+
{
|
|
264
|
+
"type": "error",
|
|
265
|
+
"name": "Expired",
|
|
266
|
+
"inputs": []
|
|
267
|
+
},
|
|
243
268
|
{
|
|
244
269
|
"type": "error",
|
|
245
270
|
"name": "FailedInnerCall",
|
package/dist/abi/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare const AgentNameRegistry: Abi;
|
|
|
10
10
|
export declare const AgentNameUniversalResolver: Abi;
|
|
11
11
|
export declare const AgentProfilePredicates: Abi;
|
|
12
12
|
export declare const AgentProfileResolver: Abi;
|
|
13
|
+
export declare const AgentRegistryBase: Abi;
|
|
13
14
|
export declare const AgentRelationship: Abi;
|
|
14
15
|
export declare const AgentRelationshipPredicates: Abi;
|
|
15
16
|
export declare const AgreementRegistry: Abi;
|
|
@@ -41,6 +42,7 @@ export declare const IERC7579ModuleLifecycle: Abi;
|
|
|
41
42
|
export declare const IERC7579ModuleLike: Abi;
|
|
42
43
|
export declare const IGovernanceView: Abi;
|
|
43
44
|
export declare const IPaymentReceiptRegistry: Abi;
|
|
45
|
+
export declare const IRegistryMembershipPolicy: Abi;
|
|
44
46
|
export declare const MockUSDC: Abi;
|
|
45
47
|
export declare const MultiSendCallOnly: Abi;
|
|
46
48
|
export declare const MultiSendCallOnlyHarness: Abi;
|
package/dist/abi/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { default as AgentNameRegistry } from './AgentNameRegistry.json' with { t
|
|
|
10
10
|
export { default as AgentNameUniversalResolver } from './AgentNameUniversalResolver.json' with { type: 'json' };
|
|
11
11
|
export { default as AgentProfilePredicates } from './AgentProfilePredicates.json' with { type: 'json' };
|
|
12
12
|
export { default as AgentProfileResolver } from './AgentProfileResolver.json' with { type: 'json' };
|
|
13
|
+
export { default as AgentRegistryBase } from './AgentRegistryBase.json' with { type: 'json' };
|
|
13
14
|
export { default as AgentRelationship } from './AgentRelationship.json' with { type: 'json' };
|
|
14
15
|
export { default as AgentRelationshipPredicates } from './AgentRelationshipPredicates.json' with { type: 'json' };
|
|
15
16
|
export { default as AgreementRegistry } from './AgreementRegistry.json' with { type: 'json' };
|
|
@@ -41,6 +42,7 @@ export { default as IERC7579ModuleLifecycle } from './IERC7579ModuleLifecycle.js
|
|
|
41
42
|
export { default as IERC7579ModuleLike } from './IERC7579ModuleLike.json' with { type: 'json' };
|
|
42
43
|
export { default as IGovernanceView } from './IGovernanceView.json' with { type: 'json' };
|
|
43
44
|
export { default as IPaymentReceiptRegistry } from './IPaymentReceiptRegistry.json' with { type: 'json' };
|
|
45
|
+
export { default as IRegistryMembershipPolicy } from './IRegistryMembershipPolicy.json' with { type: 'json' };
|
|
44
46
|
export { default as MockUSDC } from './MockUSDC.json' with { type: 'json' };
|
|
45
47
|
export { default as MultiSendCallOnly } from './MultiSendCallOnly.json' with { type: 'json' };
|
|
46
48
|
export { default as MultiSendCallOnlyHarness } from './MultiSendCallOnlyHarness.json' with { type: 'json' };
|
|
@@ -8,6 +8,7 @@ export interface Deployments {
|
|
|
8
8
|
readonly "agentNameResolver": Address;
|
|
9
9
|
readonly "agentNameUniversalResolver": Address;
|
|
10
10
|
readonly "agentProfileResolver": Address;
|
|
11
|
+
readonly "agentRegistryBase": Address;
|
|
11
12
|
readonly "agentRelationship": Address;
|
|
12
13
|
readonly "agreementRegistry": Address;
|
|
13
14
|
readonly "allowedMethodsEnforcer": Address;
|
|
@@ -7,6 +7,7 @@ export const CONTRACTS = {
|
|
|
7
7
|
"agentNameResolver": "0xcC9855A23C7a4D4592E68Fe47E195C2f9D832473",
|
|
8
8
|
"agentNameUniversalResolver": "0x7d777d2d0bbc1806B9Cc779121C27fbaAaFDb60b",
|
|
9
9
|
"agentProfileResolver": "0xc448EB159B2a35F07f6E1814CC8d125244d7384B",
|
|
10
|
+
"agentRegistryBase": "0xe320947b18D3C71710DCB75D4bf200147ded18Df",
|
|
10
11
|
"agentRelationship": "0xE6B8F6C7F1c9857013dA356DF08D0384334A9607",
|
|
11
12
|
"agreementRegistry": "0x7f4d6A74eda1D6ce251D371566f1c6639F9B0DDa",
|
|
12
13
|
"allowedMethodsEnforcer": "0xf77cf06aCd8b9e2f3204fe4bE16449B3594c9633",
|
|
@@ -31,9 +32,9 @@ export const CONTRACTS = {
|
|
|
31
32
|
"timestampEnforcer": "0x7c452434314A5bf2048f9263aeCd0cC20Fa07965",
|
|
32
33
|
"universalSignatureValidator": "0xe8F3e531429F44D77dC3A331C18032bfa1550D84",
|
|
33
34
|
"valueEnforcer": "0x7c4533e0A9fB0B1cb5aa70327b222a80d72d52db",
|
|
34
|
-
"paymentEnforcer": "
|
|
35
|
-
"paymentReceiptRegistry": "
|
|
35
|
+
"paymentEnforcer": "0xCB5D2dbfc11e146d568764471516C9DB734D2c60",
|
|
36
|
+
"paymentReceiptRegistry": "0x391Ed4D7538f80a221F910539005bB00b996Ff9F",
|
|
36
37
|
"mockUsdc": "0x8fb56ff3C13347DFC4E1287aE83E88deE5a7211C",
|
|
37
|
-
"paymentEscrow": "
|
|
38
|
+
"paymentEscrow": "0xC85EB1831e9180F5e2cA7854f362f6C9a6D727EA"
|
|
38
39
|
};
|
|
39
40
|
export default CONTRACTS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticprimitives/contracts",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.13",
|
|
4
4
|
"description": "Solidity contracts + ABIs + flattened sources + per-network deployment addresses for the agenticprimitives stack.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,6 +58,14 @@ contract PaymentEnforcer is ICaveatEnforcer {
|
|
|
58
58
|
/// delegator => delegationHash => nonce => consumed (on-chain replay guard)
|
|
59
59
|
mapping(address => mapping(bytes32 => mapping(bytes32 => bool))) private _nonceUsed;
|
|
60
60
|
|
|
61
|
+
/// @notice The ONLY authorized caller of the hooks (EXT-PMT-1 / C-NEW-1). Unlike the stateless
|
|
62
|
+
/// enforcers (timestamp/value/targets/methods), this enforcer MUTATES state (budget, nonce)
|
|
63
|
+
/// and writes a settlement receipt — so it is NOT safe to call externally. A direct caller
|
|
64
|
+
/// that is not the DelegationManager could otherwise forge a `PaymentSettled` receipt and
|
|
65
|
+
/// grief the victim's budget/nonce with no actual transfer. Bound immutably at deploy; the
|
|
66
|
+
/// hooks revert `NotDelegationManager` for any other `msg.sender`.
|
|
67
|
+
address public immutable delegationManager;
|
|
68
|
+
|
|
61
69
|
/// @notice Trustless receipt sink (PAY-CON-4). Immutable; `address(0)` disables registry receipts
|
|
62
70
|
/// (the `PaymentCharged` event below is always the primary on-chain receipt).
|
|
63
71
|
IPaymentReceiptRegistry public immutable receiptRegistry;
|
|
@@ -84,8 +92,21 @@ contract PaymentEnforcer is ICaveatEnforcer {
|
|
|
84
92
|
error AggregateExceeded(uint256 spent, uint256 maxAggregate, uint256 amount);
|
|
85
93
|
error FrequencyExceeded(uint32 callsInWindow, uint32 maxRedemptionsPerWindow);
|
|
86
94
|
error NonceReused(bytes32 nonce);
|
|
95
|
+
error NotDelegationManager(address caller);
|
|
96
|
+
|
|
97
|
+
/// @dev The hooks are state-mutating + receipt-emitting, so ONLY the DelegationManager may invoke
|
|
98
|
+
/// them (EXT-PMT-1). `_runBeforeHooks`/`_runAfterHooks` call the enforcer directly, so during a
|
|
99
|
+
/// real `redeemDelegation` msg.sender IS the DelegationManager. The `verifyAuthorization` view
|
|
100
|
+
/// path staticcalls the hook from the same DelegationManager and still reverts on the SSTORE
|
|
101
|
+
/// under staticcall (yields `false`), exactly as before — the modifier does not change that.
|
|
102
|
+
modifier onlyDelegationManager() {
|
|
103
|
+
if (msg.sender != delegationManager) revert NotDelegationManager(msg.sender);
|
|
104
|
+
_;
|
|
105
|
+
}
|
|
87
106
|
|
|
88
|
-
constructor(address receiptRegistry_) {
|
|
107
|
+
constructor(address delegationManager_, address receiptRegistry_) {
|
|
108
|
+
require(delegationManager_ != address(0), "PaymentEnforcer: zero DelegationManager");
|
|
109
|
+
delegationManager = delegationManager_;
|
|
89
110
|
receiptRegistry = IPaymentReceiptRegistry(receiptRegistry_);
|
|
90
111
|
}
|
|
91
112
|
|
|
@@ -98,7 +119,7 @@ contract PaymentEnforcer is ICaveatEnforcer {
|
|
|
98
119
|
address target,
|
|
99
120
|
uint256 value,
|
|
100
121
|
bytes calldata callData
|
|
101
|
-
) external override {
|
|
122
|
+
) external override onlyDelegationManager {
|
|
102
123
|
// ── terms (immutable, set at delegation creation) ──
|
|
103
124
|
if (terms.length != 192) revert InvalidTerms();
|
|
104
125
|
(
|
|
@@ -161,8 +182,9 @@ contract PaymentEnforcer is ICaveatEnforcer {
|
|
|
161
182
|
|
|
162
183
|
function afterHook(bytes calldata, bytes calldata, bytes32, address, address, address, uint256, bytes calldata)
|
|
163
184
|
external
|
|
164
|
-
|
|
185
|
+
view
|
|
165
186
|
override
|
|
187
|
+
onlyDelegationManager
|
|
166
188
|
{}
|
|
167
189
|
|
|
168
190
|
// ── views (off-chain budget display + idempotency checks) ──
|
|
@@ -42,7 +42,10 @@ contract PaymentEscrow is ReentrancyGuard {
|
|
|
42
42
|
Status status;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// EXT-PMT-3: holds are keyed by (payer, orderHash), NOT orderHash alone — so a stranger cannot
|
|
46
|
+
// squat a predictable orderHash to block the real payer's deposit (HoldExists griefing). Each payer
|
|
47
|
+
// has an independent namespace for any given orderHash.
|
|
48
|
+
mapping(bytes32 holdKey => Hold) private _holds;
|
|
46
49
|
|
|
47
50
|
event EscrowDeposited(bytes32 indexed orderHash);
|
|
48
51
|
event EscrowReleased(bytes32 indexed orderHash);
|
|
@@ -57,6 +60,13 @@ contract PaymentEscrow is ReentrancyGuard {
|
|
|
57
60
|
error BadExpiry();
|
|
58
61
|
error NotAuthorized();
|
|
59
62
|
error NotYetExpired();
|
|
63
|
+
error Expired();
|
|
64
|
+
|
|
65
|
+
/// @dev The (payer, orderHash) namespace key. Terminal calls by the payee/releaser pass `payer`;
|
|
66
|
+
/// the payer's own `reclaim`/`deposit` derive it from `msg.sender`.
|
|
67
|
+
function _key(address payer, bytes32 orderHash) private pure returns (bytes32) {
|
|
68
|
+
return keccak256(abi.encode(payer, orderHash));
|
|
69
|
+
}
|
|
60
70
|
|
|
61
71
|
/**
|
|
62
72
|
* @notice Fund a hold for `orderHash`. Pulls `amount` of `asset` from the caller (the payer).
|
|
@@ -72,15 +82,30 @@ contract PaymentEscrow is ReentrancyGuard {
|
|
|
72
82
|
address releaser,
|
|
73
83
|
uint64 expiry
|
|
74
84
|
) external nonReentrant {
|
|
75
|
-
|
|
85
|
+
bytes32 key = _key(msg.sender, orderHash);
|
|
86
|
+
if (_holds[key].status != Status.None) revert HoldExists();
|
|
76
87
|
if (amount == 0) revert ZeroAmount();
|
|
77
88
|
if (asset == address(0) || payee == address(0)) revert ZeroAddress();
|
|
78
89
|
if (expiry <= block.timestamp) revert BadExpiry();
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
// EXT-PMT-3: credit the REALIZED amount received (balance delta), not the requested `amount`. A
|
|
92
|
+
// fee-on-transfer / rebasing asset delivers less than `amount`, which would otherwise over-credit
|
|
93
|
+
// the hold and let it draw from other holds' funds in the shared pool (cross-hold drain). The
|
|
94
|
+
// external transfer happens before the storage write, but nonReentrant + the status check above
|
|
95
|
+
// keep it safe (a reentrant deposit is blocked; a duplicate (payer,orderHash) reverts HoldExists).
|
|
96
|
+
uint256 beforeBal = IERC20(asset).balanceOf(address(this));
|
|
97
|
+
// Triaged false positive: `deposit` is `nonReentrant`, so the balanceOf delta cannot be
|
|
98
|
+
// manipulated by re-entering between the two reads — the guard blocks any callback. The
|
|
99
|
+
// before/after balance read is the EXT-PMT-3 realized-amount fix for fee-on-transfer assets.
|
|
100
|
+
// slither-disable-next-line reentrancy-balance
|
|
101
|
+
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
|
|
102
|
+
uint256 received = IERC20(asset).balanceOf(address(this)) - beforeBal;
|
|
103
|
+
if (received == 0) revert ZeroAmount();
|
|
104
|
+
|
|
105
|
+
_holds[key] = Hold({
|
|
81
106
|
payer: msg.sender,
|
|
82
107
|
asset: asset,
|
|
83
|
-
amount:
|
|
108
|
+
amount: received,
|
|
84
109
|
payee: payee,
|
|
85
110
|
refundTo: refundTo == address(0) ? msg.sender : refundTo,
|
|
86
111
|
releaser: releaser,
|
|
@@ -88,49 +113,51 @@ contract PaymentEscrow is ReentrancyGuard {
|
|
|
88
113
|
status: Status.Held
|
|
89
114
|
});
|
|
90
115
|
emit EscrowDeposited(orderHash);
|
|
91
|
-
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
/// @notice Capture a held order to the payee. Authorized: the payee, or the configured releaser.
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
/// `payer` namespaces the hold (EXT-PMT-3). Cannot release at/after expiry — that window is
|
|
120
|
+
/// the payer's `reclaim` (removes the prior release/reclaim race; spec 243 §reclaim intent).
|
|
121
|
+
function release(address payer, bytes32 orderHash) external nonReentrant {
|
|
122
|
+
Hold storage h = _held(_key(payer, orderHash));
|
|
97
123
|
if (msg.sender != h.payee && (h.releaser == address(0) || msg.sender != h.releaser)) revert NotAuthorized();
|
|
124
|
+
if (block.timestamp >= h.expiry) revert Expired();
|
|
98
125
|
h.status = Status.Captured;
|
|
99
126
|
emit EscrowReleased(orderHash);
|
|
100
127
|
IERC20(h.asset).safeTransfer(h.payee, h.amount);
|
|
101
128
|
}
|
|
102
129
|
|
|
103
|
-
/// @notice Payee-consented refund of a held order to the payer (before capture).
|
|
104
|
-
function refund(bytes32 orderHash) external nonReentrant {
|
|
105
|
-
Hold storage h = _held(orderHash);
|
|
130
|
+
/// @notice Payee-consented refund of a held order to the payer (before capture). `payer` namespaces.
|
|
131
|
+
function refund(address payer, bytes32 orderHash) external nonReentrant {
|
|
132
|
+
Hold storage h = _held(_key(payer, orderHash));
|
|
106
133
|
if (msg.sender != h.payee) revert NotAuthorized();
|
|
107
134
|
h.status = Status.Refunded;
|
|
108
135
|
emit EscrowRefunded(orderHash);
|
|
109
136
|
IERC20(h.asset).safeTransfer(h.refundTo, h.amount);
|
|
110
137
|
}
|
|
111
138
|
|
|
112
|
-
/// @notice Payer reclaims a held order after expiry (never released).
|
|
139
|
+
/// @notice Payer reclaims a held order after expiry (never released). Payer-initiated → the hold is
|
|
140
|
+
/// derived from `msg.sender`, so only the payer can reach their own hold.
|
|
113
141
|
function reclaim(bytes32 orderHash) external nonReentrant {
|
|
114
|
-
Hold storage h = _held(orderHash);
|
|
115
|
-
if (msg.sender != h.payer) revert NotAuthorized();
|
|
142
|
+
Hold storage h = _held(_key(msg.sender, orderHash));
|
|
116
143
|
if (block.timestamp < h.expiry) revert NotYetExpired();
|
|
117
144
|
h.status = Status.Reclaimed;
|
|
118
145
|
emit EscrowReclaimed(orderHash);
|
|
119
146
|
IERC20(h.asset).safeTransfer(h.refundTo, h.amount);
|
|
120
147
|
}
|
|
121
148
|
|
|
122
|
-
/// @notice Full hold record (app reads via this view, not log scans
|
|
123
|
-
function getHold(bytes32 orderHash) external view returns (Hold memory) {
|
|
124
|
-
return _holds[orderHash];
|
|
149
|
+
/// @notice Full hold record for (payer, orderHash) — app reads via this view, not log scans (ADR-0012).
|
|
150
|
+
function getHold(address payer, bytes32 orderHash) external view returns (Hold memory) {
|
|
151
|
+
return _holds[_key(payer, orderHash)];
|
|
125
152
|
}
|
|
126
153
|
|
|
127
|
-
function statusOf(bytes32 orderHash) external view returns (Status) {
|
|
128
|
-
return _holds[orderHash].status;
|
|
154
|
+
function statusOf(address payer, bytes32 orderHash) external view returns (Status) {
|
|
155
|
+
return _holds[_key(payer, orderHash)].status;
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
/// @dev Load a hold that MUST be in `Held` (fail-closed): missing → NoHold; terminal → NotHeld.
|
|
132
|
-
function _held(bytes32
|
|
133
|
-
h = _holds[
|
|
159
|
+
function _held(bytes32 key) private view returns (Hold storage h) {
|
|
160
|
+
h = _holds[key];
|
|
134
161
|
if (h.status == Status.None) revert NoHold();
|
|
135
162
|
if (h.status != Status.Held) revert NotHeld();
|
|
136
163
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import {IRegistryMembershipPolicy} from "./IRegistryMembershipPolicy.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title AgentRegistryBase
|
|
8
|
+
* @notice The generic, SA-anchored registry base that vertical registries are built FROM (spec 279 §4,
|
|
9
|
+
* ADR-0038 "many-registries"). It is a MEMBERSHIP LEDGER of facets — entries that point AT a
|
|
10
|
+
* Smart Agent address — not an account. It holds no funds, signs nothing, executes nothing, and
|
|
11
|
+
* grants NO authority to act: an entry is evidence ("this SA is a member of registry R, here is
|
|
12
|
+
* its card hash + binding-proof hash + claims"), never a delegation. Authority lives in the
|
|
13
|
+
* AgentAccount + DelegationManager; this contract only records and gates membership.
|
|
14
|
+
*
|
|
15
|
+
* ─── How it differs from AgentAccount ───
|
|
16
|
+
* AgentAccount IS the agent (ERC-4337/7579 custody + execution + ERC-1271). AgentRegistryBase is
|
|
17
|
+
* a directory of MANY entries pointing at MANY agents, one instance per registry. The subject SA
|
|
18
|
+
* controls its own entry because it must be the caller (RB-01); the registry's controller / policy
|
|
19
|
+
* hook gates admission + ejection (RB-02/03). The binding proof referenced by `bindingProofHash`
|
|
20
|
+
* is verified OFF-CHAIN by a verifier (registry-kit `verifyEntryBindingProof`) against the
|
|
21
|
+
* subject SA's ERC-1271 — this contract stores the hash as a pointer, it does not re-verify it.
|
|
22
|
+
*
|
|
23
|
+
* ─── Key invariants (RB-*) ───
|
|
24
|
+
* RB-01 entry control follows the subject SA custody policy — register / renew / self-revoke /
|
|
25
|
+
* self-update require `msg.sender == subjectAgent` (so the SA's own custody produced the
|
|
26
|
+
* call); the contract never re-implements custody.
|
|
27
|
+
* RB-02 admission is gated by the registry's pluggable membership hook (or open if unset); a
|
|
28
|
+
* policy returning false OR reverting fails closed (ADR-0013) — no second path.
|
|
29
|
+
* RB-03 suspension / revocation / claim-update by a non-subject require the registry controller
|
|
30
|
+
* OR a policy-authorized actor (`canAdminister`); never an arbitrary caller.
|
|
31
|
+
* RB-04 revocation is terminal; a revoked entry cannot be renewed, re-registered, suspended, or
|
|
32
|
+
* claim-updated.
|
|
33
|
+
* RB-05 NO external-registry-protocol fields in storage (no ENS/8004/HCS/OASF) — those are
|
|
34
|
+
* external projections (ADR-0037), not AP state.
|
|
35
|
+
* RB-06 reads are storage views only — discovery never scans chain logs (ADR-0012); the indexer
|
|
36
|
+
* consumes the events below.
|
|
37
|
+
* RB-07 epoch-bucket registration timestamps (raw block.timestamp never stored); `expiresAt` is a
|
|
38
|
+
* caller-supplied absolute unix-seconds bound compared against block.timestamp for liveness.
|
|
39
|
+
*
|
|
40
|
+
* Spec 279 §4 + ADR-0010 / 0012 / 0013 / 0037 / 0038.
|
|
41
|
+
*/
|
|
42
|
+
contract AgentRegistryBase {
|
|
43
|
+
// ─── Constants ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/// @dev RB-07 — registration time is bucketed (provenance, not a precise clock). Mirrors AttestationRegistry.
|
|
46
|
+
uint64 public constant EPOCH_SECONDS = 3600;
|
|
47
|
+
|
|
48
|
+
// ─── Types ──────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/// @dev Stored status. `Expired` is NOT stored (no keeper to flip it) — it is DERIVED from `expiresAt`
|
|
51
|
+
/// by {isActive}; storage only tracks the explicit lifecycle transitions.
|
|
52
|
+
enum EntryStatus {
|
|
53
|
+
None,
|
|
54
|
+
Active,
|
|
55
|
+
Suspended,
|
|
56
|
+
Revoked
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
struct Registry {
|
|
60
|
+
address controller; // the governing SA (set on create); address(0) iff !exists
|
|
61
|
+
address policy; // pluggable membership hook; address(0) = open registry (subject self-service only)
|
|
62
|
+
bool exists;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
struct Entry {
|
|
66
|
+
address subjectAgent; // the canonical SA this entry is a facet of
|
|
67
|
+
bytes32 cardHash; // sha256 of the agent-profile signed card bundle
|
|
68
|
+
bytes32 bindingProofHash; // sha256 of the off-chain binding proof (verified off-chain, not here)
|
|
69
|
+
bytes32 claimsRoot; // keccak of the registration-time claimHashes[] (the set the binding proof signed)
|
|
70
|
+
EntryStatus status;
|
|
71
|
+
uint64 registeredAtBucket; // block.timestamp / EPOCH_SECONDS at registration
|
|
72
|
+
uint64 expiresAt; // absolute unix seconds; 0 = non-expiring
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Storage ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
mapping(bytes32 => Registry) private _registries; // registryId => Registry
|
|
78
|
+
mapping(bytes32 => Entry) private _entries; // entryKey => Entry
|
|
79
|
+
mapping(bytes32 => mapping(bytes32 => bytes32)) private _claims; // entryKey => claimSlotId => claimHash
|
|
80
|
+
|
|
81
|
+
// ─── Errors ─────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
error RegistryExists();
|
|
84
|
+
error RegistryNotFound();
|
|
85
|
+
error NotController();
|
|
86
|
+
error EntryExists();
|
|
87
|
+
error EntryNotFound();
|
|
88
|
+
error ZeroSubject();
|
|
89
|
+
error SubjectMismatch(); // msg.sender is not the entry's subject SA (RB-01)
|
|
90
|
+
error NotAuthorized(); // msg.sender is neither controller nor a policy-authorized admin (RB-03)
|
|
91
|
+
error NotActive();
|
|
92
|
+
error AlreadyRevoked();
|
|
93
|
+
error InvalidExpiry();
|
|
94
|
+
error MembershipDenied(); // RB-02 fail-closed admission denial
|
|
95
|
+
|
|
96
|
+
// ─── Events ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
// Registry provisioning (not part of the registry-kit lifecycle ABI).
|
|
99
|
+
event RegistryCreated(bytes32 indexed registryId, address indexed controller, address policy);
|
|
100
|
+
event RegistryPolicyUpdated(bytes32 indexed registryId, address policy);
|
|
101
|
+
|
|
102
|
+
// Entry lifecycle — MUST match registry-kit `AGENT_REGISTRY_BASE_ABI` + spec 279 §4.1 EXACTLY
|
|
103
|
+
// (the SDK encodes calls against / decodes logs from these signatures).
|
|
104
|
+
event RegistryEntryRegistered(
|
|
105
|
+
bytes32 indexed registryId,
|
|
106
|
+
bytes32 indexed entryId,
|
|
107
|
+
address indexed subjectAgent,
|
|
108
|
+
bytes32 cardHash,
|
|
109
|
+
bytes32 bindingProofHash
|
|
110
|
+
);
|
|
111
|
+
event RegistryEntryRenewed(bytes32 indexed registryId, bytes32 indexed entryId, uint64 expiresAt);
|
|
112
|
+
event RegistryEntrySuspended(bytes32 indexed registryId, bytes32 indexed entryId, bytes32 reasonHash);
|
|
113
|
+
event RegistryEntryRevoked(bytes32 indexed registryId, bytes32 indexed entryId, bytes32 reasonHash);
|
|
114
|
+
event RegistryClaimUpdated(
|
|
115
|
+
bytes32 indexed registryId, bytes32 indexed entryId, bytes32 indexed claimSlotId, bytes32 claimHash
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// ─── Registry provisioning ──────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/// @notice Create a registry. Permissionless + first-create-wins: `msg.sender` becomes the controller.
|
|
121
|
+
/// @dev `registryId` is `keccak256(urn:ap:registry:…)` off-chain; squatting on an id is a PROVISIONING
|
|
122
|
+
/// concern (create the registry in the same flow that mints its urn), out of scope for this base.
|
|
123
|
+
/// `policy == address(0)` ⇒ open registry: any SA may self-register, no admission gate.
|
|
124
|
+
function createRegistry(bytes32 registryId, address policy) external {
|
|
125
|
+
if (_registries[registryId].exists) revert RegistryExists();
|
|
126
|
+
_registries[registryId] = Registry({controller: msg.sender, policy: policy, exists: true});
|
|
127
|
+
emit RegistryCreated(registryId, msg.sender, policy);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// @notice Swap the pluggable membership hook. Controller only.
|
|
131
|
+
function setRegistryPolicy(bytes32 registryId, address policy) external {
|
|
132
|
+
Registry storage r = _registries[registryId];
|
|
133
|
+
if (!r.exists) revert RegistryNotFound();
|
|
134
|
+
if (msg.sender != r.controller) revert NotController();
|
|
135
|
+
r.policy = policy;
|
|
136
|
+
emit RegistryPolicyUpdated(registryId, policy);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Entry lifecycle ────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/// @notice Register a facet of `subjectAgent`. RB-01: the subject SA must be the caller. RB-02: the
|
|
142
|
+
/// registry's membership hook (if any) must admit. Signature ABI matches registry-kit.
|
|
143
|
+
function registerEntry(
|
|
144
|
+
bytes32 registryId,
|
|
145
|
+
bytes32 entryId,
|
|
146
|
+
address subjectAgent,
|
|
147
|
+
bytes32 cardHash,
|
|
148
|
+
bytes32 bindingProofHash,
|
|
149
|
+
bytes32[] calldata claimHashes,
|
|
150
|
+
uint64 expiresAt
|
|
151
|
+
) external {
|
|
152
|
+
Registry storage r = _registries[registryId];
|
|
153
|
+
if (!r.exists) revert RegistryNotFound();
|
|
154
|
+
if (subjectAgent == address(0)) revert ZeroSubject();
|
|
155
|
+
if (msg.sender != subjectAgent) revert SubjectMismatch(); // RB-01
|
|
156
|
+
if (expiresAt != 0 && expiresAt <= block.timestamp) revert InvalidExpiry();
|
|
157
|
+
|
|
158
|
+
bytes32 key = _entryKey(registryId, entryId);
|
|
159
|
+
if (_entries[key].status != EntryStatus.None) revert EntryExists();
|
|
160
|
+
|
|
161
|
+
// RB-02 — admission gate, fail-closed: a false return OR a revert blocks registration.
|
|
162
|
+
address policy = r.policy;
|
|
163
|
+
if (policy != address(0)) {
|
|
164
|
+
try IRegistryMembershipPolicy(policy)
|
|
165
|
+
.canRegister(registryId, entryId, subjectAgent, cardHash, claimHashes) returns (
|
|
166
|
+
bool ok
|
|
167
|
+
) {
|
|
168
|
+
if (!ok) revert MembershipDenied();
|
|
169
|
+
} catch {
|
|
170
|
+
revert MembershipDenied();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_entries[key] = Entry({
|
|
175
|
+
subjectAgent: subjectAgent,
|
|
176
|
+
cardHash: cardHash,
|
|
177
|
+
bindingProofHash: bindingProofHash,
|
|
178
|
+
claimsRoot: keccak256(abi.encode(claimHashes)),
|
|
179
|
+
status: EntryStatus.Active,
|
|
180
|
+
registeredAtBucket: uint64(block.timestamp) / EPOCH_SECONDS,
|
|
181
|
+
expiresAt: expiresAt
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
emit RegistryEntryRegistered(registryId, entryId, subjectAgent, cardHash, bindingProofHash);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// @notice Extend an active entry's expiry. RB-01: subject SA only. RB-04: a revoked/suspended entry
|
|
188
|
+
/// cannot be renewed.
|
|
189
|
+
function renewEntry(bytes32 registryId, bytes32 entryId, uint64 expiresAt) external {
|
|
190
|
+
Entry storage e = _entries[_entryKey(registryId, entryId)];
|
|
191
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
192
|
+
if (msg.sender != e.subjectAgent) revert SubjectMismatch(); // RB-01
|
|
193
|
+
if (e.status != EntryStatus.Active) revert NotActive(); // RB-04 (Suspended/Revoked)
|
|
194
|
+
if (expiresAt != 0 && expiresAt <= block.timestamp) revert InvalidExpiry();
|
|
195
|
+
e.expiresAt = expiresAt;
|
|
196
|
+
emit RegistryEntryRenewed(registryId, entryId, expiresAt);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// @notice Suspend an active entry. RB-03: admin action (controller or policy-authorized) — NOT subject.
|
|
200
|
+
function suspendEntry(bytes32 registryId, bytes32 entryId, bytes32 reasonHash) external {
|
|
201
|
+
Entry storage e = _entries[_entryKey(registryId, entryId)];
|
|
202
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
203
|
+
_requireAdmin(registryId, entryId); // RB-03
|
|
204
|
+
if (e.status != EntryStatus.Active) revert NotActive();
|
|
205
|
+
e.status = EntryStatus.Suspended;
|
|
206
|
+
emit RegistryEntrySuspended(registryId, entryId, reasonHash);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// @notice Revoke an entry (terminal). The subject SA may leave; the controller / policy-admin may eject.
|
|
210
|
+
function revokeEntry(bytes32 registryId, bytes32 entryId, bytes32 reasonHash) external {
|
|
211
|
+
Entry storage e = _entries[_entryKey(registryId, entryId)];
|
|
212
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
213
|
+
if (msg.sender != e.subjectAgent) _requireAdmin(registryId, entryId); // subject (leave) or admin (eject)
|
|
214
|
+
if (e.status == EntryStatus.Revoked) revert AlreadyRevoked(); // RB-04
|
|
215
|
+
e.status = EntryStatus.Revoked;
|
|
216
|
+
emit RegistryEntryRevoked(registryId, entryId, reasonHash);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// @notice Set a per-slot claim hash. Subject self-service OR admin (issuer/registry-managed claims).
|
|
220
|
+
/// @dev Distinct from `claimsRoot` (the immutable snapshot the binding proof signed at registration):
|
|
221
|
+
/// this is the mutable, slot-keyed projection the indexer reads for discovery.
|
|
222
|
+
function updateClaim(bytes32 registryId, bytes32 entryId, bytes32 claimSlotId, bytes32 claimHash) external {
|
|
223
|
+
bytes32 key = _entryKey(registryId, entryId);
|
|
224
|
+
Entry storage e = _entries[key];
|
|
225
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
226
|
+
if (e.status == EntryStatus.Revoked) revert AlreadyRevoked(); // RB-04
|
|
227
|
+
if (msg.sender != e.subjectAgent) _requireAdmin(registryId, entryId);
|
|
228
|
+
_claims[key][claimSlotId] = claimHash;
|
|
229
|
+
emit RegistryClaimUpdated(registryId, entryId, claimSlotId, claimHash);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Views (RB-06: discovery reads use these, never log scans) ──────
|
|
233
|
+
|
|
234
|
+
function getRegistry(bytes32 registryId) external view returns (Registry memory) {
|
|
235
|
+
Registry memory r = _registries[registryId];
|
|
236
|
+
if (!r.exists) revert RegistryNotFound();
|
|
237
|
+
return r;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getEntry(bytes32 registryId, bytes32 entryId) external view returns (Entry memory) {
|
|
241
|
+
Entry memory e = _entries[_entryKey(registryId, entryId)];
|
|
242
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
243
|
+
return e;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// @notice Effective liveness: stored-Active AND not past `expiresAt`. A missing entry returns false
|
|
247
|
+
/// (empty is an answer, ADR-0013 — not a trigger for a fallback lookup).
|
|
248
|
+
function isActive(bytes32 registryId, bytes32 entryId) external view returns (bool) {
|
|
249
|
+
Entry memory e = _entries[_entryKey(registryId, entryId)];
|
|
250
|
+
if (e.status != EntryStatus.Active) return false;
|
|
251
|
+
if (e.expiresAt != 0 && block.timestamp > e.expiresAt) return false;
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// @notice The current hash for a claim slot (0 if unset). Slot semantics are defined by the registry.
|
|
256
|
+
function getClaim(bytes32 registryId, bytes32 entryId, bytes32 claimSlotId) external view returns (bytes32) {
|
|
257
|
+
return _claims[_entryKey(registryId, entryId)][claimSlotId];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Internal ───────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
function _entryKey(bytes32 registryId, bytes32 entryId) internal pure returns (bytes32) {
|
|
263
|
+
return keccak256(abi.encodePacked(registryId, entryId));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// @dev RB-03 — controller passes; otherwise the pluggable policy may authorize the actor. Fail-closed:
|
|
267
|
+
/// no policy, a false return, or a revert all deny.
|
|
268
|
+
function _requireAdmin(bytes32 registryId, bytes32 entryId) internal view {
|
|
269
|
+
Registry storage r = _registries[registryId];
|
|
270
|
+
if (msg.sender == r.controller) return;
|
|
271
|
+
address policy = r.policy;
|
|
272
|
+
if (policy != address(0)) {
|
|
273
|
+
try IRegistryMembershipPolicy(policy).canAdminister(registryId, entryId, msg.sender) returns (bool ok) {
|
|
274
|
+
if (ok) return;
|
|
275
|
+
} catch {
|
|
276
|
+
// fail-closed: treat a reverting policy as denial
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
revert NotAuthorized();
|
|
280
|
+
}
|
|
281
|
+
}
|