@agenticprimitives/contracts 1.0.0-alpha.25 → 1.0.0-alpha.27
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 +5 -0
- package/README.md +2 -0
- package/deployments-faithchain.json +2 -1
- package/dist/abi/AgentAccount.json +127 -0
- package/dist/abi/AgentAccountFactory.json +18 -0
- package/dist/abi/ApprovedHashRegistry.json +47 -2
- package/dist/abi/CustodyPolicy.json +35 -0
- package/dist/abi/DigestBindingEnforcer.json +125 -3
- package/dist/abi/EstateProjectionRegistry.json +293 -0
- package/dist/abi/IApprovedHashRegistryWrite.json +35 -0
- package/dist/abi/ICustodyPolicyAccountModuleView.json +31 -0
- package/dist/abi/PayloadClassesEnforcer.json +117 -0
- package/dist/abi/{ContentCorpusRegistry.json → ReceiptAnchorRegistry.json} +61 -82
- package/dist/abi/TreasurySpendPolicy.json +369 -0
- package/dist/abi/index.d.ts +6 -0
- package/dist/abi/index.js +6 -0
- package/dist/deployments/faithchain.d.ts +1 -0
- package/dist/deployments/faithchain.js +1 -0
- package/package.json +16 -5
- package/src/AgentAccount.sol +137 -7
- package/src/AgentAccountFactory.sol +17 -10
- package/src/ApprovedHashRegistry.sol +41 -6
- package/src/IAgentAccount.sol +23 -0
- package/src/custody/CustodyPolicy.sol +72 -2
- package/src/custody/TreasurySpendPolicy.sol +166 -0
- package/src/enforcers/DigestBindingEnforcer.sol +45 -10
- package/src/enforcers/PayloadClassesEnforcer.sol +30 -0
- package/src/provenance/ReceiptAnchorRegistry.sol +55 -0
- package/src/registry/EstateProjectionRegistry.sol +109 -0
- package/deployments-anvil.json +0 -1
- package/dist/abi/AgentSkillClaimRegistry.json +0 -735
- package/dist/abi/IERC1271.json +0 -26
- package/dist/abi/ValidationAttestationRegistry.json +0 -205
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title TreasurySpendPolicy — an aggregate ceiling AT THE RESOURCE (spec 410 §7, THESIS-6)
|
|
6
|
+
* @notice Every caveat enforcer bounds ONE delegation: `ValueEnforcer` a call's value, `PaymentEnforcer` a
|
|
7
|
+
* mandate's budget under (delegator, delegationHash). None of them can see the treasury's other
|
|
8
|
+
* delegates, so two delegates each under their own ceiling can jointly exceed the principal's
|
|
9
|
+
* budget. This module is the ceiling that belongs to the TREASURY: an ERC-7579 HOOK the account
|
|
10
|
+
* installs, consulted on every `execute` / `executeBatch` — a custodian's own userOp and a delegated
|
|
11
|
+
* redemption alike, because the account runs its hooks on both — that adds the value each call moves
|
|
12
|
+
* to the current window and refuses the one that would pass the ceiling.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IT COUNTS. Native value (`value` on the call) when `asset == address(0)`; otherwise ERC-20
|
|
15
|
+
* `transfer(to, amount)` and `transferFrom(from, to, amount)` calldata aimed at `asset`. Anything else
|
|
16
|
+
* moves no value of this asset and is not counted. A batch is one call: its inner calls are summed.
|
|
17
|
+
*
|
|
18
|
+
* WINDOWS are fixed-length epochs from `windowStart` — never sliding — so the check is O(1) and the
|
|
19
|
+
* refusal is reproducible off chain from the receipt and the block time.
|
|
20
|
+
*
|
|
21
|
+
* WHO SETS IT. The budget is the install data: `installModule(4, this, abi.encode(asset, ceiling,
|
|
22
|
+
* windowSeconds))`, which on a custody-governed treasury is a governance selector under its quorum;
|
|
23
|
+
* changing it is an uninstall + reinstall under the same gate. There is no setter a single key could
|
|
24
|
+
* reach. Per-delegation ceilings are untouched: the tighter of the two refuses.
|
|
25
|
+
*
|
|
26
|
+
* WHAT IT IS NOT: a policy of who may spend (that is the mandate), a rate limit on calls, or anything
|
|
27
|
+
* a planner consults — the harness may READ `budget()` to say "this would exceed the treasury's
|
|
28
|
+
* shared budget" before a signature (honesty); the hook is the authority.
|
|
29
|
+
*/
|
|
30
|
+
contract TreasurySpendPolicy {
|
|
31
|
+
struct Budget {
|
|
32
|
+
address asset; // address(0) = native value
|
|
33
|
+
uint256 ceiling; // per window, in the asset's base units
|
|
34
|
+
uint64 windowSeconds; // fixed epochs from windowStart
|
|
35
|
+
uint64 windowStart;
|
|
36
|
+
uint64 windowIndex; // the epoch `spent` belongs to
|
|
37
|
+
uint256 spent;
|
|
38
|
+
bool installed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// account => budget
|
|
42
|
+
mapping(address => Budget) private _budgets;
|
|
43
|
+
|
|
44
|
+
event BudgetInstalled(address indexed account, address indexed asset, uint256 ceiling, uint64 windowSeconds, uint64 windowStart);
|
|
45
|
+
event BudgetUninstalled(address indexed account);
|
|
46
|
+
event Spent(address indexed account, uint64 indexed windowIndex, uint256 amount, uint256 spentInWindow);
|
|
47
|
+
|
|
48
|
+
error BudgetExceeded(uint256 ceiling, uint256 spentInWindow, uint256 amount);
|
|
49
|
+
error NotInstalledOn(address account);
|
|
50
|
+
error AlreadyInstalledOn(address account);
|
|
51
|
+
error BadBudget();
|
|
52
|
+
|
|
53
|
+
bytes4 private constant TRANSFER = bytes4(keccak256("transfer(address,uint256)"));
|
|
54
|
+
bytes4 private constant TRANSFER_FROM = bytes4(keccak256("transferFrom(address,address,uint256)"));
|
|
55
|
+
|
|
56
|
+
// ─── ERC-7579 module surface ───────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function isModuleType(uint256 moduleTypeId) external pure returns (bool) { return moduleTypeId == 4; }
|
|
59
|
+
function moduleId() external pure returns (string memory) { return "agenticprimitives-treasury-spend-policy-1"; }
|
|
60
|
+
|
|
61
|
+
/// @notice Called by the account from `installModule` (governance-gated there): the budget is the install data.
|
|
62
|
+
function onInstall(bytes calldata initData) external {
|
|
63
|
+
Budget storage b = _budgets[msg.sender];
|
|
64
|
+
if (b.installed) revert AlreadyInstalledOn(msg.sender);
|
|
65
|
+
(address asset, uint256 ceiling, uint64 windowSeconds) = abi.decode(initData, (address, uint256, uint64));
|
|
66
|
+
if (windowSeconds == 0) revert BadBudget();
|
|
67
|
+
b.asset = asset;
|
|
68
|
+
b.ceiling = ceiling;
|
|
69
|
+
b.windowSeconds = windowSeconds;
|
|
70
|
+
b.windowStart = uint64(block.timestamp);
|
|
71
|
+
b.windowIndex = 0;
|
|
72
|
+
b.spent = 0;
|
|
73
|
+
b.installed = true;
|
|
74
|
+
emit BudgetInstalled(msg.sender, asset, ceiling, windowSeconds, b.windowStart);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onUninstall(bytes calldata) external {
|
|
78
|
+
if (!_budgets[msg.sender].installed) revert NotInstalledOn(msg.sender);
|
|
79
|
+
delete _budgets[msg.sender];
|
|
80
|
+
emit BudgetUninstalled(msg.sender);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── The hook ──────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/// @notice `preCheck` from the account: `msgData` is `abi.encode(target, value, data)` for `execute` and
|
|
86
|
+
/// `abi.encode(Call[])` for `executeBatch`. Sums the value this call moves in the budget's asset and
|
|
87
|
+
/// refuses when the window's total would pass the ceiling.
|
|
88
|
+
function preCheck(address, uint256, bytes calldata msgData) external returns (bytes memory) {
|
|
89
|
+
Budget storage b = _budgets[msg.sender];
|
|
90
|
+
if (!b.installed) return hex"";
|
|
91
|
+
uint256 amount = _valueMoved(b.asset, msgData);
|
|
92
|
+
if (amount == 0) return hex"";
|
|
93
|
+
uint64 idx = uint64((block.timestamp - b.windowStart) / b.windowSeconds);
|
|
94
|
+
if (idx != b.windowIndex) { b.windowIndex = idx; b.spent = 0; }
|
|
95
|
+
uint256 next = b.spent + amount;
|
|
96
|
+
if (next > b.ceiling) revert BudgetExceeded(b.ceiling, b.spent, amount);
|
|
97
|
+
b.spent = next;
|
|
98
|
+
emit Spent(msg.sender, idx, amount, next);
|
|
99
|
+
return hex"";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function postCheck(bytes calldata) external {}
|
|
103
|
+
|
|
104
|
+
// ─── Reads (the harness's honesty, never its authority) ────────────
|
|
105
|
+
|
|
106
|
+
function budget(address account) external view returns (address asset, uint256 ceiling, uint64 windowSeconds, uint64 windowStart, uint64 windowIndex, uint256 spent, bool installed) {
|
|
107
|
+
Budget storage b = _budgets[account];
|
|
108
|
+
return (b.asset, b.ceiling, b.windowSeconds, b.windowStart, b.windowIndex, b.spent, b.installed);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// @notice What the account has spent in the CURRENT window (0 once the window rolled), and the ceiling.
|
|
112
|
+
function remaining(address account) external view returns (uint256 left, uint256 ceiling) {
|
|
113
|
+
Budget storage b = _budgets[account];
|
|
114
|
+
if (!b.installed) return (type(uint256).max, 0);
|
|
115
|
+
uint64 idx = uint64((block.timestamp - b.windowStart) / b.windowSeconds);
|
|
116
|
+
uint256 spent = idx == b.windowIndex ? b.spent : 0;
|
|
117
|
+
return (b.ceiling > spent ? b.ceiling - spent : 0, b.ceiling);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Internals ─────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/// @dev Both encodings the account uses, told apart by the first word: `abi.encode(Call[])` opens with the
|
|
123
|
+
/// array's offset (exactly 0x20), `abi.encode(target, value, data)` with the target address — and no
|
|
124
|
+
/// contract lives at address 0x20. The decode runs through an external self-call so a malformed payload
|
|
125
|
+
/// is caught rather than reverting the account's call (abi.decode cannot be tried in place).
|
|
126
|
+
function _valueMoved(address asset, bytes calldata msgData) internal view returns (uint256 total) {
|
|
127
|
+
if (msgData.length < 32) return 0;
|
|
128
|
+
uint256 first = uint256(bytes32(msgData[0:32]));
|
|
129
|
+
if (first == 0x20) {
|
|
130
|
+
try this.decodeBatch(msgData) returns (Call[] memory calls) {
|
|
131
|
+
for (uint256 i = 0; i < calls.length; i++) total += _amountOf(asset, calls[i].target, calls[i].value, calls[i].data);
|
|
132
|
+
return total;
|
|
133
|
+
} catch { return 0; }
|
|
134
|
+
}
|
|
135
|
+
try this.decodeSingle(msgData) returns (address target, uint256 value, bytes memory data) {
|
|
136
|
+
return _amountOf(asset, target, value, data);
|
|
137
|
+
} catch { return 0; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
struct Call { address target; uint256 value; bytes data; }
|
|
141
|
+
|
|
142
|
+
function decodeSingle(bytes calldata msgData) external pure returns (address, uint256, bytes memory) {
|
|
143
|
+
return abi.decode(msgData, (address, uint256, bytes));
|
|
144
|
+
}
|
|
145
|
+
function decodeBatch(bytes calldata msgData) external pure returns (Call[] memory) {
|
|
146
|
+
return abi.decode(msgData, (Call[]));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _amountOf(address asset, address target, uint256 value, bytes memory data) internal pure returns (uint256) {
|
|
150
|
+
if (asset == address(0)) return value;
|
|
151
|
+
if (target != asset || data.length < 4) return 0;
|
|
152
|
+
bytes4 sel;
|
|
153
|
+
assembly { sel := mload(add(data, 32)) }
|
|
154
|
+
if (sel == TRANSFER && data.length >= 68) {
|
|
155
|
+
uint256 amount;
|
|
156
|
+
assembly { amount := mload(add(data, 68)) }
|
|
157
|
+
return amount;
|
|
158
|
+
}
|
|
159
|
+
if (sel == TRANSFER_FROM && data.length >= 100) {
|
|
160
|
+
uint256 amount;
|
|
161
|
+
assembly { amount := mload(add(data, 100)) }
|
|
162
|
+
return amount;
|
|
163
|
+
}
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -10,14 +10,15 @@ import "../agency/ICaveatEnforcer.sol";
|
|
|
10
10
|
* §2): authority *for a reason*, not authority for any purpose until revoked.
|
|
11
11
|
*
|
|
12
12
|
* @dev terms = abi.encode(bytes32 kind, bytes32 digest)
|
|
13
|
-
* args = abi.encode(bytes32 presented)
|
|
13
|
+
* args = abi.encode(bytes32 presented, bytes32 stepNonce) — supplied by the redeemer at redemption
|
|
14
14
|
*
|
|
15
15
|
* `kind` names WHICH artifact the digest is of, so an off-chain reader can tell an intent binding
|
|
16
16
|
* from a projection binding without guessing:
|
|
17
17
|
* keccak256("ap:intent") — the canonical intent (RFC 8785 JCS bytes)
|
|
18
18
|
* keccak256("ap:projection") — what was disclosed to the grantee (spec 336 §4)
|
|
19
19
|
* keccak256("ap:offer") — the accepted offer (spec 336 §5)
|
|
20
|
-
*
|
|
20
|
+
* keccak256("ap:plan") — the admitted plan (spec 408 §2.3)
|
|
21
|
+
* One enforcer serves all four: the mechanics are identical and the kind belongs in the terms,
|
|
21
22
|
* where it is signed, rather than in which contract address was chosen.
|
|
22
23
|
*
|
|
23
24
|
* WHAT THIS ENFORCES, HONESTLY. The chain cannot know what an intent *means*; it can only hold
|
|
@@ -30,33 +31,67 @@ import "../agency/ICaveatEnforcer.sol";
|
|
|
30
31
|
* 2. the presented digest lands in the transaction, so the receipt's `intentDigest` and the
|
|
31
32
|
* chain's record of what the redeemer claimed to be doing are the same bytes — evidence a
|
|
32
33
|
* third party can check without trusting either side.
|
|
33
|
-
* A redeemer that copies `terms` into `args` satisfies the check; that is the point — it has
|
|
34
|
+
* A redeemer that copies `terms` into `args` satisfies the digest check; that is the point — it has
|
|
34
35
|
* now committed, on chain, to that intent, and a step whose real intent differs is caught by the
|
|
35
36
|
* verifier that computed the digest from the step rather than from the caveat.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* R917-C-4 (spec 408 §1.4) — SINGLE-USE PER STEP. A digest-bound redemption is consumed under
|
|
39
|
+
* `(delegator, delegationHash, kind, stepNonce)`: the harness derives `stepNonce` from the intent
|
|
40
|
+
* digest and the step's reference (the same derivation as `PaymentEnforcer`'s nonce), so ONE step
|
|
41
|
+
* redeems once and a replay of it reverts `StepNonceReused`. Which steps exist is bound off chain by
|
|
42
|
+
* the plan digest the mandate carries; the chain holds each of them to one redemption. The hooks
|
|
43
|
+
* therefore mutate state and are `onlyDelegationManager` (EXT-PMT-1's rule for stateful enforcers):
|
|
44
|
+
* a stranger cannot burn a step's nonce ahead of the legitimate redemption.
|
|
39
45
|
*/
|
|
40
46
|
contract DigestBindingEnforcer is ICaveatEnforcer {
|
|
41
47
|
error BadTermsLength();
|
|
42
48
|
error BadArgsLength();
|
|
43
49
|
error DigestMismatch(bytes32 kind, bytes32 bound, bytes32 presented);
|
|
50
|
+
error StepNonceReused(bytes32 kind, bytes32 stepNonce);
|
|
51
|
+
error NotDelegationManager(address caller);
|
|
52
|
+
|
|
53
|
+
/// @notice The ONLY authorized caller of `beforeHook` — it consumes a nonce (R917-C-4).
|
|
54
|
+
address public immutable delegationManager;
|
|
55
|
+
|
|
56
|
+
/// delegator => delegationHash => keccak256(kind, stepNonce) => consumed
|
|
57
|
+
mapping(address => mapping(bytes32 => mapping(bytes32 => bool))) private _used;
|
|
58
|
+
|
|
59
|
+
event DigestBindingRedeemed(address indexed delegator, bytes32 indexed delegationHash, bytes32 indexed kind, bytes32 presented, bytes32 stepNonce);
|
|
60
|
+
|
|
61
|
+
constructor(address delegationManager_) {
|
|
62
|
+
require(delegationManager_ != address(0), "DigestBindingEnforcer: zero DelegationManager");
|
|
63
|
+
delegationManager = delegationManager_;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
modifier onlyDelegationManager() {
|
|
67
|
+
if (msg.sender != delegationManager) revert NotDelegationManager(msg.sender);
|
|
68
|
+
_;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// @notice Whether `stepNonce` was already consumed for this binding (a reader's replay check).
|
|
72
|
+
function isUsed(address delegator, bytes32 delegationHash, bytes32 kind, bytes32 stepNonce) external view returns (bool) {
|
|
73
|
+
return _used[delegator][delegationHash][keccak256(abi.encode(kind, stepNonce))];
|
|
74
|
+
}
|
|
44
75
|
|
|
45
76
|
function beforeHook(
|
|
46
77
|
bytes calldata terms,
|
|
47
78
|
bytes calldata args,
|
|
48
|
-
bytes32,
|
|
49
|
-
address,
|
|
79
|
+
bytes32 delegationHash,
|
|
80
|
+
address delegator,
|
|
50
81
|
address,
|
|
51
82
|
address,
|
|
52
83
|
uint256,
|
|
53
84
|
bytes calldata
|
|
54
|
-
) external
|
|
85
|
+
) external override onlyDelegationManager {
|
|
55
86
|
if (terms.length != 64) revert BadTermsLength();
|
|
56
|
-
if (args.length !=
|
|
87
|
+
if (args.length != 64) revert BadArgsLength();
|
|
57
88
|
(bytes32 kind, bytes32 bound) = abi.decode(terms, (bytes32, bytes32));
|
|
58
|
-
bytes32 presented = abi.decode(args, (bytes32));
|
|
89
|
+
(bytes32 presented, bytes32 stepNonce) = abi.decode(args, (bytes32, bytes32));
|
|
59
90
|
if (presented != bound) revert DigestMismatch(kind, bound, presented);
|
|
91
|
+
bytes32 slot = keccak256(abi.encode(kind, stepNonce));
|
|
92
|
+
if (_used[delegator][delegationHash][slot]) revert StepNonceReused(kind, stepNonce);
|
|
93
|
+
_used[delegator][delegationHash][slot] = true;
|
|
94
|
+
emit DigestBindingRedeemed(delegator, delegationHash, kind, presented, stepNonce);
|
|
60
95
|
}
|
|
61
96
|
|
|
62
97
|
function afterHook(
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../agency/ICaveatEnforcer.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title PayloadClassesEnforcer — the INFORMATION-FLOW caveat (spec 410 §7)
|
|
8
|
+
* @notice Where a read precedes a write in one plan — read a member's record, then send a message — the
|
|
9
|
+
* mandate names, as ontology classes, what the write MAY CARRY: `apvault:ContactEmail`, never
|
|
10
|
+
* `apvault:GivingHistory`. terms = abi.encode(string[] classIris).
|
|
11
|
+
*
|
|
12
|
+
* THE CHAIN CANNOT READ A PAYLOAD, and this enforcer does not pretend to: its hooks are pure no-ops.
|
|
13
|
+
* What it does is pin the terms INTO THE SIGNED DELEGATION — the caveat is part of the EIP-712 hash
|
|
14
|
+
* the person approved — so the off-chain per-step verifier (`delegation.evaluateCaveats`, strict)
|
|
15
|
+
* judges the write's declared payload classes (the tool contract's `carries`, per argument) against
|
|
16
|
+
* exactly what was signed, and nobody can widen the list without a new signature. An enforcer that
|
|
17
|
+
* judged nothing and pinned nothing would be a comment; this one pins.
|
|
18
|
+
*
|
|
19
|
+
* What it is NOT: authority over the read (the read has its own mandate and scope), a content
|
|
20
|
+
* filter, or a substitute for the vault's per-record scope — those are enforced where the data is.
|
|
21
|
+
*/
|
|
22
|
+
contract PayloadClassesEnforcer is ICaveatEnforcer {
|
|
23
|
+
function beforeHook(bytes calldata, bytes calldata, bytes32, address, address, address, uint256, bytes calldata) external pure override {}
|
|
24
|
+
function afterHook(bytes calldata, bytes calldata, bytes32, address, address, address, uint256, bytes calldata) external pure override {}
|
|
25
|
+
|
|
26
|
+
/// @notice The classes a wire's terms name — for a reader with the wire and no SDK.
|
|
27
|
+
function classesOf(bytes calldata terms) external pure returns (string[] memory) {
|
|
28
|
+
return abi.decode(terms, (string[]));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title ReceiptAnchorRegistry
|
|
6
|
+
* @notice The on-chain ANCHOR for a harness receipt (spec 351 P0.6, spec 406 W2): a run's record digest, bound
|
|
7
|
+
* to the intent digest the mandate was over and the mandate's reference, anchored once by the acting
|
|
8
|
+
* runtime's agent. A counterparty who holds the record recomputes its digest and reads the anchor here —
|
|
9
|
+
* no trust in the runtime's logs, no vendor, no vault: the chain says a runtime with THIS agent's key
|
|
10
|
+
* committed to THESE bytes at THIS block, and the intent digest says which ask it was under.
|
|
11
|
+
*
|
|
12
|
+
* @dev DIGESTS AND ADDRESSES ONLY — nothing of what the run was about reaches the chain (the S1 firewall of
|
|
13
|
+
* spec 395 holds by construction). Per-row storage, so a reader uses `readContract` (ADR-0012: no log
|
|
14
|
+
* walks in product paths); the event is for indexers. Anchoring is permissionless: WHO anchored is the
|
|
15
|
+
* evidence (`anchoredBy`), never a gate — a stranger anchoring random digests proves nothing about a
|
|
16
|
+
* run whose receipt names a different agent. A digest anchors ONCE PER ANCHORER (R917-C-3, spec 408
|
|
17
|
+
* §1.5): rows are keyed by `(anchoredBy, receiptDigest)`, so a front-runner on a public mempool cannot
|
|
18
|
+
* squat the digest ahead of the agent, and an anchor cannot be re-attributed after the fact.
|
|
19
|
+
*/
|
|
20
|
+
contract ReceiptAnchorRegistry {
|
|
21
|
+
struct Anchor {
|
|
22
|
+
address anchoredBy;
|
|
23
|
+
uint64 at;
|
|
24
|
+
bytes32 intentDigest;
|
|
25
|
+
bytes32 mandateRef;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// anchoredBy => receiptDigest => anchor (R917-C-3: keyed by WHO anchored, so a stranger's anchor of
|
|
29
|
+
/// the same digest is a different row — it can neither pre-empt the agent's nor be mistaken for it).
|
|
30
|
+
mapping(address => mapping(bytes32 => Anchor)) private _anchors;
|
|
31
|
+
|
|
32
|
+
event ReceiptAnchored(bytes32 indexed receiptDigest, address indexed anchoredBy, bytes32 indexed intentDigest, bytes32 mandateRef, uint64 at);
|
|
33
|
+
|
|
34
|
+
error ZeroDigest();
|
|
35
|
+
error AlreadyAnchored(bytes32 receiptDigest, address by);
|
|
36
|
+
|
|
37
|
+
/// @notice Anchor a receipt digest, once per anchorer. `intentDigest` and `mandateRef` may be zero for a run under no mandate (a read).
|
|
38
|
+
function anchor(bytes32 receiptDigest, bytes32 intentDigest, bytes32 mandateRef) external {
|
|
39
|
+
if (receiptDigest == bytes32(0)) revert ZeroDigest();
|
|
40
|
+
Anchor storage a = _anchors[msg.sender][receiptDigest];
|
|
41
|
+
if (a.anchoredBy != address(0)) revert AlreadyAnchored(receiptDigest, a.anchoredBy);
|
|
42
|
+
_anchors[msg.sender][receiptDigest] = Anchor({ anchoredBy: msg.sender, at: uint64(block.timestamp), intentDigest: intentDigest, mandateRef: mandateRef });
|
|
43
|
+
emit ReceiptAnchored(receiptDigest, msg.sender, intentDigest, mandateRef, uint64(block.timestamp));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// @notice The anchor `anchoredBy` made of a digest, or a zero struct when none — the reader names the
|
|
47
|
+
/// agent from the receipt it holds, and decides what absence means.
|
|
48
|
+
function anchorOf(address anchoredBy, bytes32 receiptDigest) external view returns (Anchor memory) {
|
|
49
|
+
return _anchors[anchoredBy][receiptDigest];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isAnchored(address anchoredBy, bytes32 receiptDigest) external view returns (bool) {
|
|
53
|
+
return _anchors[anchoredBy][receiptDigest].anchoredBy != address(0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title EstateProjectionRegistry — an estate's authority, projected onto PUBLIC GROUND (spec 410 §4)
|
|
6
|
+
* @notice One registry, many estates. An estate's grants and relationships are enforced on its own chain and
|
|
7
|
+
* held in its principals' vaults; a verifier elsewhere can read neither. What the estate anchors here
|
|
8
|
+
* is two Merkle roots over BLINDED commitments — the live-grant root (every grant currently live) and
|
|
9
|
+
* the membership root (every countersigned relationship credential) — plus the hash of its signed
|
|
10
|
+
* charter (spec 410 §9), under a monotonic sequence. A presentation against these roots tells a
|
|
11
|
+
* verifier in another estate that a grant is live or an agent is a member in that estate as of that
|
|
12
|
+
* sequence, and nothing else (the estate-projection SDK package).
|
|
13
|
+
*
|
|
14
|
+
* WHO MAY WRITE. An estate REGISTERS once: its id — keccak256("eip155:<chainId>:<governance>"), the
|
|
15
|
+
* one fact public ground learns about it — bound to the SIGNER its governance controls here (an EOA
|
|
16
|
+
* or an account on this chain). First registration wins; the signer rotates only by the current
|
|
17
|
+
* signer. `setRoots` is the signer's, with a sequence strictly above the last: a replayed or reordered
|
|
18
|
+
* projection is refused. No admin, no pause, no upgrade: public ground has no operator either.
|
|
19
|
+
*
|
|
20
|
+
* WHAT THIS IS NOT: authority. A root proves what the estate vouches for at a moment; whether the
|
|
21
|
+
* receiving estate lets an agent ACT is its own mandate, per step (ADR-0056 / ADR-0057).
|
|
22
|
+
*/
|
|
23
|
+
contract EstateProjectionRegistry {
|
|
24
|
+
struct Roots {
|
|
25
|
+
bytes32 liveGrantRoot;
|
|
26
|
+
bytes32 membershipRoot;
|
|
27
|
+
bytes32 charterHash;
|
|
28
|
+
uint64 sequence;
|
|
29
|
+
uint64 anchoredAt;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mapping(bytes32 estateId => address signer) public signerOf;
|
|
33
|
+
mapping(bytes32 estateId => Roots) private _roots;
|
|
34
|
+
|
|
35
|
+
event EstateRegistered(bytes32 indexed estateId, address indexed signer);
|
|
36
|
+
event EstateSignerRotated(bytes32 indexed estateId, address indexed from, address indexed to);
|
|
37
|
+
event RootsAnchored(bytes32 indexed estateId, uint64 indexed sequence, bytes32 liveGrantRoot, bytes32 membershipRoot, bytes32 charterHash);
|
|
38
|
+
|
|
39
|
+
error AlreadyRegistered(bytes32 estateId);
|
|
40
|
+
error NotRegistered(bytes32 estateId);
|
|
41
|
+
error NotTheSigner(bytes32 estateId, address caller);
|
|
42
|
+
error SequenceNotAbove(uint64 last, uint64 given);
|
|
43
|
+
error ZeroAddress();
|
|
44
|
+
|
|
45
|
+
/// @notice Bind an estate id to the signer its governance controls on this chain. First come, once.
|
|
46
|
+
function register(bytes32 estateId, address signer) external {
|
|
47
|
+
if (signer == address(0)) revert ZeroAddress();
|
|
48
|
+
if (signerOf[estateId] != address(0)) revert AlreadyRegistered(estateId);
|
|
49
|
+
signerOf[estateId] = signer;
|
|
50
|
+
emit EstateRegistered(estateId, signer);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rotateSigner(bytes32 estateId, address to) external {
|
|
54
|
+
if (to == address(0)) revert ZeroAddress();
|
|
55
|
+
address cur = signerOf[estateId];
|
|
56
|
+
if (cur == address(0)) revert NotRegistered(estateId);
|
|
57
|
+
if (msg.sender != cur) revert NotTheSigner(estateId, msg.sender);
|
|
58
|
+
signerOf[estateId] = to;
|
|
59
|
+
emit EstateSignerRotated(estateId, cur, to);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// @notice Anchor the estate's roots under `sequence` (strictly above the last). Refreshed on every revocation
|
|
63
|
+
/// and membership change; a verifier's freshness policy reads `anchoredAt`.
|
|
64
|
+
function setRoots(bytes32 estateId, bytes32 liveGrantRoot, bytes32 membershipRoot, bytes32 charterHash, uint64 sequence) external {
|
|
65
|
+
address cur = signerOf[estateId];
|
|
66
|
+
if (cur == address(0)) revert NotRegistered(estateId);
|
|
67
|
+
if (msg.sender != cur) revert NotTheSigner(estateId, msg.sender);
|
|
68
|
+
Roots storage r = _roots[estateId];
|
|
69
|
+
if (sequence <= r.sequence) revert SequenceNotAbove(r.sequence, sequence);
|
|
70
|
+
r.liveGrantRoot = liveGrantRoot;
|
|
71
|
+
r.membershipRoot = membershipRoot;
|
|
72
|
+
r.charterHash = charterHash;
|
|
73
|
+
r.sequence = sequence;
|
|
74
|
+
r.anchoredAt = uint64(block.timestamp);
|
|
75
|
+
emit RootsAnchored(estateId, sequence, liveGrantRoot, membershipRoot, charterHash);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// @notice The one read a verifier makes: the roots as anchored, with the sequence and when.
|
|
79
|
+
function roots(bytes32 estateId) external view returns (bytes32 liveGrantRoot, bytes32 membershipRoot, bytes32 charterHash, uint64 sequence, uint64 anchoredAt) {
|
|
80
|
+
Roots storage r = _roots[estateId];
|
|
81
|
+
return (r.liveGrantRoot, r.membershipRoot, r.charterHash, r.sequence, r.anchoredAt);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// @notice keccak256("eip155:<chainId>:<governance>") — the same derivation the SDK's `estateIdOf` uses.
|
|
85
|
+
function estateIdOf(uint256 chainId, address governance) external pure returns (bytes32) {
|
|
86
|
+
return keccak256(abi.encodePacked("eip155:", _utoa(chainId), ":", _lowerHex(governance)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _utoa(uint256 v) private pure returns (string memory) {
|
|
90
|
+
if (v == 0) return "0";
|
|
91
|
+
uint256 j = v; uint256 len;
|
|
92
|
+
while (j != 0) { len++; j /= 10; }
|
|
93
|
+
bytes memory b = new bytes(len);
|
|
94
|
+
while (v != 0) { len -= 1; b[len] = bytes1(uint8(48 + v % 10)); v /= 10; }
|
|
95
|
+
return string(b);
|
|
96
|
+
}
|
|
97
|
+
function _lowerHex(address a) private pure returns (string memory) {
|
|
98
|
+
bytes memory alphabet = "0123456789abcdef";
|
|
99
|
+
bytes memory s = new bytes(42);
|
|
100
|
+
s[0] = "0"; s[1] = "x";
|
|
101
|
+
uint160 v = uint160(a);
|
|
102
|
+
for (uint256 i = 0; i < 20; i++) {
|
|
103
|
+
uint8 bt = uint8(v >> (8 * (19 - i)));
|
|
104
|
+
s[2 + i * 2] = alphabet[bt >> 4];
|
|
105
|
+
s[3 + i * 2] = alphabet[bt & 0x0f];
|
|
106
|
+
}
|
|
107
|
+
return string(s);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/deployments-anvil.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"agentAccountFactory":"0x139e1D41943ee15dDe4DF876f9d0E7F85e26660A","agentAccountImplementation":"0x2f663C00A026A416802A629fb44490E3F276cF04","agentNameRegistry":"0x9385556B571ab92bf6dC9a0DbD75429Dd4d56F91","agentNameResolver":"0x162700d1613DfEC978032A909DE02643bC55df1A","agentNameUniversalResolver":"0x67aD6EA566BA6B0fC52e97Bc25CE46120fdAc04c","agentProfileResolver":"0xb185E9f6531BA9877741022C92CE858cDCc5760E","agentRelationship":"0x871ACbEabBaf8Bed65c22ba7132beCFaBf8c27B5","agreementRegistry":"0x01E21d7B8c39dc4C764c19b308Bd8b14B1ba139E","allowedMethodsEnforcer":"0xcE0066b1008237625dDDBE4a751827de037E53D2","allowedTargetsEnforcer":"0x7B4f352Cd40114f12e82fC675b5BA8C7582FC513","approvedHashRegistry":"0xD6b040736e948621c5b6E0a494473c47a6113eA8","attestationRegistry":"0x9Fcca440F19c62CDF7f973eB6DDF218B15d4C71D","callDataHashEnforcer":"0x87006e75a5B6bE9D1bbF61AC8Cd84f05D9140589","chainId":31337,"custodyPolicy":"0xFE5f411481565fbF70D8D33D992C78196E014b90","delegationManager":"0x2625760C4A8e8101801D3a48eE64B2bEA42f1E96","deployer":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","entryPoint":"0x3904b8f5b0F49cD206b7d5AABeE5D1F37eE15D8d","geoFeatureRegistry":"0x1343248Cbd4e291C6979e70a138f4c774e902561","mockUsdc":"0xc9952Fc93Fa9bE383ccB39008c786b9f94eAc95d","ontologyTermRegistry":"0x70eE76691Bdd9696552AF8d4fd634b3cF79DD529","paymentEnforcer":"0xC7143d5bA86553C06f5730c8dC9f8187a621A8D4","paymentReceiptRegistry":"0x8fC8CFB7f7362E44E472c690A6e025B80E406458","permissionlessSubregistry":"0xe1708FA6bb2844D5384613ef0846F9Bc1e8eC55E","permissionlessSubregistryDemoAgent":"0xa722bdA6968F50778B973Ae2701e90200C564B49","quorumEnforcer":"0x51C65cd0Cdb1A8A8b79dfc2eE965B1bA0bb8fc89","relationshipTypeRegistry":"0x8e264821AFa98DD104eEcfcfa7FD9f8D8B320adA","shapeRegistry":"0x8B190573374637f144AC8D37375d97fd84cBD3a0","skillDefinitionRegistry":"0x3C1Cb427D20F15563aDa8C249E71db76d7183B6c","smartAgentPaymaster":"0xDde063eBe8E85D666AD99f731B4Dbf8C98F29708","timestampEnforcer":"0xAdE429ba898c34722e722415D722A70a297cE3a2","universalSignatureValidator":"0xD5724171C2b7f0AA717a324626050BD05767e2C6","valueEnforcer":"0x82EdA215Fa92B45a3a76837C65Ab862b6C7564a8"}
|