@agenticprimitives/contracts 0.1.0-alpha.2
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 +67 -0
- package/CLAUDE.md +40 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/deployments-anvil.json +1 -0
- package/deployments-base-sepolia.json +1 -0
- package/dist/abi/AgentNameAttributeResolver.json +798 -0
- package/dist/abi/AgentNamePredicates.json +1 -0
- package/dist/abi/AgentNameRegistry.json +826 -0
- package/dist/abi/AgentNameUniversalResolver.json +222 -0
- package/dist/abi/AgentProfilePredicates.json +1 -0
- package/dist/abi/AgentProfileResolver.json +1044 -0
- package/dist/abi/AgentRelationship.json +583 -0
- package/dist/abi/AgentRelationshipPredicates.json +1 -0
- package/dist/abi/AgenticGovernance.json +259 -0
- package/dist/abi/AllowedMethodsEnforcer.json +108 -0
- package/dist/abi/AllowedTargetsEnforcer.json +103 -0
- package/dist/abi/ApprovedHashRegistry.json +114 -0
- package/dist/abi/AttributeStorage.json +557 -0
- package/dist/abi/CaveatEnforcerBase.json +130 -0
- package/dist/abi/GovernanceManaged.json +43 -0
- package/dist/abi/IAttributeReader.json +98 -0
- package/dist/abi/ICaveatEnforcer.json +98 -0
- package/dist/abi/IDelegationManager.json +211 -0
- package/dist/abi/IERC7579Module.json +34 -0
- package/dist/abi/IERC7579ModuleLifecycle.json +60 -0
- package/dist/abi/IGovernanceView.json +34 -0
- package/dist/abi/MultiSendCallOnly.json +29 -0
- package/dist/abi/MultiSendCallOnlyHarness.json +42 -0
- package/dist/abi/OntologyTermRegistry.json +397 -0
- package/dist/abi/P256Verifier.json +1 -0
- package/dist/abi/PermissionlessSubregistry.json +207 -0
- package/dist/abi/RelationshipTypeRegistry.json +455 -0
- package/dist/abi/ShapeRegistry.json +627 -0
- package/dist/abi/SmartAgentModuleTypes.json +1 -0
- package/dist/abi/TimestampEnforcer.json +108 -0
- package/dist/abi/ValueEnforcer.json +103 -0
- package/dist/abi/WebAuthnLib.json +1 -0
- package/dist/abi/index.d.ts +35 -0
- package/dist/abi/index.js +35 -0
- package/package.json +48 -0
- package/spec.md +52 -0
- package/src/AgentAccount.sol +1374 -0
- package/src/AgentAccountFactory.sol +274 -0
- package/src/ApprovedHashRegistry.sol +57 -0
- package/src/IAgentAccount.sol +138 -0
- package/src/SmartAgentPaymaster.sol +281 -0
- package/src/UniversalSignatureValidator.sol +136 -0
- package/src/agency/DelegationManager.sol +374 -0
- package/src/agency/ICaveatEnforcer.sol +62 -0
- package/src/agency/IDelegationManager.sol +69 -0
- package/src/custody/CustodyPolicy.sol +892 -0
- package/src/custody/IERC7579Module.sol +60 -0
- package/src/enforcers/AllowedMethodsEnforcer.AUDIT.md +51 -0
- package/src/enforcers/AllowedMethodsEnforcer.sol +48 -0
- package/src/enforcers/AllowedTargetsEnforcer.AUDIT.md +49 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +44 -0
- package/src/enforcers/CaveatEnforcerBase.sol +19 -0
- package/src/enforcers/QuorumEnforcer.AUDIT.md +71 -0
- package/src/enforcers/QuorumEnforcer.sol +191 -0
- package/src/enforcers/TimestampEnforcer.AUDIT.md +50 -0
- package/src/enforcers/TimestampEnforcer.sol +43 -0
- package/src/enforcers/ValueEnforcer.AUDIT.md +51 -0
- package/src/enforcers/ValueEnforcer.sol +41 -0
- package/src/governance/AgenticGovernance.sol +140 -0
- package/src/governance/GovernanceManaged.sol +75 -0
- package/src/governance/IGovernance.sol +15 -0
- package/src/identity/AgentProfilePredicates.sol +40 -0
- package/src/identity/AgentProfileResolver.sol +194 -0
- package/src/libraries/MultiSendCallOnly.sol +95 -0
- package/src/libraries/P256Verifier.sol +47 -0
- package/src/libraries/SignatureSlotRecovery.sol +196 -0
- package/src/libraries/WebAuthnLib.sol +164 -0
- package/src/naming/AgentNameAttributeResolver.sol +95 -0
- package/src/naming/AgentNamePredicates.sol +74 -0
- package/src/naming/AgentNameRegistry.sol +362 -0
- package/src/naming/AgentNameUniversalResolver.sol +210 -0
- package/src/naming/PermissionlessSubregistry.sol +98 -0
- package/src/ontology/AttributeStorage.sol +289 -0
- package/src/ontology/OntologyTermRegistry.sol +146 -0
- package/src/ontology/ShapeRegistry.sol +240 -0
- package/src/relationships/AgentRelationship.sol +289 -0
- package/src/relationships/AgentRelationshipPredicates.sol +44 -0
- package/src/relationships/RelationshipTypeRegistry.sol +143 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import {IAgentAccount, AgentAccountRecoveryArgs, AgentAccountRecoveryPasskeyAdd} from "../IAgentAccount.sol";
|
|
5
|
+
import {SignatureSlotRecovery} from "../libraries/SignatureSlotRecovery.sol";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @title CustodyPolicy
|
|
9
|
+
* @notice ERC-7579 module that owns the schedule / apply / cancel
|
|
10
|
+
* custody-change surface for `AgentAccount` accounts running
|
|
11
|
+
* in any non-`single` mode. Per spec 209 (ERC-7579 module
|
|
12
|
+
* taxonomy) this is the first module extraction; per spec 213
|
|
13
|
+
* (custody-layer carve-out, phase 6g.1) this is renamed from
|
|
14
|
+
* the original `ThresholdValidator` to align with the
|
|
15
|
+
* custody-layer vocabulary firewall.
|
|
16
|
+
*
|
|
17
|
+
* Despite the underlying module type being `EXECUTOR` (id 2),
|
|
18
|
+
* the user-facing semantics are CUSTODY POLICY — it decides
|
|
19
|
+
* who can authorize a custody change (m-of-n approvals from
|
|
20
|
+
* the custody council) and applies the change via
|
|
21
|
+
* `executeFromModule(...)`.
|
|
22
|
+
*
|
|
23
|
+
* State is keyed by account address — one CustodyPolicy
|
|
24
|
+
* instance can serve many accounts. `msg.sender` during
|
|
25
|
+
* install is the account; subsequent custody calls take the
|
|
26
|
+
* account as an explicit first arg so they can be made from
|
|
27
|
+
* any caller (the signature blob is what authorizes).
|
|
28
|
+
*
|
|
29
|
+
* Spec 207 § 5 tier matrix (T1 Read / T2 Write / T3 Value /
|
|
30
|
+
* T4 Admin / T5 Critical / T6 Recovery) is unchanged from the
|
|
31
|
+
* pre-extraction surface. Per spec 213 § 2.2, the enum was
|
|
32
|
+
* renamed from `AdminAction` → `CustodyAction`, the propose /
|
|
33
|
+
* execute / cancel functions to schedule / apply / cancel
|
|
34
|
+
* scheduled change, state vars (threshold → approvalsRequired,
|
|
35
|
+
* timelock → safetyDelay, guardian → trustee, owner → custodian),
|
|
36
|
+
* and EIP-712 typehashes likewise.
|
|
37
|
+
*/
|
|
38
|
+
contract CustodyPolicy {
|
|
39
|
+
// ─── ERC-7579 marker constants (mirror of the account-side ids) ──
|
|
40
|
+
uint256 internal constant MODULE_TYPE_EXECUTOR = 2;
|
|
41
|
+
|
|
42
|
+
// ─── CustodyAction enum + structs (moved from AgentAccount) ────────
|
|
43
|
+
|
|
44
|
+
enum CustodyAction {
|
|
45
|
+
AddCustodian, // 0 — T4
|
|
46
|
+
RemoveCustodian, // 1 — T4
|
|
47
|
+
AddPasskeyCredential, // 2 — T4
|
|
48
|
+
RemovePasskeyCredential, // 3 — T4
|
|
49
|
+
AddTrustee, // 4 — T4
|
|
50
|
+
RemoveTrustee, // 5 — T4
|
|
51
|
+
ChangeCustodyMode, // 6 — T4
|
|
52
|
+
ApplySystemUpdate, // 7 — T5
|
|
53
|
+
RotateDelegationManager, // 8 — T5
|
|
54
|
+
RotatePaymaster, // 9 — T5 (stubbed; reverts on execute)
|
|
55
|
+
RotateSessionIssuer, // 10 — T5 (stubbed)
|
|
56
|
+
RotateAllCustodians, // 11 — T4
|
|
57
|
+
ChangeValueCeiling, // 12 — T4
|
|
58
|
+
SetRecoveryApprovals, // 13 — T4
|
|
59
|
+
RecoverAccount, // 14 — T6
|
|
60
|
+
ChangeApprovalsRequired // 15 — T4 (per-tier custody quorum threshold)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
struct ScheduledChange {
|
|
64
|
+
CustodyAction action;
|
|
65
|
+
bytes args;
|
|
66
|
+
uint64 proposedAt;
|
|
67
|
+
uint64 eta;
|
|
68
|
+
address proposer;
|
|
69
|
+
bool executed;
|
|
70
|
+
bool cancelled;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// @dev Per-account config. One mapping entry per installed-on account.
|
|
74
|
+
struct Config {
|
|
75
|
+
bool installed;
|
|
76
|
+
/// @dev Audit C-11: once an account uninstalls the CustodyPolicy,
|
|
77
|
+
/// reinstall is permanently forbidden — stale trustees /
|
|
78
|
+
/// thresholds / pending changes would otherwise compose with
|
|
79
|
+
/// the next install in unpredictable ways. Accounts that
|
|
80
|
+
/// want a fresh policy state must deploy a fresh account.
|
|
81
|
+
bool permanentlyUninstalled;
|
|
82
|
+
uint8 mode; // 0=single, 1=hybrid, 2=threshold, 3=org
|
|
83
|
+
uint8 recoveryApprovals;
|
|
84
|
+
uint256 t3HighValueCeiling;
|
|
85
|
+
mapping(uint8 => uint8) approvalsRequiredByTier;
|
|
86
|
+
mapping(uint8 => uint32) safetyDelayByTier;
|
|
87
|
+
mapping(address => bool) trustees;
|
|
88
|
+
uint256 trusteeCount;
|
|
89
|
+
uint256 nextChangeId;
|
|
90
|
+
mapping(uint256 => ScheduledChange) pending;
|
|
91
|
+
mapping(uint256 => mapping(address => bool)) proposerCustodians;
|
|
92
|
+
address approvedHashRegistry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
mapping(address => Config) internal _configs;
|
|
96
|
+
|
|
97
|
+
// ─── Constants ────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
// ─── EIP-712 (spec 207 § 15) ─────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
bytes32 internal constant EIP712_DOMAIN_TYPEHASH = keccak256(
|
|
102
|
+
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
103
|
+
);
|
|
104
|
+
bytes32 internal constant EIP712_NAME_HASH = keccak256("agenticprimitives.CustodyPolicy");
|
|
105
|
+
bytes32 internal constant EIP712_VERSION_HASH = keccak256("1");
|
|
106
|
+
|
|
107
|
+
bytes32 internal constant SCHEDULE_CUSTODY_CHANGE_TYPEHASH = keccak256(
|
|
108
|
+
"ScheduleCustodyChangeRequest(address account,uint8 action,bytes32 argsHash,uint256 changeId)"
|
|
109
|
+
);
|
|
110
|
+
bytes32 internal constant APPLY_CUSTODY_CHANGE_TYPEHASH = keccak256(
|
|
111
|
+
"ApplyCustodyChangeRequest(address account,uint8 action,bytes32 argsHash,uint256 changeId,uint64 eta)"
|
|
112
|
+
);
|
|
113
|
+
bytes32 internal constant CANCEL_SCHEDULED_CHANGE_TYPEHASH = keccak256(
|
|
114
|
+
"CancelScheduledChangeRequest(address account,uint8 action,bytes32 argsHash,uint256 changeId,uint64 eta)"
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/// @dev Cached at deploy-time for the deploy-time chainId. If
|
|
118
|
+
/// `block.chainid` differs at call time (chain fork), we
|
|
119
|
+
/// recompute on demand. Mirrors OpenZeppelin's EIP712 base.
|
|
120
|
+
uint256 private immutable _CACHED_CHAIN_ID;
|
|
121
|
+
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
|
|
122
|
+
|
|
123
|
+
constructor() {
|
|
124
|
+
_CACHED_CHAIN_ID = block.chainid;
|
|
125
|
+
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _buildDomainSeparator() internal view returns (bytes32) {
|
|
129
|
+
return keccak256(
|
|
130
|
+
abi.encode(
|
|
131
|
+
EIP712_DOMAIN_TYPEHASH,
|
|
132
|
+
EIP712_NAME_HASH,
|
|
133
|
+
EIP712_VERSION_HASH,
|
|
134
|
+
block.chainid,
|
|
135
|
+
address(this)
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _domainSeparator() internal view returns (bytes32) {
|
|
141
|
+
if (block.chainid == _CACHED_CHAIN_ID) return _CACHED_DOMAIN_SEPARATOR;
|
|
142
|
+
return _buildDomainSeparator();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// @notice EIP-712 domain separator. Off-chain code can `eth_call` to
|
|
146
|
+
/// confirm what domain wallet sigs are bound to.
|
|
147
|
+
function domainSeparator() external view returns (bytes32) {
|
|
148
|
+
return _domainSeparator();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
|
|
152
|
+
return keccak256(abi.encodePacked(bytes2(0x1901), _domainSeparator(), structHash));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _hashProposeRequest(
|
|
156
|
+
address account,
|
|
157
|
+
CustodyAction action,
|
|
158
|
+
bytes memory args,
|
|
159
|
+
uint256 changeId
|
|
160
|
+
) internal view returns (bytes32) {
|
|
161
|
+
return _hashTypedDataV4(
|
|
162
|
+
keccak256(abi.encode(SCHEDULE_CUSTODY_CHANGE_TYPEHASH, account, uint8(action), keccak256(args), changeId))
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _hashExecuteRequest(
|
|
167
|
+
address account,
|
|
168
|
+
CustodyAction action,
|
|
169
|
+
bytes memory args,
|
|
170
|
+
uint256 changeId,
|
|
171
|
+
uint64 eta
|
|
172
|
+
) internal view returns (bytes32) {
|
|
173
|
+
return _hashTypedDataV4(
|
|
174
|
+
keccak256(abi.encode(APPLY_CUSTODY_CHANGE_TYPEHASH, account, uint8(action), keccak256(args), changeId, eta))
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function _hashCancelRequest(
|
|
179
|
+
address account,
|
|
180
|
+
CustodyAction action,
|
|
181
|
+
bytes memory args,
|
|
182
|
+
uint256 changeId,
|
|
183
|
+
uint64 eta
|
|
184
|
+
) internal view returns (bytes32) {
|
|
185
|
+
return _hashTypedDataV4(
|
|
186
|
+
keccak256(abi.encode(CANCEL_SCHEDULED_CHANGE_TYPEHASH, account, uint8(action), keccak256(args), changeId, eta))
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// @dev Spec 207 § 8 — first 24h of the T6 timelock is the
|
|
191
|
+
/// primary-owner cancel window.
|
|
192
|
+
uint64 internal constant RECOVERY_PRIMARY_CANCEL_WINDOW = 24 hours;
|
|
193
|
+
|
|
194
|
+
// ─── Events ───────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
event CustodyPolicyInstalled(address indexed account, uint8 mode, uint8 recoveryApprovals);
|
|
197
|
+
event CustodyPolicyUninstalled(address indexed account);
|
|
198
|
+
|
|
199
|
+
event CustodyChangeScheduled(address indexed account, uint256 indexed changeId, CustodyAction indexed action, uint64 eta, address proposer);
|
|
200
|
+
event CustodyChangeApplied(address indexed account, uint256 indexed changeId);
|
|
201
|
+
event ScheduledChangeCancelled(address indexed account, uint256 indexed changeId);
|
|
202
|
+
event GuardianAdded(address indexed account, address indexed guardian);
|
|
203
|
+
event GuardianRemoved(address indexed account, address indexed guardian);
|
|
204
|
+
event ThresholdChanged(address indexed account, uint8 indexed tier, uint8 oldValue, uint8 newValue);
|
|
205
|
+
event ModeChanged(address indexed account, uint8 oldMode, uint8 newMode);
|
|
206
|
+
event T3CeilingChanged(address indexed account, uint256 oldCeiling, uint256 newCeiling);
|
|
207
|
+
event RecoveryThresholdChanged(address indexed account, uint8 oldValue, uint8 newValue);
|
|
208
|
+
event OwnersRotated(address indexed account, uint256 newOwnerCount);
|
|
209
|
+
/// @notice Audit C-10: per-rotation count of custodians removed.
|
|
210
|
+
event CustodiansRemovedDuringRotation(address indexed account, uint256 removedCount);
|
|
211
|
+
/// @notice Audit C-11: CustodyPolicy was uninstalled; the account
|
|
212
|
+
/// can never reinstall it. Surfaced as a loud event so
|
|
213
|
+
/// operators see the lock applied.
|
|
214
|
+
event CustodyPolicyPermanentlyUninstalled(address indexed account);
|
|
215
|
+
event AccountRecovered(
|
|
216
|
+
address indexed account,
|
|
217
|
+
uint256 ownersAddedCount,
|
|
218
|
+
uint256 ownersRemovedCount,
|
|
219
|
+
uint256 passkeysAddedCount,
|
|
220
|
+
uint256 passkeysRemovedCount
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// ─── Errors ───────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
error NotInstalledOn(address account);
|
|
226
|
+
error AlreadyInstalledOn(address account);
|
|
227
|
+
error OnInstallNotByAccount(address caller);
|
|
228
|
+
|
|
229
|
+
error InvalidCustodyAction(uint8 action);
|
|
230
|
+
error InvalidTier(uint8 tier);
|
|
231
|
+
error ProposalNotFound(uint256 changeId);
|
|
232
|
+
error ProposalAlreadyExecuted(uint256 changeId);
|
|
233
|
+
error ProposalAlreadyCancelled(uint256 changeId);
|
|
234
|
+
error ProposalNotReady(uint256 changeId, uint64 eta);
|
|
235
|
+
error AdminInsufficientQuorum(uint256 supplied, uint8 required);
|
|
236
|
+
error AdminDuplicateOrUnsortedSigner(address signer);
|
|
237
|
+
error AdminUnauthorizedSigner(address signer);
|
|
238
|
+
error TrusteeAlreadyExists(address guardian);
|
|
239
|
+
error TrusteeDoesNotExist(address guardian);
|
|
240
|
+
error InvalidMode(uint8 mode_);
|
|
241
|
+
error InvalidThresholdValue(uint8 thr);
|
|
242
|
+
error RecoveryRequiresGuardians();
|
|
243
|
+
error EmptyOwnerSet();
|
|
244
|
+
error CustodyActionNotYetImplemented(uint8 action);
|
|
245
|
+
error CannotDowngradeWithTrustees();
|
|
246
|
+
error TimelockRequiredForTier(uint8 tier);
|
|
247
|
+
error SeparationOfDutiesViolation(address signer);
|
|
248
|
+
error RecoveryRequiresGuardianQuorum();
|
|
249
|
+
error UnauthorizedTrustee(address signer);
|
|
250
|
+
error ZeroAddress();
|
|
251
|
+
/// @dev Audit C-11: reinstall over a previously-uninstalled config
|
|
252
|
+
/// is forbidden — stale mappings would compose unpredictably.
|
|
253
|
+
error ReinstallForbidden(address account);
|
|
254
|
+
|
|
255
|
+
// ─── ERC-7579 lifecycle ───────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Install-time init data shape:
|
|
259
|
+
* abi.encode(
|
|
260
|
+
* uint8 mode,
|
|
261
|
+
* uint8 recoveryApprovals,
|
|
262
|
+
* address[] trustees,
|
|
263
|
+
* uint8[7] approvalsRequiredByTier, // index 0 unused; T1..T6 use 1..6
|
|
264
|
+
* uint32[7] safetyDelayByTier,
|
|
265
|
+
* uint256 t3HighValueCeiling,
|
|
266
|
+
* address approvedHashRegistry
|
|
267
|
+
* )
|
|
268
|
+
*/
|
|
269
|
+
function onInstall(bytes calldata data) external {
|
|
270
|
+
address account = msg.sender;
|
|
271
|
+
Config storage c = _configs[account];
|
|
272
|
+
if (c.installed) revert AlreadyInstalledOn(account);
|
|
273
|
+
// Audit C-11: reinstall over previously-uninstalled state is
|
|
274
|
+
// forbidden. The stale mappings (trustees, thresholds, pending)
|
|
275
|
+
// never zero out — silently re-using them on a fresh install
|
|
276
|
+
// could compose with adversary-chosen new state in pathological
|
|
277
|
+
// ways. Force a new deploy if the operator genuinely needs a
|
|
278
|
+
// fresh policy for this account.
|
|
279
|
+
if (c.permanentlyUninstalled) revert ReinstallForbidden(account);
|
|
280
|
+
|
|
281
|
+
(
|
|
282
|
+
uint8 modeVal,
|
|
283
|
+
uint8 recThr,
|
|
284
|
+
address[] memory trustees,
|
|
285
|
+
uint8[7] memory thresholds,
|
|
286
|
+
uint32[7] memory timelocks,
|
|
287
|
+
uint256 t3Ceiling,
|
|
288
|
+
address approvedHashReg
|
|
289
|
+
) = abi.decode(data, (uint8, uint8, address[], uint8[7], uint32[7], uint256, address));
|
|
290
|
+
|
|
291
|
+
if (modeVal > 3) revert InvalidMode(modeVal);
|
|
292
|
+
|
|
293
|
+
c.installed = true;
|
|
294
|
+
c.mode = modeVal;
|
|
295
|
+
c.recoveryApprovals = recThr;
|
|
296
|
+
c.t3HighValueCeiling = t3Ceiling;
|
|
297
|
+
c.approvedHashRegistry = approvedHashReg;
|
|
298
|
+
|
|
299
|
+
for (uint8 t = 1; t <= 6; t++) {
|
|
300
|
+
if (thresholds[t] > 0) c.approvalsRequiredByTier[t] = thresholds[t];
|
|
301
|
+
if (timelocks[t] > 0) c.safetyDelayByTier[t] = timelocks[t];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (uint256 i; i < trustees.length; i++) {
|
|
305
|
+
address g = trustees[i];
|
|
306
|
+
if (g == address(0)) revert ZeroAddress();
|
|
307
|
+
if (c.trustees[g]) revert TrusteeAlreadyExists(g);
|
|
308
|
+
c.trustees[g] = true;
|
|
309
|
+
c.trusteeCount += 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
emit CustodyPolicyInstalled(account, modeVal, recThr);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function onUninstall(bytes calldata) external {
|
|
316
|
+
address account = msg.sender;
|
|
317
|
+
Config storage c = _configs[account];
|
|
318
|
+
if (!c.installed) revert NotInstalledOn(account);
|
|
319
|
+
c.installed = false;
|
|
320
|
+
// Audit C-11: lock against ever re-installing on this account.
|
|
321
|
+
// The mapping state (trustees, thresholds, proposals) is left
|
|
322
|
+
// in place intentionally — clearing all of it via storage
|
|
323
|
+
// iteration is unbounded gas. The `permanentlyUninstalled`
|
|
324
|
+
// flag prevents the stale state from being composed against
|
|
325
|
+
// a fresh install instead.
|
|
326
|
+
c.permanentlyUninstalled = true;
|
|
327
|
+
emit CustodyPolicyUninstalled(account);
|
|
328
|
+
emit CustodyPolicyPermanentlyUninstalled(account);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Public propose / execute / cancel ──────────────────────────
|
|
332
|
+
|
|
333
|
+
function scheduleCustodyChange(
|
|
334
|
+
address account,
|
|
335
|
+
CustodyAction action,
|
|
336
|
+
bytes calldata args,
|
|
337
|
+
bytes calldata quorumSigs
|
|
338
|
+
) external returns (uint256 changeId) {
|
|
339
|
+
Config storage c = _configs[account];
|
|
340
|
+
if (!c.installed) revert NotInstalledOn(account);
|
|
341
|
+
|
|
342
|
+
// Audit C-8: tier escalates for ChangeApprovalsRequired based on
|
|
343
|
+
// which tier is being changed + direction of change.
|
|
344
|
+
uint8 tier = _effectiveTierFor(c, action, args);
|
|
345
|
+
uint32 timelock = _safetyDelayValue(c, tier);
|
|
346
|
+
|
|
347
|
+
if ((tier == 5 || tier == 6) && timelock == 0) {
|
|
348
|
+
revert TimelockRequiredForTier(tier);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
bool isRecovery = (action == CustodyAction.RecoverAccount);
|
|
352
|
+
uint8 reqThreshold;
|
|
353
|
+
if (isRecovery) {
|
|
354
|
+
if (c.trusteeCount == 0 || c.recoveryApprovals == 0) {
|
|
355
|
+
revert RecoveryRequiresGuardianQuorum();
|
|
356
|
+
}
|
|
357
|
+
reqThreshold = c.recoveryApprovals;
|
|
358
|
+
} else {
|
|
359
|
+
reqThreshold = _approvalsValue(c, tier);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// H7-C.5 / XCON-002: aggressive block-scoping so `forge coverage`
|
|
363
|
+
// compiles without `--ir-minimum`. The verify + write halves are
|
|
364
|
+
// independent on the stack; we let each release its locals before
|
|
365
|
+
// the next runs.
|
|
366
|
+
uint64 eta;
|
|
367
|
+
changeId = ++c.nextChangeId;
|
|
368
|
+
{
|
|
369
|
+
uint64 nowTs = uint64(block.timestamp);
|
|
370
|
+
eta = uint64(nowTs + timelock);
|
|
371
|
+
c.pending[changeId] = ScheduledChange({
|
|
372
|
+
action: action,
|
|
373
|
+
args: args,
|
|
374
|
+
proposedAt: nowTs,
|
|
375
|
+
eta: eta,
|
|
376
|
+
proposer: msg.sender,
|
|
377
|
+
executed: false,
|
|
378
|
+
cancelled: false
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
{
|
|
382
|
+
ScheduleVerifyCtx memory sctx = ScheduleVerifyCtx({
|
|
383
|
+
action: action,
|
|
384
|
+
changeId: changeId,
|
|
385
|
+
reqThreshold: reqThreshold,
|
|
386
|
+
isRecovery: isRecovery
|
|
387
|
+
});
|
|
388
|
+
address[] memory propSigners = _verifyScheduleQuorum(account, c, sctx, args, quorumSigs);
|
|
389
|
+
for (uint256 i; i < propSigners.length; i++) {
|
|
390
|
+
c.proposerCustodians[changeId][propSigners[i]] = true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
emit CustodyChangeScheduled(account, changeId, action, eta, msg.sender);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function applyCustodyChange(address account, uint256 changeId, bytes calldata quorumSigs) external {
|
|
398
|
+
Config storage c = _configs[account];
|
|
399
|
+
if (!c.installed) revert NotInstalledOn(account);
|
|
400
|
+
|
|
401
|
+
ScheduledChange storage p = c.pending[changeId];
|
|
402
|
+
if (p.eta == 0) revert ProposalNotFound(changeId);
|
|
403
|
+
if (p.executed) revert ProposalAlreadyExecuted(changeId);
|
|
404
|
+
if (p.cancelled) revert ProposalAlreadyCancelled(changeId);
|
|
405
|
+
if (block.timestamp < p.eta) revert ProposalNotReady(changeId, p.eta);
|
|
406
|
+
|
|
407
|
+
bool isRecovery = (p.action == CustodyAction.RecoverAccount);
|
|
408
|
+
uint8 tier = _effectiveTierFor(c, p.action, p.args);
|
|
409
|
+
uint8 reqThreshold = isRecovery ? c.recoveryApprovals : _approvalsValue(c, tier);
|
|
410
|
+
bytes32 payloadHash = _hashExecuteRequest(account, p.action, p.args, changeId, p.eta);
|
|
411
|
+
address[] memory execSigners = _verifyQuorum(account, c, QuorumVerifyArgs({payloadHash: payloadHash, reqThreshold: reqThreshold, guardianMode: isRecovery}), quorumSigs);
|
|
412
|
+
|
|
413
|
+
if (c.mode == 3 && !isRecovery) {
|
|
414
|
+
for (uint256 i; i < execSigners.length; i++) {
|
|
415
|
+
if (c.proposerCustodians[changeId][execSigners[i]]) {
|
|
416
|
+
revert SeparationOfDutiesViolation(execSigners[i]);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
p.executed = true;
|
|
422
|
+
emit CustodyChangeApplied(account, changeId);
|
|
423
|
+
_applyCustodyChange(account, c, p.action, p.args);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function cancelScheduledChange(address account, uint256 changeId, bytes calldata quorumSigs) external {
|
|
427
|
+
Config storage c = _configs[account];
|
|
428
|
+
if (!c.installed) revert NotInstalledOn(account);
|
|
429
|
+
|
|
430
|
+
ScheduledChange storage p = c.pending[changeId];
|
|
431
|
+
if (p.eta == 0) revert ProposalNotFound(changeId);
|
|
432
|
+
if (p.executed) revert ProposalAlreadyExecuted(changeId);
|
|
433
|
+
if (p.cancelled) revert ProposalAlreadyCancelled(changeId);
|
|
434
|
+
|
|
435
|
+
bytes32 payloadHash = _hashCancelRequest(account, p.action, p.args, changeId, p.eta);
|
|
436
|
+
|
|
437
|
+
if (p.action == CustodyAction.RecoverAccount) {
|
|
438
|
+
uint64 cancelWindowEnds = p.proposedAt + RECOVERY_PRIMARY_CANCEL_WINDOW;
|
|
439
|
+
bool inOwnerCancelWindow = block.timestamp < cancelWindowEnds;
|
|
440
|
+
uint8 reqThreshold = inOwnerCancelWindow
|
|
441
|
+
? _approvalsValue(c, 4)
|
|
442
|
+
: c.recoveryApprovals;
|
|
443
|
+
_verifyQuorum(account, c, QuorumVerifyArgs({payloadHash: payloadHash, reqThreshold: reqThreshold, guardianMode: !inOwnerCancelWindow}), quorumSigs);
|
|
444
|
+
} else {
|
|
445
|
+
uint8 tier = _effectiveTierFor(c, p.action, p.args);
|
|
446
|
+
uint8 reqThreshold = _approvalsValue(c, tier);
|
|
447
|
+
_verifyQuorum(account, c, QuorumVerifyArgs({payloadHash: payloadHash, reqThreshold: reqThreshold, guardianMode: false}), quorumSigs);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
p.cancelled = true;
|
|
451
|
+
emit ScheduledChangeCancelled(account, changeId);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ─── Views ──────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
function custodyMode(address account) external view returns (uint8) {
|
|
457
|
+
return _configs[account].mode;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function approvalsRequired(address account, uint8 tier) external view returns (uint8) {
|
|
461
|
+
return _approvalsValue(_configs[account], tier);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function recoveryApprovals(address account) external view returns (uint8) {
|
|
465
|
+
return _configs[account].recoveryApprovals;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isTrustee(address account, address signer) external view returns (bool) {
|
|
469
|
+
return _configs[account].trustees[signer];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function trusteeCount(address account) external view returns (uint256) {
|
|
473
|
+
return _configs[account].trusteeCount;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function scheduledChangeCount(address account) external view returns (uint256) {
|
|
477
|
+
return _configs[account].nextChangeId;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function t3HighValueCeiling(address account) external view returns (uint256) {
|
|
481
|
+
return _configs[account].t3HighValueCeiling;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function safetyDelay(address account, uint8 tier) external view returns (uint32) {
|
|
485
|
+
return _safetyDelayValue(_configs[account], tier);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function approvedHashRegistry(address account) external view returns (address) {
|
|
489
|
+
return _configs[account].approvedHashRegistry;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function isInstalledOn(address account) external view returns (bool) {
|
|
493
|
+
return _configs[account].installed;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function getScheduledChange(address account, uint256 changeId) external view returns (
|
|
497
|
+
CustodyAction action,
|
|
498
|
+
bytes memory args,
|
|
499
|
+
uint64 proposedAt,
|
|
500
|
+
uint64 eta,
|
|
501
|
+
address proposer,
|
|
502
|
+
bool executed,
|
|
503
|
+
bool cancelled
|
|
504
|
+
) {
|
|
505
|
+
ScheduledChange storage p = _configs[account].pending[changeId];
|
|
506
|
+
return (p.action, p.args, p.proposedAt, p.eta, p.proposer, p.executed, p.cancelled);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/// @notice Pure helper exposing the default-threshold matrix from
|
|
510
|
+
/// spec § 5.1 — owners over n=1..N produce per-tier defaults.
|
|
511
|
+
function defaultApprovals(uint8 nCustodians, uint8 tier) external pure returns (uint8) {
|
|
512
|
+
return _defaultApprovals(nCustodians, tier);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ─── Internal helpers ──────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
function _tierFor(CustodyAction action) internal pure returns (uint8) {
|
|
518
|
+
if (action == CustodyAction.RecoverAccount) return 6;
|
|
519
|
+
if (
|
|
520
|
+
action == CustodyAction.ApplySystemUpdate ||
|
|
521
|
+
action == CustodyAction.RotateDelegationManager ||
|
|
522
|
+
action == CustodyAction.RotatePaymaster ||
|
|
523
|
+
action == CustodyAction.RotateSessionIssuer
|
|
524
|
+
) return 5;
|
|
525
|
+
return 4;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* @dev Audit C-8: `ChangeApprovalsRequired` must require AT LEAST the
|
|
530
|
+
* tier being modified — otherwise a T4 admin quorum could
|
|
531
|
+
* silently lower the T5 critical threshold to 1, defeating the
|
|
532
|
+
* whole layered-threshold model. Decreases also bump up one
|
|
533
|
+
* tier (require T5 for any reduction) because lowering a
|
|
534
|
+
* threshold is the security-critical direction.
|
|
535
|
+
*/
|
|
536
|
+
function _effectiveTierFor(
|
|
537
|
+
Config storage c,
|
|
538
|
+
CustodyAction action,
|
|
539
|
+
bytes memory args
|
|
540
|
+
) internal view returns (uint8) {
|
|
541
|
+
uint8 base = _tierFor(action);
|
|
542
|
+
if (action != CustodyAction.ChangeApprovalsRequired) return base;
|
|
543
|
+
// Decode (uint8 tier, uint8 newCount). Bound the change tier in
|
|
544
|
+
// [1, 5]; T6 (recovery) routes through SetRecoveryApprovals.
|
|
545
|
+
(uint8 targetTier, uint8 newCount) = abi.decode(args, (uint8, uint8));
|
|
546
|
+
if (targetTier == 0 || targetTier > 5) return base;
|
|
547
|
+
uint8 required = targetTier > base ? targetTier : base;
|
|
548
|
+
// If this would DECREASE the threshold, require at least T5
|
|
549
|
+
// authority (one tier higher than ordinary admin).
|
|
550
|
+
uint8 currentValue = c.approvalsRequiredByTier[targetTier];
|
|
551
|
+
if (currentValue == 0) currentValue = 1; // matches _approvalsValue fallback
|
|
552
|
+
if (newCount < currentValue && required < 5) required = 5;
|
|
553
|
+
return required;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function _approvalsValue(Config storage c, uint8 tier) internal view returns (uint8) {
|
|
557
|
+
if (tier == 0 || tier > 6) revert InvalidTier(tier);
|
|
558
|
+
uint8 v = c.approvalsRequiredByTier[tier];
|
|
559
|
+
return v == 0 ? 1 : v;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function _safetyDelayValue(Config storage c, uint8 tier) internal view returns (uint32) {
|
|
563
|
+
if (tier == 0 || tier > 6) revert InvalidTier(tier);
|
|
564
|
+
return c.safetyDelayByTier[tier];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/// @dev H7-C.5 / XCON-002 — scalar args packed into a memory struct so
|
|
568
|
+
/// `_verifyQuorum`'s callers stay below the via-IR-off stack limit
|
|
569
|
+
/// and `forge coverage` builds without `--ir-minimum`.
|
|
570
|
+
struct QuorumVerifyArgs {
|
|
571
|
+
bytes32 payloadHash;
|
|
572
|
+
uint8 reqThreshold;
|
|
573
|
+
bool guardianMode;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/// @dev H7-C.5 / XCON-002 — context struct packed so the verify call
|
|
577
|
+
/// from `scheduleCustodyChange` doesn't blow the stack under
|
|
578
|
+
/// coverage compile.
|
|
579
|
+
struct ScheduleVerifyCtx {
|
|
580
|
+
CustodyAction action;
|
|
581
|
+
uint256 changeId;
|
|
582
|
+
uint8 reqThreshold;
|
|
583
|
+
bool isRecovery;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function _verifyScheduleQuorum(
|
|
587
|
+
address account,
|
|
588
|
+
Config storage c,
|
|
589
|
+
ScheduleVerifyCtx memory ctx,
|
|
590
|
+
bytes calldata args,
|
|
591
|
+
bytes calldata quorumSigs
|
|
592
|
+
) internal view returns (address[] memory) {
|
|
593
|
+
bytes32 payloadHash = _hashProposeRequest(account, ctx.action, args, ctx.changeId);
|
|
594
|
+
QuorumVerifyArgs memory va = QuorumVerifyArgs({
|
|
595
|
+
payloadHash: payloadHash,
|
|
596
|
+
reqThreshold: ctx.reqThreshold,
|
|
597
|
+
guardianMode: ctx.isRecovery
|
|
598
|
+
});
|
|
599
|
+
return _verifyQuorum(account, c, va, quorumSigs);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function _verifyQuorum(
|
|
603
|
+
address account,
|
|
604
|
+
Config storage c,
|
|
605
|
+
QuorumVerifyArgs memory vargs,
|
|
606
|
+
bytes calldata signatures
|
|
607
|
+
) internal view returns (address[] memory signers) {
|
|
608
|
+
if (signatures.length < uint256(vargs.reqThreshold) * 65) {
|
|
609
|
+
revert AdminInsufficientQuorum(signatures.length / 65, vargs.reqThreshold);
|
|
610
|
+
}
|
|
611
|
+
bytes memory sigsMem = signatures;
|
|
612
|
+
address approvedHashReg = c.approvedHashRegistry;
|
|
613
|
+
signers = new address[](vargs.reqThreshold);
|
|
614
|
+
address prev;
|
|
615
|
+
for (uint256 i; i < vargs.reqThreshold; i++) {
|
|
616
|
+
address signer = SignatureSlotRecovery.recoverFromSlot(
|
|
617
|
+
vargs.payloadHash, sigsMem, i, approvedHashReg
|
|
618
|
+
);
|
|
619
|
+
if (signer <= prev) revert AdminDuplicateOrUnsortedSigner(signer);
|
|
620
|
+
prev = signer;
|
|
621
|
+
if (vargs.guardianMode) {
|
|
622
|
+
if (!c.trustees[signer]) revert UnauthorizedTrustee(signer);
|
|
623
|
+
} else {
|
|
624
|
+
if (!IAgentAccount(account).isCustodian(signer)) revert AdminUnauthorizedSigner(signer);
|
|
625
|
+
}
|
|
626
|
+
signers[i] = signer;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ─── Action dispatcher + handlers ──────────────────────────────
|
|
631
|
+
|
|
632
|
+
function _applyCustodyChange(
|
|
633
|
+
address account,
|
|
634
|
+
Config storage c,
|
|
635
|
+
CustodyAction action,
|
|
636
|
+
bytes memory args
|
|
637
|
+
) internal {
|
|
638
|
+
if (action == CustodyAction.AddCustodian) {
|
|
639
|
+
(address newOwner) = abi.decode(args, (address));
|
|
640
|
+
_execute(account, abi.encodeCall(IAgentAccount.addCustodian, (newOwner)));
|
|
641
|
+
} else if (action == CustodyAction.RemoveCustodian) {
|
|
642
|
+
(address oldOwner) = abi.decode(args, (address));
|
|
643
|
+
_execute(account, abi.encodeCall(IAgentAccount.removeCustodian, (oldOwner)));
|
|
644
|
+
} else if (action == CustodyAction.AddPasskeyCredential) {
|
|
645
|
+
// H7-C.1 / CON-WEBAUTHN-001: rpIdHash now bundled in the action args
|
|
646
|
+
// so credential additions through the custody-policy entrypoint
|
|
647
|
+
// also bind to a specific RP.
|
|
648
|
+
(bytes32 cid, uint256 x, uint256 y, bytes32 rpIdHash) =
|
|
649
|
+
abi.decode(args, (bytes32, uint256, uint256, bytes32));
|
|
650
|
+
_execute(account, abi.encodeWithSignature(
|
|
651
|
+
"addPasskey(bytes32,uint256,uint256,bytes32)",
|
|
652
|
+
cid, x, y, rpIdHash
|
|
653
|
+
));
|
|
654
|
+
} else if (action == CustodyAction.RemovePasskeyCredential) {
|
|
655
|
+
(bytes32 cid) = abi.decode(args, (bytes32));
|
|
656
|
+
_execute(account, abi.encodeWithSignature("removePasskey(bytes32)", cid));
|
|
657
|
+
} else if (action == CustodyAction.AddTrustee) {
|
|
658
|
+
(address g) = abi.decode(args, (address));
|
|
659
|
+
_applyAddGuardian(account, c, g);
|
|
660
|
+
} else if (action == CustodyAction.RemoveTrustee) {
|
|
661
|
+
(address g) = abi.decode(args, (address));
|
|
662
|
+
_applyRemoveGuardian(account, c, g);
|
|
663
|
+
} else if (action == CustodyAction.ChangeCustodyMode) {
|
|
664
|
+
(uint8 newMode) = abi.decode(args, (uint8));
|
|
665
|
+
_applyChangeMode(account, c, newMode);
|
|
666
|
+
} else if (action == CustodyAction.RotateAllCustodians) {
|
|
667
|
+
// Audit C-10: args shape changed to (addCustodians, removeCustodians).
|
|
668
|
+
// The legacy single-array form only ADDED — a compromised
|
|
669
|
+
// custodian was never actually rotated OUT. Wire-format
|
|
670
|
+
// break is acceptable in pre-alpha; the demo's only
|
|
671
|
+
// current caller (none) was using the legacy form.
|
|
672
|
+
(address[] memory addCustodians, address[] memory removeCustodians) =
|
|
673
|
+
abi.decode(args, (address[], address[]));
|
|
674
|
+
_applyRotateAllOwners(account, addCustodians, removeCustodians);
|
|
675
|
+
} else if (action == CustodyAction.ChangeValueCeiling) {
|
|
676
|
+
(uint256 newCeiling) = abi.decode(args, (uint256));
|
|
677
|
+
_applyChangeT3Ceiling(account, c, newCeiling);
|
|
678
|
+
} else if (action == CustodyAction.SetRecoveryApprovals) {
|
|
679
|
+
(uint8 newThr) = abi.decode(args, (uint8));
|
|
680
|
+
_applySetRecoveryThreshold(account, c, newThr);
|
|
681
|
+
} else if (action == CustodyAction.ChangeApprovalsRequired) {
|
|
682
|
+
(uint8 tier, uint8 newCount) = abi.decode(args, (uint8, uint8));
|
|
683
|
+
_applyChangeApprovalsRequired(account, c, tier, newCount);
|
|
684
|
+
} else if (action == CustodyAction.RecoverAccount) {
|
|
685
|
+
AgentAccountRecoveryArgs memory r = abi.decode(args, (AgentAccountRecoveryArgs));
|
|
686
|
+
_applyRecoverAccount(account, r);
|
|
687
|
+
} else if (action == CustodyAction.ApplySystemUpdate) {
|
|
688
|
+
(address newImpl) = abi.decode(args, (address));
|
|
689
|
+
_execute(account, abi.encodeWithSignature("upgradeToAndCall(address,bytes)", newImpl, ""));
|
|
690
|
+
} else if (action == CustodyAction.RotateDelegationManager) {
|
|
691
|
+
(address newDm) = abi.decode(args, (address));
|
|
692
|
+
_execute(account, abi.encodeWithSignature("setDelegationManager(address)", newDm));
|
|
693
|
+
} else if (
|
|
694
|
+
action == CustodyAction.RotatePaymaster ||
|
|
695
|
+
action == CustodyAction.RotateSessionIssuer
|
|
696
|
+
) {
|
|
697
|
+
revert CustodyActionNotYetImplemented(uint8(action));
|
|
698
|
+
} else {
|
|
699
|
+
revert InvalidCustodyAction(uint8(action));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/// @dev Single entry-point for account self-calls. The account's
|
|
704
|
+
/// `executeFromModule` does the EVM-level call; when target ==
|
|
705
|
+
/// account, msg.sender at the callee is the account itself,
|
|
706
|
+
/// satisfying onlySelf gates on `addCustodian` / `removeCustodian` / etc.
|
|
707
|
+
function _execute(address account, bytes memory data) internal {
|
|
708
|
+
// Use a low-level call so the calldata is the exact ABI-encoded
|
|
709
|
+
// call to executeFromModule, which then bubble-reverts the
|
|
710
|
+
// inner call. Solidity's high-level call generates the same
|
|
711
|
+
// shape but we keep the low-level form for explicitness.
|
|
712
|
+
(bool ok, bytes memory ret) = account.call(
|
|
713
|
+
abi.encodeWithSignature(
|
|
714
|
+
"executeFromModule(address,uint256,bytes)",
|
|
715
|
+
account,
|
|
716
|
+
uint256(0),
|
|
717
|
+
data
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
if (!ok) {
|
|
721
|
+
assembly { revert(add(ret, 32), mload(ret)) }
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function _applyAddGuardian(address account, Config storage c, address g) internal {
|
|
726
|
+
if (g == address(0)) revert ZeroAddress();
|
|
727
|
+
if (c.trustees[g]) revert TrusteeAlreadyExists(g);
|
|
728
|
+
c.trustees[g] = true;
|
|
729
|
+
c.trusteeCount += 1;
|
|
730
|
+
emit GuardianAdded(account, g);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function _applyRemoveGuardian(address account, Config storage c, address g) internal {
|
|
734
|
+
if (!c.trustees[g]) revert TrusteeDoesNotExist(g);
|
|
735
|
+
if (c.recoveryApprovals > 0 && c.trusteeCount - 1 < c.recoveryApprovals) {
|
|
736
|
+
revert RecoveryRequiresGuardians();
|
|
737
|
+
}
|
|
738
|
+
c.trustees[g] = false;
|
|
739
|
+
c.trusteeCount -= 1;
|
|
740
|
+
emit GuardianRemoved(account, g);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function _applyChangeMode(address account, Config storage c, uint8 newMode) internal {
|
|
744
|
+
if (newMode > 3) revert InvalidMode(newMode);
|
|
745
|
+
uint8 oldMode = c.mode;
|
|
746
|
+
if (newMode == 0 && c.trusteeCount > 0) revert CannotDowngradeWithTrustees();
|
|
747
|
+
c.mode = newMode;
|
|
748
|
+
emit ModeChanged(account, oldMode, newMode);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* @notice Add new custodians AND remove old ones in one ceremony.
|
|
753
|
+
* @dev Audit C-10: the previous shape only added — a "rotate" that
|
|
754
|
+
* didn't rotate. New shape is exact-replacement-friendly: pass
|
|
755
|
+
* the additions in `addCustodians` and the removals in
|
|
756
|
+
* `removeCustodians`. Validates the final custodian count
|
|
757
|
+
* stays ≥ 1 (the on-chain `removeCustodian` will revert with
|
|
758
|
+
* CannotRemoveLastCustodian if we'd zero it, but we double-check
|
|
759
|
+
* here for explicitness).
|
|
760
|
+
*/
|
|
761
|
+
function _applyRotateAllOwners(
|
|
762
|
+
address account,
|
|
763
|
+
address[] memory addCustodians,
|
|
764
|
+
address[] memory removeCustodians
|
|
765
|
+
) internal {
|
|
766
|
+
if (addCustodians.length == 0 && removeCustodians.length == 0) {
|
|
767
|
+
revert EmptyOwnerSet();
|
|
768
|
+
}
|
|
769
|
+
uint256 added;
|
|
770
|
+
for (uint256 i; i < addCustodians.length; i++) {
|
|
771
|
+
address o = addCustodians[i];
|
|
772
|
+
if (o == address(0)) revert ZeroAddress();
|
|
773
|
+
if (!IAgentAccount(account).isCustodian(o)) {
|
|
774
|
+
_execute(account, abi.encodeCall(IAgentAccount.addCustodian, (o)));
|
|
775
|
+
added++;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Remove AFTER add so we never transiently dip below count=1 in
|
|
779
|
+
// the "rotate sole custodian to a new sole custodian" case.
|
|
780
|
+
uint256 removed;
|
|
781
|
+
for (uint256 i; i < removeCustodians.length; i++) {
|
|
782
|
+
address o = removeCustodians[i];
|
|
783
|
+
if (o == address(0)) revert ZeroAddress();
|
|
784
|
+
if (IAgentAccount(account).isCustodian(o)) {
|
|
785
|
+
_execute(account, abi.encodeCall(IAgentAccount.removeCustodian, (o)));
|
|
786
|
+
removed++;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
emit OwnersRotated(account, added);
|
|
790
|
+
// Note: OwnersRotated only carries `added` for ABI compat. A
|
|
791
|
+
// future revision should add a `removed` field; that's a
|
|
792
|
+
// wire-format break we'll batch with the next event-schema rev.
|
|
793
|
+
if (removed > 0) emit CustodiansRemovedDuringRotation(account, removed);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function _applyChangeT3Ceiling(address account, Config storage c, uint256 newCeiling) internal {
|
|
797
|
+
uint256 oldCeiling = c.t3HighValueCeiling;
|
|
798
|
+
c.t3HighValueCeiling = newCeiling;
|
|
799
|
+
emit T3CeilingChanged(account, oldCeiling, newCeiling);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function _applySetRecoveryThreshold(address account, Config storage c, uint8 newThr) internal {
|
|
803
|
+
// Audit C-9: a `SetRecoveryApprovals(0)` previously disabled
|
|
804
|
+
// recovery entirely under a T4 admin quorum — quiet enough to
|
|
805
|
+
// be missed by reviewers, catastrophic if a key was lost
|
|
806
|
+
// afterwards. Refuse zero here; explicit disable-recovery
|
|
807
|
+
// semantics should live behind a separate DisableRecovery
|
|
808
|
+
// action gated at T5 + timelock (not yet wired).
|
|
809
|
+
if (newThr == 0) revert InvalidThresholdValue(newThr);
|
|
810
|
+
if (c.trusteeCount < newThr) revert RecoveryRequiresGuardians();
|
|
811
|
+
if (newThr > c.trusteeCount) revert InvalidThresholdValue(newThr);
|
|
812
|
+
uint8 oldThr = c.recoveryApprovals;
|
|
813
|
+
c.recoveryApprovals = newThr;
|
|
814
|
+
emit RecoveryThresholdChanged(account, oldThr, newThr);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/// @dev Per-tier custody-quorum threshold mutator. Tier 6 (recovery)
|
|
818
|
+
/// lives in `recoveryApprovals` instead, so it routes through
|
|
819
|
+
/// `SetRecoveryApprovals`; this surface covers T1..T5. The new
|
|
820
|
+
/// count must be ≥ 1 and ≤ the account's current custodianCount.
|
|
821
|
+
function _applyChangeApprovalsRequired(
|
|
822
|
+
address account,
|
|
823
|
+
Config storage c,
|
|
824
|
+
uint8 tier,
|
|
825
|
+
uint8 newCount
|
|
826
|
+
) internal {
|
|
827
|
+
if (tier == 0 || tier > 5) revert InvalidTier(tier);
|
|
828
|
+
if (newCount == 0) revert InvalidThresholdValue(newCount);
|
|
829
|
+
uint256 n = IAgentAccount(account).custodianCount();
|
|
830
|
+
if (uint256(newCount) > n) revert InvalidThresholdValue(newCount);
|
|
831
|
+
uint8 oldValue = c.approvalsRequiredByTier[tier];
|
|
832
|
+
if (oldValue == 0) oldValue = 1; // mirror _approvalsValue fallback
|
|
833
|
+
c.approvalsRequiredByTier[tier] = newCount;
|
|
834
|
+
emit ThresholdChanged(account, tier, oldValue, newCount);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function _applyRecoverAccount(address account, AgentAccountRecoveryArgs memory r) internal {
|
|
838
|
+
uint256 addedOwners;
|
|
839
|
+
for (uint256 i; i < r.addOwners.length; i++) {
|
|
840
|
+
address o = r.addOwners[i];
|
|
841
|
+
if (o == address(0)) revert ZeroAddress();
|
|
842
|
+
if (!IAgentAccount(account).isCustodian(o)) {
|
|
843
|
+
_execute(account, abi.encodeCall(IAgentAccount.addCustodian, (o)));
|
|
844
|
+
addedOwners++;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
uint256 addedPasskeys;
|
|
849
|
+
for (uint256 i; i < r.addPasskeys.length; i++) {
|
|
850
|
+
AgentAccountRecoveryPasskeyAdd memory pk = r.addPasskeys[i];
|
|
851
|
+
// H7-C.1 / CON-WEBAUTHN-001: addPasskey now requires rpIdHash.
|
|
852
|
+
_execute(account, abi.encodeWithSignature(
|
|
853
|
+
"addPasskey(bytes32,uint256,uint256,bytes32)",
|
|
854
|
+
pk.credentialIdDigest, pk.x, pk.y, pk.rpIdHash
|
|
855
|
+
));
|
|
856
|
+
addedPasskeys++;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
uint256 removedOwners;
|
|
860
|
+
for (uint256 i; i < r.removeOwners.length; i++) {
|
|
861
|
+
address o = r.removeOwners[i];
|
|
862
|
+
if (IAgentAccount(account).isCustodian(o)) {
|
|
863
|
+
_execute(account, abi.encodeCall(IAgentAccount.removeCustodian, (o)));
|
|
864
|
+
removedOwners++;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
uint256 removedPasskeys;
|
|
869
|
+
for (uint256 i; i < r.removePasskeyCredentialIdDigests.length; i++) {
|
|
870
|
+
bytes32 cid = r.removePasskeyCredentialIdDigests[i];
|
|
871
|
+
_execute(account, abi.encodeWithSignature("removePasskey(bytes32)", cid));
|
|
872
|
+
removedPasskeys++;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
emit AccountRecovered(account, addedOwners, removedOwners, addedPasskeys, removedPasskeys);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/// @dev Spec § 5.1 default-threshold matrix.
|
|
879
|
+
function _defaultApprovals(uint8 nCustodians, uint8 tier) internal pure returns (uint8) {
|
|
880
|
+
if (nCustodians == 0) return 0;
|
|
881
|
+
if (tier == 4) {
|
|
882
|
+
if (nCustodians <= 3) return nCustodians;
|
|
883
|
+
if (nCustodians <= 6) return nCustodians - 1;
|
|
884
|
+
return nCustodians - 2;
|
|
885
|
+
}
|
|
886
|
+
if (tier == 5) {
|
|
887
|
+
if (nCustodians <= 5) return nCustodians;
|
|
888
|
+
return nCustodians - 1;
|
|
889
|
+
}
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
892
|
+
}
|