Skip to content

Changes from CBSWv2 research - #46

Merged
chunter-cb merged 13 commits into
mainfrom
amie/cbswv2-research
Aug 6, 2026
Merged

Changes from CBSWv2 research#46
chunter-cb merged 13 commits into
mainfrom
amie/cbswv2-research

Conversation

@amiecorso

@amiecorso amiecorso commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Captures several changes surfaced while researching a CBSWv1 -> v2 migration on EIP-8130.

1. DefaultAccount: single-call execute

Adds execute(address,uint256,bytes) (V1-compatible selector 0xb61d27f6) alongside executeBatch, so integrations that call the V1 ABI directly (e.g. SpendPermissionManager) work against an 8130 account. Same authorization as executeBatch. DefaultAccount stays minimal, with no lock/high-rate behavior.

2. CanonicalHighRatePayerAccount: add execute

The high-rate payer account keeps its lock-respecting behavior (outbound ETH value transfers blocked while the account is locked) and now exposes the same single-call execute in addition to executeBatch, both lock-gated.

3. PolicyManager: remove executeAttested; policies are 8130-native only

Removes the executeAttested entrypoint. execute resolves the acting actorId solely from the EIP-8130 transaction-context precompile; where the precompile is absent the acting actorId is 0 and the call reverts NoActivePolicy. There is deliberately no ERC-4337 / off-8130 policy path: a policy-gated session key carrying a PAYER scope cannot be safely confined under 4337.

4. Rename AccountConfiguration -> Keystore

Renames the registry contract to Keystore throughout: contract + file (src/Keystore.sol), the KeystoreTest test base, the test/unit/Keystore/ directory, all type/import references, ACCOUNT_CONFIGURATION -> KEYSTORE, accountConfiguration -> keystore, and NatSpec/README prose. The EIP-8130 title "Account Abstraction by Account Configuration" is left unchanged.

Testing

forge test: 333 pass, 0 fail. forge fmt --check clean. Deploy.s.sol / SmokeTest.s.sol compile.

Unreviewed prototype for team review.

amiecorso and others added 10 commits July 28, 2026 11:03
- DefaultAccount: add single-call execute(address,uint256,bytes) (V1-compatible selector 0xb61d27f6)
- CanonicalHighRatePayerAccount: lock-gate the new execute() outbound value transfer
- PolicyManager: remove executeAttested; resolve the acting actorId from the tx-context
  precompile with a fallback to the account's own ITransactionContext surface, and enforce
  actor expiry on execute()
- Add EIP7702ProxyFor8130: minimal EIP-8130-native 7702 delegation proxy (ported from
  base-account-v2), registry-gated setImplementation + default-implementation fallback,
  no EOA-key override

Unreviewed prototype for team review.
- Require the resolved actor be gated to this manager (getPolicyManager == address(this)),
  mirroring the external path; closes an off-8130 confinement gap
- Reject actorId == 0 explicitly (the "no acting actor" sentinel)
- Require exact 32-byte returns in _resolveActorId so malformed data degrades to "no actor"
  instead of reverting decode
- Document the off-8130 account-side trust assumption in NatSpec
- Tests: add manager-mismatch rejection + execute() reentrancy regression coverage
…tePayerAccount

- DefaultAccount now blocks outbound value transfers while locked (execute + executeBatch)
  via a shared _isLocked() helper, with an AccountLocked error — high-rate-payer-safe by default
- Remove CanonicalHighRatePayerAccount: its lock-gate now lives in the base account. High-rate
  admission is ultimately a node allowlist, so an immutable ERC-1167 clone of DefaultAccount (or
  another allowlisted implementation) is the admission path; an immutable proxy can be added later
  if ever needed
- Deploy.s.sol: deploy only DefaultAccount and log its ERC-1167 match bytecode for high-rate payers
- README: document the single lock-respecting account + allowlist-based admission
- Tests: fold lock-gating + execute() coverage into DefaultAccount.t.sol; drop the
  CanonicalHighRatePayerAccount suite
A policy-gated session key carrying a PAYER scope cannot be safely confined under
ERC-4337, so policies are supported ONLY via native EIP-8130 protocol dispatch.

- Remove the account-exposed ITransactionContext fallback in actor resolution: execute()
  reads the transaction-context precompile only; where it is absent the acting actorId is 0
  and the call reverts NoActivePolicy (it just fails)
- Revert execute() to the protocol-only form: drop the fallback-motivated manager-match,
  actorId==0, and local expiry checks (the 8130 protocol gate already guarantees these on the
  only reachable path)
- Restore _actingActorId (precompile-only); document that there is deliberately no
  account-attested / 4337 policy path
- Tests: drop the account-fallback / precedence / expiry / manager-match cases; keep the
  execute() reentrancy regression test
…o-EOA-fallback invariant

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
…comment

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
…r works on any chain

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
@amiecorso
amiecorso requested a review from chunter-cb July 29, 2026 21:51
@amiecorso
amiecorso marked this pull request as ready for review July 29, 2026 21:51
amiecorso and others added 3 commits July 30, 2026 17:37
A policy-gated (confined) actor may no longer also hold a payer scope
(SCOPE_SELF_PAYER or SCOPE_SPONSOR_PAYER). Payer authority lets a key
spend the account's funds on gas, and that outflow cannot be metered or
capped per-key, so a confined key with payer scope could drain gas
regardless of its policy gate. Its gas must be covered by a separate,
non-policy payer or a paymaster.

