Repository navigation
Commit e32e478
feat(contracts): TotalReclawAccount — revocable session-key signing (P3-1, P3-2, P3-4) (#636)
* feat(contracts): TotalReclawAccount — revocable session-key signing (P3-1, P3-2, P3-4)
Implements Option E Phase 3's account-model decision (D1, approved): a
`SimpleAccount` (eth-infinitism v0.7.0) subclass, `TotalReclawAccount`,
that accepts an owner-EOA signature (owner-first, byte-identical to
stock) OR a scoped, revocable session-key signature — replacing the
standalone-module design (`SessionKeyModule.sol`, never deployed, and
per the account-model memo §5 unable to pass ERC-7562 bundler
validation because its grant registry keys on `signer` rather than
`msg.sender`).
P3-1/P3-2 — `contracts/account/contracts/TotalReclawAccount.sol`:
- Constructor-less (hardcoded canonical v0.7 EntryPoint), so CREATE2
init code is bare `creationCode`.
- All new session-key state in ERC-7201 namespaced storage; zero plain
state variables added (verified — see storage-layout gate below).
- `_grantDigest`, the four EIP-712 typehash/domain constants,
`_isCallDataInScope`/`_isBatchInScope`/`_inSelectors`, `_recoverEcdsa`
(EIP-2 malleability guard), and monotonic `_minNonces` replay
protection ported VERBATIM from `SessionKeyModule.sol` per the spec's
explicit instruction.
- P3-3's `verifyingContract` → account-address binding applied as part
of the module→account move (falls out of `address(this)` now meaning
the account, not a separate module).
- Deliberate deviation from a literal "call stock's exact code" port:
the owner path uses `ECDSA.tryRecover` instead of `ECDSA.recover`
(which reverts on non-65-byte input), because a literal port would
revert the whole `validateUserOp` on every session-key lazy-install
UserOp before ever reaching the session-key branch. Reasoned through
and documented at length in the function's NatSpec — flagged in the
PR description as the one thing to scrutinize hardest.
- No custom errors: `ISessionKeyModule.sol`'s three dead errors and
`SessionKeyModule.sol`'s two more (never reverted, by the same
defect) are not carried forward; validation fails closed via
SIG_VALIDATION_FAILED and `revokeSessionKey` reuses the inherited
`onlyOwner` modifier's existing "only owner" require.
- `SessionKeyModule.sol` gains a `grantDigest` external pure wrapper so
`SessionKeyModule.t.sol`'s `_grantDigestExt` calls the production
digest path instead of a hand-copied mirror of it (the exact
duplication pattern that let the §2 `bytes4[]` selector-hashing bug
ship unnoticed).
P3-4 — tests (`contracts/account/test/`):
- `TotalReclawAccount.t.sol`: 29 fast, non-forked unit tests (fresh
ERC1967Proxy + Foundry-controlled owner) — the six executeBatch
scope-invariant tests ported from `SessionKeyModule.t.sol:424-591`,
plus owner-path/session-key/revoke/replay/grant-binding coverage, a
pinned CREATE2-address test (NOT the tautological
`test_create2_address_unchanged_after_module_install` pattern), and a
local slot-0 storage check.
- `TotalReclawAccountFork.t.sol`: 6 read-only fork tests against live
Gnosis mainnet. Upgrades the REAL, already-deployed, 66-UserOp-history
account (`0x2c0CF74B2b76110708CA431796367779e3738250`) via the owner
EOA's direct `upgradeToAndCall` (the audit-risk memo §5 escape hatch)
and asserts owner()/slot-0/EntryPoint-nonce are byte-unchanged, plus
upgrade idempotence. A second account, deployed fresh through the
real, live factory with a Foundry-controlled owner (we do not and
should not hold the real owner's key), exercises the full signature
paths: owner-signed validates, session-key lazy-install + steady-state
validates, out-of-scope is rejected, and revocation actually blocks
reuse.
- `storage-layout-diff.sh`: CI gate wiring `forge inspect ...
storageLayout` (already emitted, never consumed before this) — asserts
`TotalReclawAccount`'s storage array is byte-identical to the
`SimpleAccount` it subclasses. Currently: one entry, slot 0, `owner`,
in both.
Isolation note: `contracts/account/` is a deliberately SEPARATE Foundry
project (own `foundry.toml`/`lib`/`remappings.txt`), not new files under
the existing `contracts/` tree. Adding `account-abstraction`/
`openzeppelin-contracts` to the outer project's `lib/`+`remappings.txt`
was tried first and reverted: solc embeds the full project-wide
`settings.remappings` into EVERY compiled contract's metadata regardless
of use, which shifted `EventfulDataEdge`'s bytecode and broke
`DataEdgeStagingCreate2.t.sol`'s pinned CREATE2 address for the real
deployed staging DataEdge — a self-inflicted change to a contract this
PR has nothing to do with. Full isolation via a separate project root
was the only fix that left the outer project's compiled output
byte-for-byte unaffected (re-verified: 26/26 outer tests pass unchanged).
Test results: 35/35 passing in contracts/account (29 unit + 6 fork,
including live-Gnosis fork tests); 26/26 unchanged in the outer
contracts/ project.
Out of scope (per assignment): core Rust (P3-5), Python client (P3-6+),
SPA (P3-13), subgraph (P3-14), and P3-15 docs (CLAUDE.md/SECURITY.md).
Never deployed anything; never sent a transaction; fork tests are
read-only simulation against a public RPC.
Refs: totalreclaw-internal #621 task 4 (P3-1, P3-2, P3-4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(contracts): make TotalReclawAccount creationCode deterministic
`test_pinned_implementation_create2_address` passed locally and FAILED in
CI, predicting a different address:
CI: 0x7D12D9f0Ad437895c65F9dC7580cC712c8268066
pinned: 0x25D4E71865E8A47F010219526A618f950a8f86B4
Not a flaky test — the build was not reproducible. Foundry's default
`bytecode_hash = "ipfs"` appends a hash of the solc metadata JSON to the
bytecode, and that JSON varies with toolchain details beyond the pinned
`solc_version`, so the same source compiled in two environments produces
two different `creationCode` values and therefore two different CREATE2
addresses. Pinning `solc_version` and the optimizer settings (already
done) is necessary but not sufficient.
This matters well beyond the test. The deployment plan pins a CREATE2
address for the implementation, and a pin that depends on WHO compiled the
contract is not a pin — nobody could independently reproduce or verify the
deployed implementation, which is precisely the property an external
auditor and any user checking our deployment would rely on.
Fix: `bytecode_hash = "none"` + `cbor_metadata = false` in
contracts/account/foundry.toml. `creationCode` becomes a pure function of
(source, solc version, optimizer settings). Verified the CBOR metadata
trailer is gone from the compiled object.
Pinned address updated to the post-fix deterministic value
0x6d17938F29072B3b7E883d410076B0D73676Cf34, with the reasoning recorded on
the test so a future reader does not "fix" it by re-pinning to whatever
their own machine emits.
Trade-off accepted and noted: stripping the metadata hash makes
block-explorer verification marginally less automatic. Exact-settings
verification still works; determinism is worth more for a CREATE2 deploy.
Verified after the change: account project 29 unit + 6 fork (live Gnosis)
= 35/35; outer contracts project 26/26 unaffected.
Refs: #621 task 4, P3-1/P3-4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent c6056db commit e32e478
11 files changed
Lines changed: 2017 additions & 30 deletions
File tree
- .github/workflows
- contracts
- account
- contracts
- script
- test
- contracts
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
135 | 143 | | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
0 commit comments