Enforced in _authorizeActor (the single authorization chokepoint), so it
applies to createAccount, importAccount, and applySignedActorChanges
alike. New error PolicyActorCannotBePayer. Updates the SCOPE_POLICY and
_authorizeActor docs, which previously stated no scope combinations were
rejected.

Tests: replaced authorizePolicyActor_allowsAnyScopeCombination with
rejectsPayerScope (non-payer combos succeed; SELF_PAYER|POLICY and
SPONSOR_PAYER|POLICY revert); masked payer bits out of the fuzzed gated
scope in the policy-accessor and authenticate suites. 331 tests pass;
fmt clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
…t high-rate, drop 7702 proxy

- Restore CanonicalHighRatePayerAccount with both executeBatch and execute lock-gated overrides
  (keeps high-rate behavior on the dedicated account and gives it the execute interface)
- Revert DefaultAccount to a plain single-call execute; remove all high-rate/lock specifics
  (AccountLocked error, _isLocked, value>0 lock checks, high-rate NatSpec)
- Remove EIP7702ProxyFor8130 (contract + test)
- Restore Deploy.s.sol and README to the two-account layout
- Tests: move execute lock-gating coverage back onto CanonicalHighRatePayerAccount.t.sol; keep
  plain execute coverage on DefaultAccount.t.sol

Unchanged: executeAttested stays removed; PolicyManager and the AccountConfiguration
payer-scope rejection are untouched.
Rename:
- Contract AccountConfiguration -> Keystore (src/Keystore.sol)
- Test base AccountConfigurationTest -> KeystoreTest (test/lib/KeystoreTest.sol)
- Test dir test/unit/AccountConfiguration/ -> test/unit/Keystore/
- All type/import references, plus ACCOUNT_CONFIGURATION -> KEYSTORE and
  accountConfiguration -> keystore, and NatSpec/README prose. The EIP-8130 title
  "Account Abstraction by Account Configuration" in the README is intentionally left unchanged.

Revert:
- Undo "reject payer scope on policy-gated actors" (former 77793e5); Keystore no longer
  rejects SCOPE_POLICY combined with a payer scope.
/// @param value Wei forwarded with the call.
/// @param data Calldata passed to `target`.
function execute(address target, uint256 value, bytes calldata data) external virtual {
if (!_isAuthorizedCaller(msg.sender)) revert UnauthorizedCaller();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will consider a follow up PR to bubble up errors here too. Tracking

@chunter-cb
chunter-cb merged commit abee952 into main Aug 6, 2026
5 checks passed
@chunter-cb
chunter-cb deleted the amie/cbswv2-research branch August 6, 2026 14:03
chunter-cb added a commit that referenced this pull request Aug 6, 2026
…mpty, drop public isActor

Rebased onto main (post-#46 Keystore rename / single-call execute) and resliced to three changes:

- Widen actor scope uint8 -> uint16 and reorder ActorConfig/InitialActor/AccountState to the
  normative slot layout authenticator ‖ expiry ‖ scope ‖ reserved. Updated the ACTOR*_TYPEHASH
  strings, the import digest / actors-commitment encodings, and the ActorAuthorized event packing
  to match.
- getActorConfig resolves an expired actor (non-zero expiry, block.timestamp past it) to the
  all-zero config, identical to an unknown actor. PolicyManager's external-path gate becomes a
  presence check (_requireActiveActor) since expiry is no longer observable via getActorConfig.
- Drop the public isActor getter; keep an internal _isActor for the revoke path. Tests read
  liveness via getActorConfig(account, actorId).authenticator != 0.
chunter-cb added a commit that referenced this pull request Aug 6, 2026
… public isActor (#55)

* refactor: uint16 scope + normative slot layout, expired actors read empty, drop public isActor

Rebased onto main (post-#46 Keystore rename / single-call execute) and resliced to three changes:

- Widen actor scope uint8 -> uint16 and reorder ActorConfig/InitialActor/AccountState to the
  normative slot layout authenticator ‖ expiry ‖ scope ‖ reserved. Updated the ACTOR*_TYPEHASH
  strings, the import digest / actors-commitment encodings, and the ActorAuthorized event packing
  to match.
- getActorConfig resolves an expired actor (non-zero expiry, block.timestamp past it) to the
  all-zero config, identical to an unknown actor. PolicyManager's external-path gate becomes a
  presence check (_requireActiveActor) since expiry is no longer observable via getActorConfig.
- Drop the public isActor getter; keep an internal _isActor for the revoke path. Tests read
  liveness via getActorConfig(account, actorId).authenticator != 0.

* refactor: drop unused policyTarget from authenticateActor return

authenticateActor now returns (actorId, scope) only. policyTarget was
never consumed by any caller; the policy manager is resolved separately
via getPolicyManager (an execution-time read). Removes the conditional
policy-manager SLOAD from the auth path and the now-dead _policyTargetFor
helper. Tests updated to resolve the manager via getPolicyManager.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants