Evm automation feature implementation - #23
Conversation
* add customization required for evm automation * rm some spaces and cleanup
… use it in other places
-added test cases for deployContract
…e_to_revm Add tx hash precompile to revm at 0x53555001
-removed BlockBasedCounter -updated BlockMeta with deregister and enumerableSet -updated testcases
* added test cases * added tests for CoreFacet * added test cases * updated imports * addded test cases for RegistryFacet and CoreFacet * - resolved PR comments - fixed system gas committed in dropOrChargeTask - added task duration param to registerUst and registerGst
- Update genesis transaction generator to allow custom contracts injection to genesis set from application layer - Made build scripts solidity package compilation logic generic to be utilized from other packages as well Co-authored-by: Aregnaz Harutyunyan <>
…s well (#32) Relates: Entropy-Foundation/smr-moonshot#2908 Co-authored-by: Aregnaz Harutyunyan <>
isaacdoidge
left a comment
There was a problem hiding this comment.
Production-readiness review of the whole PR (reviewed at 47cefd8, the commit smr-moonshot actually pins — tree-identical to this head). Split into three parts: the core revm engine delta, the supra-extension Rust crate, and the genesis Solidity contracts. Inline comments below; process/summary points here.
Most important finding — a fund-loss bug in the genesis contracts (Critical): LibAccounting narrows uint128 cycle-fee amounts to uint64 on the refund/unlock paths. Since the token is 18-decimal, any cycle fee above ~18.4 SUPRA is silently truncated on refund and the locked-fee accounting is corrupted. This fires on ordinary governance actions (disabling automation) and stopTasks. Details inline on LibAccounting.sol. This alone blocks baking these contracts into production genesis.
Consensus-safety verdict (good news): I traced the two cross-layer risks the core-engine review raised, into smr-moonshot:
- The
tx_hashfield is not user-forgeable on the consensus path:EvmExecutor::convert_to_tx_envalways sets it totxn.digest_as_b256()(the canonical computed digest) for every tx type, so the TxHash precompile returns a trustworthy hash. Only the RPC simulation path can see a zero/placeholder hash, which is harmless. - For the default
Userexecution mode, the engine delta is behaviorally equivalent to upstream revm v29 (gas, nonce, balance, refunds, EIP-3607 all gated behindExecutionMode, defaultUser). The one un-gated new rule isvalidate_caller(comment inline) — benign in practice but a genuine new consensus rule with a stringly-typed error.
Reproducible-genesis blocker (same root cause as my smr-moonshot bluealloy#2791 comment): the Solidity toolchain is unpinned — floating pragmas, no solc_version, default bytecode_hash=ipfs metadata — so independently-built validators can produce byte-different genesis bytecode and different CREATE2 addresses. Comment on foundry.toml.
Dependency hygiene: Cargo.toml [patch.crates-io] points gmp-mpfr-sys at a moving branch=master ssh ref; and smr-moonshot consumes this repo via branch = task/issue-2908 rather than a tag (flagged on the smr-moonshot PRs). Cut an immutable tag for the release.
Open review threads: most of your existing threads are answered; the still-relevant ones are the tx_hash end-user-settability question (resolved above — enforced node-side), the ByPass→Bypass rename, and the doc typos. The .gitmodules private-submodule access question also applies to the smr-moonshot build.
Verified sound: automation transaction-construction (automated_transaction/automation_record/block_metadata) is deterministic (BTreeMaps, no clock/RNG on the hot path) and panic-free on the execution path; Diamond deployment is atomic (no uninit-proxy window); AppStorage slot-0 pattern avoids facet storage collisions; the privileged Solidity surface (diamondCut, config setters, mint/burn, BlockMeta admin) is consistently owner/VM-signer gated.
| _task.taskIndex, | ||
| _task.owner, | ||
| registryState.cycleLockedFees, | ||
| uint64(_refundFee) |
There was a problem hiding this comment.
[Critical — fund loss + accounting corruption] uint64(_refundFee) truncates a uint128 cycle fee. safeFeeRefund then uses that truncated value for both the payout (safeRefund(..., _refundableFee, ...)) and the cycleLockedFees decrement. With an 18-decimal token, uint64 max ≈ 18.4 SUPRA, so any task whose cycle fee exceeds that (a long-lived / high-gas task) is refunded only fee mod 2^64 — the owner loses the rest — and cycleLockedFees is left over-locked forever.
Scenario: governance disables automation → refundTaskFees runs for every task → a user owed 100 SUPRA receives 100e18 mod 2^64 (a tiny fraction), irrecoverably. Fix: thread cycle-fee amounts as uint128/uint256 end-to-end and drop the uint64 narrowing (same at line 403 in unlockDepositAndCycleFee, where the mismatch is even more direct — payout uses the untruncated cycleFeeRefund while the unlock uses uint64(cycleLockedFeeForTask)).
|
|
||
| if (cycleLockedFeeForTask < cycleFeeRefund) { revert IRegistryFacet.InvalidCycleRefundFee(); } | ||
|
|
||
| (bool hasLockedFee, uint256 remainingCycleLockedFees ) = safeUnlockLockedCycleFee(registryState.cycleLockedFees, uint64(cycleLockedFeeForTask), _taskIndex); |
There was a problem hiding this comment.
[Critical — paired with the line-226 finding] uint64(cycleLockedFeeForTask) truncates the uint128 locked cycle fee when unlocking on stopTasks, while the actual refund paid out (cycleFeeRefund, derived from taskFeeForResidualTime / REFUND_FACTOR) is not truncated. For a locked fee above ~18.4 SUPRA these diverge: cycleLockedFees is decremented by the wrong amount (permanent accounting corruption that withdrawFees relies on), or the operation reverts and the user cannot stop their task. Same fix — carry the fee as uint128/uint256.
| * @dev Function to remove existing owners from the wallet. | ||
| * @param _owners Array of existing owner addresses to be removed. | ||
| */ | ||
| function removeOwners(address[] memory _owners) external { |
There was a problem hiding this comment.
[High — threshold bypass] removeOwners removes owners from the owners set but never decrements transaction.numConfirmations nor purges confirmations[_txIndex] for pending transactions, and executeTransaction only checks numConfirmations >= numConfirmationsRequired without re-verifying each confirmer is still an owner. So confirmations from removed owners still count toward quorum. Since this multisig is the governance root for the Diamond, ERC20Supra, and BlockMeta, a rotated-out / compromised key's stale confirmation can still help execute privileged actions. Purge pending confirmations from removed owners (and re-derive numConfirmations), or re-validate owner status at execution time.
| @@ -0,0 +1,24 @@ | |||
| [profile.default] | |||
There was a problem hiding this comment.
[High — non-reproducible genesis] No solc_version is pinned, pragmas float (^0.8.0, ^0.8.27), and bytecode_hash/cbor_metadata are left at defaults (ipfs metadata trailer). build.rs embeds the compiled bytecode via include_bytes! and it becomes genesis state with CREATE2-derived addresses. Two validators building from source at different times can get different solc patch releases / metadata and thus different genesis bytecode and deploy addresses → divergent genesis state root. Pin an exact solc_version, set bytecode_hash = "none" and cbor_metadata = false, and freeze the pragmas. (Lib deps are already pinned via foundry.lock — good.)
| /// Validation against state is done later in pre-execution phase in deduct_caller function. | ||
| #[inline] | ||
| fn validate(&self, evm: &mut Self::Evm) -> Result<InitialAndFloorGas, Self::Error> { | ||
| self.validate_caller(evm)?; |
There was a problem hiding this comment.
[Medium] validate() calls validate_caller unconditionally, so the SUPRA-reserved-address rejection runs for every transaction including plain User ones (confirmed: node's transact_one routes User txns through handler.run → this validate). It's benign in practice (no real user controls a key in the 0x…535550xx band) but it is a permanent new consensus rule diverging from upstream/Ethereum, and it's ordered before validate_env, changing error precedence. Also the error is from_string(...) (there's a // TODO create InvalidTransaction variant), so eth tooling can't classify it. Please add a typed InvalidTransaction variant and document the reserved band as an intentional consensus rule.
| /// Transaction hash (32 bytes). | ||
| /// | ||
| /// Note : Common field for all transactions. | ||
| fn tx_hash(&self) -> B256; |
There was a problem hiding this comment.
Resolving your earlier question here: I traced the enforcement into smr-moonshot. EvmExecutor::convert_to_tx_env (consensus/execution/src/evm/executor.rs) sets tx_hash to txn.digest_as_b256() — the canonical computed digest — for User and every automation variant, so a user cannot inject a chosen tx_hash into consensus execution and the TxHash precompile returns a trustworthy value. Recommend a comment here stating the field is populated only by the app→execution conversion (never from inbound user bytes), and ideally making the TxEnvBuilder::tx_hash setter conversion-only, so this invariant can't regress.
| const CONTRACT_BYTECODES_RAW: &[u8] = | ||
| include_bytes!(concat!(env!("OUT_DIR"), "/supra_contracts_bytecode.bin")); | ||
|
|
||
| const CONTRACT_BYTECODES: Lazy<BTreeMap<String, Vec<u8>>> = Lazy::new(|| { |
There was a problem hiding this comment.
[Medium] const CONTRACT_BYTECODES: Lazy<...> = Lazy::new(...) — a const is inlined at every use site, so each load_contract_bytecode call constructs a fresh Lazy and re-runs the bincode deserialization of the whole bytecode blob; the caching Lazy provides never actually caches. It also trips clippy::declare_interior_mutable_const. Make it a static.
| let project = foundry_config.project()?; | ||
|
|
||
| let output = project.compile()?; | ||
| let _ = output.succeeded(); |
There was a problem hiding this comment.
[Medium] let _ = output.succeeded(); discards the Solidity compile status — a solc failure won't fail the build here, it only surfaces later as a missing-artifact error for whichever contract names are looked up, so a partially-failed compile can slip through and produce incomplete genesis. Propagate a hard error when !output.succeeded().
There was a problem hiding this comment.
Internally the call checks and asserts the success, so the errors will cause early termination on this call.
| # For more details see crates/supra-extension/build.rs | ||
| #forge = { git = "https://github.com/foundry-rs/foundry.git", tag="v1.4.1"} | ||
| #alloy-chains = { version = "0.2.13" } | ||
| #shlex = { version = "1.3.0" } |
There was a problem hiding this comment.
[Low] Two things on the dep graph: (1) the [patch.crates-io] gmp-mpfr-sys = { git = ..., branch = "master" } uses a moving branch ref in a consensus-critical build — pin to a rev/tag. (2) The build/codegen deps kept "for regenerating bindings" (your reply on this thread) — please confirm they're [build-dependencies]/dev-only and not pulled into the node's runtime dependency graph. And cut a release tag for this repo so smr-moonshot can pin to it instead of branch = task/issue-2908.
There was a problem hiding this comment.
This comment will be solved when branch is ready to be tagged for the final release.
| } | ||
|
|
||
| /// @notice Calls all registered functions for the targets. | ||
| function blockPrologue() external { |
There was a problem hiding this comment.
[Medium — block-production robustness] blockPrologue does target.call(abi.encodePacked(selector)), forwarding all remaining gas with no per-call cap. Reverts are caught (so one bad target can't halt the block — good), but a registered target with an expensive/unbounded loop can drain the prologue's gas budget every block on every validator. Registration is onlyOwner so it's a governance-config hazard, not an open attack, but a per-call gas ceiling would harden this system hook against a mis-registered or upgraded-malicious target.
There was a problem hiding this comment.
Will be fixed in a separate PR. I think introducing user specified gas-cap for each registered entry will be more appropriate rather to have it fixed for each call. and the contract may have total cap which should not exceed by the sum caps of registered entries.
@udityadav-supraoracles , can you please follow up on this.
isaacdoidge
left a comment
There was a problem hiding this comment.
One more item from the deeper pass on the cross-boundary types (follow-up to the main review).
| #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] | ||
| #[repr(u8)] | ||
| /// Automated transaction type corresponding automation task type. | ||
| pub enum AutomatedTransactionType { |
There was a problem hiding this comment.
[Medium — forward-compat wire break] AutomatedTransactionType (and AutomationTaskPredicate above, plus the automation_record.rs / block_metadata.rs types) are bcs-serialized and persisted / re-exported to the node, but they carry no version discriminant and rely on declaration order for their bcs variant indices with no explicit index pinning. Round-trip is correct today, but a future PR that inserts or reorders a variant (or reorders a struct field) silently changes the wire layout and mis-decodes already-persisted/on-chain data — e.g. adding a variant before GST makes every stored GST transaction decode as the new variant. Since smr-moonshot persists these, please add a version field (or pin variant indices) on the cross-boundary types before they carry production data. Note the parallel v1 API concern I raised on smr-moonshot bluealloy#2791 (EvmAutomationPayloadVersioned using untagged serde).
isaacdoidge
left a comment
There was a problem hiding this comment.
One more from a second core-engine pass (verified against the diff).
| context | ||
| .journal_mut() | ||
| .nonce_bump_journal_entry(inputs.caller); | ||
| if should_update_nonce { |
There was a problem hiding this comment.
[Medium — confirm invariant] Nonce-based CREATE address collision in non-nonce-updating modes. make_create_frame derives created_address from old_nonce (inputs.caller.create(old_nonce)) but only increments the caller nonce when should_update_nonce, which is false for Automated/AutomatedGasless/System/ReadOnly. So if a transaction in one of those modes performs two nonce-based CREATE deployments from the same caller, both resolve to the same address and the second hits a CreateCollision, diverging from standard EVM semantics (where each CREATE advances the nonce). It's deterministic — the mode is in the committed CfgEnv, so it can't fork the chain — but it is a latent correctness hazard. Please confirm automated/system transactions never perform nonce-based CREATE (CREATE2 is unaffected), or derive the address from a locally-incremented counter even when the persisted nonce is frozen.
…33) * Addressed review comments left on main feature/evm_automation branch * Addressed followup comments --------- Co-authored-by: Aregnaz Harutyunyan <>
Co-authored-by: Aregnaz Harutyunyan <>
* added changes for gas cap for execution entry * added test cases and input validation * fix(evm-automation): validate genesis configs and close gas-budget gaps BlockMeta's gas-cap feature added a per-entry gas limit and an overall blockPrologueGasCap, but nothing validated that generated genesis configs or deployment scripts actually respected those limits, and one duplicate check was fragile to gas-bit contamination: - Add is_valid() to AutomationRegistryConfigV1, AutomationRegistryConfig, and GenesisTransactionGeneratorConfig so malformed configs (zero caps, cycle duration exceeding task duration, threshold above owner count, task capacity exceeding the benchmarked MAX_SUPPORTED_AUTOMATION_TASK, etc.) are caught before generating non-failable genesis transactions. - Fix AutomationRegistryConfigV1::default() to stay within MAX_SUPPORTED_AUTOMATION_TASK (task_capacity 160 + sys_task_capacity 40 = 200) so the shipped default passes its own validation. - Thread block_prologue_gas_cap through GenesisTransactionGeneratorConfig into BlockMeta::initialize, matching the contract's new signature. - Harden BlockMeta.checkDuplicate to mask the gas-limit bits on both sides of the comparison, so callers no longer need to remember to pre-mask their argument. - Align GovActions.s.sol's governance registration gas limit (100_000) with DeployBlockMeta.s.sol's, so both registrations fit under the 1_000_000 blockPrologueGasCap instead of overflowing it. - Add unit tests covering the new validation logic and the gas-cap threading into the encoded initialize() call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(block-meta): widen gas-cap accumulation and close genesis validation gaps totalGasAllocated + gasLimit (register) and the running total in updateExecutionOrder were accumulated as uint64. Once the sum approached type(uint64).max it overflowed, and since Solidity 0.8+ checks arithmetic by default, the call reverted with the generic Panic(0x11) instead of the intended GasCapExceeded() custom error. Both call sites are owner-only, so this was not exploitable, but it produced a confusing error instead of the documented one. Widen both accumulations to uint256 before comparing against the uint64 cap so the check can never overflow and always surfaces GasCapExceeded() when the cap is hit. Also add regression tests: verify updateExecutionOrder resets (rather than accumulates onto) totalGasAllocated when replacing an existing execution order, and verify register succeeds again after deregister frees budget under a fully consumed cap. Round out the same gas-budget/validation effort on the Rust and tooling side: - configs.rs: rename MAX_SUPPORTED_AUTOMATION_TASK(S) typo, fix missing spaces in two error messages, correct the congestion threshold error text to match the actual `> 100` check, and use saturating_add when comparing sys_task_capacity + task_capacity against the cap so two u16 values near the max can't wrap around and slip past validation. - generator.rs: call config.is_valid() before building genesis transactions, so an invalid GenesisTransactionGeneratorConfig is rejected up front instead of producing failable transactions. - GovActions.s.sol: read the monitorCycleEnd() registration gas limit from a new SELECTOR_GAS_LIMIT env var instead of hardcoding 100_000, with a comment noting a value above blockPrologueGasCap will fail the registration tx. - IBlockMeta.sol: drop indexed from SelectorDeregistered's gasLimit param to match SelectorRegistered and avoid an unnecessary topic for a non-filterable uint64. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* perf(automation): use a plain array for expectedTasksToBeProcessed onCycleEndInternal's gas cost scales linearly with registered task count, and was dominated by populating expectedTasksToBeProcessed as an EnumerableSet.UintSet: each add() costs two 20,000-gas SSTOREs (the value array push, plus an O(1)-lookup index mapping entry). That mapping was never actually queried anywhere in the codebase - the field is only ever read back sequentially via length()/at() - so it was pure overhead. Switching the field to a plain uint256[] halves the per-task SSTORE cost (~40-45k -> ~22k gas/task measured via MonitorCycleEndGas.t.sol), raising the safe task-registry capacity within the 16,777,216 block-prologue gas budget from ~364 to ~731 tasks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Addressed review comments --------- Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* added fix to not consider removed owners confirmation * removed _clearOwnerConfirmations and updated view functions * updated isConfirmed
…an earlier tx's touch (#38) * fix(journal): don't let a later ReadOnly tx discard an earlier tx's touch `caller_accounting_journal_entry` unconditionally pushed an `AccountTouched` journal entry on every transaction, even when the account was already touched by an earlier, already-committed transaction in the same batch. Since `Touched` is a sticky, block-scoped flag and `AccountTouched::revert()` unconditionally clears it, discarding a later `ExecutionMode::ReadOnly` transaction (e.g. an automation-task predicate check) that touched the same account would incorrectly un-touch it. State-diff builders such as `CacheState::apply_account_state` skip untouched accounts entirely, so the earlier transaction's committed nonce/balance change was silently dropped from the persisted output. Route the touch through the existing guarded `touch()` helper instead of pushing unconditionally, and remove the redundant raw `mark_touch()` calls that ran immediately before it in `pre_execution.rs` and `op-revm/handler.rs` — those calls would otherwise flip the flag before the guard is checked, defeating it for every transaction (not just a second one) and reintroducing a different bug where a genuinely first-time touch that is itself discarded would incorrectly stay touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(journal): preserve EIP-2200/EIP-6780 semantics across multi-tx journal reuse revm can reuse a single Journal across many transactions in a block (commit_tx()/discard_tx() between them, one finalize() at the end), which previously caused two consensus-relevant bugs when a later transaction touched an account an earlier, already-committed transaction had modified: - EIP-2200/3529: EvmStorageSlot.original_value was only ever set once, at the slot's first load, and never refreshed at the transaction boundary. A transaction reusing a slot written by an earlier committed transaction would compute SSTORE gas refunds against the pre-block value instead of the value at the start of the current transaction, as EIP-2200 requires. Fixed by refreshing original_value in sload_with_account's Occupied branch, keyed off a transaction_id mismatch (not the generic is_cold flag, since is_cold can also flip from an in-tx revert re-marking a slot cold without crossing a transaction boundary). - EIP-6780: a contract created and selfdestructed in one transaction, then recreated by a later transaction in the same block without being destroyed again, stayed flagged as globally selfdestructed. State-diff builders (CacheState::apply_account_state, CacheDB::commit) check that flag before checking is_created, so the later transaction's live contract was silently wiped from the final result. Fixed by clearing the global SelfDestructed flag in load_account_optional's lazy cold-load wipe, at the exact point the physical wipe already happens - this is the same non-revertible, fact-resolving step that already handles the account's local flags, so it adds no cost to the finalize-per-tx (transact()) execution pattern where the bug never occurs. Also fixes the same unjournaled-mutation bug class in apply_eip7702_auth_list: the authority account's code/code_hash/nonce were mutated without journal entries, so a discarded ExecutionMode::ReadOnly transaction (e.g. an automation-task predicate check) would leak the mutation into subsequent transactions in the batch, undetected by has_state_mutations() since it only inspects journal entries. Both the code change and the nonce bump are now journaled via set_code_with_hash and nonce_bump_journal_entry. Adds regression tests covering: original_value refresh across transaction boundaries (and confirming the finalize-per-tx pattern was never affected); the create->destroy->recreate chain and several variations (destroy->recreate->destroy, cross-tx selfdestruct not fully deleting, recreate attempts correctly still colliding, intervening non-recreating touches, multi-cycle chains, and balance carried across a recreate); and the EIP-7702 ReadOnly auth-list leak. Corrects a pre-existing ee-tests assertion and two golden-file fixtures that had encoded the original recreate-after-selfdestruct bug as expected behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(journal): unify AccountStatus bookkeeping across execution modes; drop unused Solidity local crates/context/src/journal/inner.rs / crates/ee-tests/src/revm_tests.rs: Extends the recreate-after-selfdestruct fix so an account's internal AccountStatus bookkeeping is identical whether a block's transactions share one journal (commit_tx() between them) or each run in its own finalize-per-tx session. The lazy cold-load wipe in load_account_optional already cleared the stale global SelfDestructed flag when a later transaction touches an account destroyed by an earlier one; it now also clears the global Created flag the same way, since that bit's only real consumer (the is_newly_created DB-skip optimization in sload_with_account) stays correct regardless - the account's in-memory storage is already cleared by the same wipe. Without this, a shared journal retained a stale Created bit that a fresh, standalone session would never have set, even though both executions produce identical observable behavior. Adds two regression tests (single-journal and standalone-journal variants of create->destroy->call) asserting the resulting AccountStatus is now byte-identical between the two execution modes. solidity/supra_contracts/src/MultiSignatureWallet.sol: Removes an unused local (Transaction storage transaction) from hasValidNumberOfConfirmations - the function never reads it, only confirmations/owners. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Added more tests * fix(journal): keep COINBASE/precompiles warm across multi-tx journal reuse crates/context/src/journal/inner.rs: Fixes a gas-consensus bug reported against the single-journal, multi-tx execution mode: EIP-3651 requires COINBASE (and EIP-2929 requires precompiles) to be warm at the start of every transaction, but load_account_optional's Occupied branch only checked an account's own stale transaction_id stamp to decide warmth - once an earlier transaction inserted COINBASE into the shared `state` map, every later transaction paid a full cold access (2600 gas) instead of the guaranteed warm 100 gas, since the perpetually-pre-warmed `warm_addresses` set (which the Vacant branch already consulted) was never checked once the account became Occupied. Splits the cold/warm decision into two signals: `is_new_tx_touch` (still drives the existing EIP-6780 lazy cold-load wipe, unaffected) and the actual `is_cold` returned to the caller, which now also checks `warm_addresses` so COINBASE/precompiles stay correctly warm across every transaction sharing the journal. crates/ee-tests/src/coinbase_scratch_test.rs (new): Regression test reproducing the reported deviation (two consecutive calls doing BALANCE on COINBASE previously differed by exactly 2500 gas - COLD_ACCOUNT_ACCESS_COST minus WARM_STORAGE_READ_COST). A companion test guards against the fix over-broadening: an ordinary address (not COINBASE, not a precompile) must still correctly reset to cold at the start of every transaction, per standard EIP-2929 semantics. crates/ee-tests/src/access_list_scratch.rs (new): Confirms EIP-2930 access-list warming is unaffected by the fix, since access-listed addresses are never added to the perpetually-warm set: a transaction without its own access list still pays the full cold cost for an address an earlier transaction's access list had listed, and the access-listing transaction itself pays exactly the expected net discount (opcode goes warm, minus the upfront per-address declaration cost). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(journal): restore prior code on CodeChange revert; scope selfdestruct-flag clearing to journal-created accounts Addresses PR review feedback on the multi-tx journal reuse fixes. crates/context/src/journal/entry.rs, crates/context/src/journal/inner.rs: - JournalEntry::CodeChange previously reverted unconditionally to code_hash = KECCAK_EMPTY, code = None. That's correct for CREATE (whose collision check requires the target to already be empty) but wrong for EIP-7702: step 5 of the spec explicitly permits re-delegating an authority that already holds a delegation, so the value being reverted-to is not necessarily empty. A discarded ReadOnly re-delegation of an already-delegated authority would wipe its real delegation instead of restoring it. CodeChange now carries the previous code_hash/code, captured in set_code_with_hash before overwriting, and revert() restores those exact values. JournalEntryTr::code_changed's signature grew the two extra parameters; JournalEntry remains its only implementer. - The lazy cold-load wipe's global SelfDestructed/Created clearing (introduced to fix the recreate-after-selfdestruct bug) was gated on is_selfdestructed_locally(), which selfdestruct() also sets pre-Cancun for ANY destroy, not just a same-tx creation - a cross-tx destroy of a pre-existing contract would have its permanent-destruction flag incorrectly cleared by a later transaction merely touching the address, letting apply_account_state/CacheDB::commit skip wiping that contract's real on-disk storage. Gating on spec.is_enabled_in(CANCUN) alone regressed an already-fixed pre-Cancun cross-tx recreate scenario (test_multi_tx_create), since destroy-then-recreate across different transactions is a legitimate pattern independent of hardfork. Gated on account.is_created() instead - the account's own creation history is the actual invariant that makes "blank slate" safe, and is always true post-Cancun whenever this branch is reachable at all. crates/ee-tests/src/revm_tests.rs: - Added test_read_only_eip7702_redelegate_restores_prior_delegation_on_discard: commits a real delegation, has a ReadOnly transaction attempt to re-delegate the same authority and get discarded, and asserts the original delegation (nonce, code_hash, code bytes) survives intact. - Added pre_cancun_cross_tx_destroy_of_pre_existing_contract_stays_destroyed (in crates/context/src/journal/inner.rs) confirming a pre-existing contract destroyed pre-Cancun stays permanently destroyed even after a later same-block transaction merely touches it. - Regenerated test_selfdestruct_multi_tx.json - its contract is a pre-existing (never-created-this-session) BENCH_TARGET, so it now correctly stays marked destroyed under the more precise is_created()-based gate; one status field, matching the pattern of the prior regenerations in this PR. crates/ee-tests/src/coinbase_scratch_test.rs -> coinbase_warmth.rs, crates/ee-tests/src/access_list_scratch.rs -> access_list_warmth.rs: Renamed - these are permanent regression tests, not throwaway scratch files. Added trailing newlines to both. crates/op-revm/src/handler.rs: Reordered two comments in the failed-deposit path so each sits directly above the line it describes. Also ran cargo fmt on every file touched across this PR (entry.rs, inner.rs, the two renamed test files, revm_tests.rs, pre_execution.rs) - confirmed cfg.rs/result.rs's reported diffs are pre-existing and left untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(journal): stamp real transaction_id when loading a pre-existing account for the first time crates/context/src/journal/inner.rs: load_account_optional's Vacant branch constructed newly-loaded accounts via `From<AccountInfo> for Account` (crates/state/src/lib.rs), which hardcodes transaction_id: 0 - unlike its sibling branch, Account::new_not_existing(self.transaction_id), which correctly threads the journal's real current id. In a fresh journal this is harmless, since the first transaction genuinely has transaction_id == 0. But once an earlier, unrelated transaction has already committed to the same shared journal (single-journal, multi-tx execution mode) and advanced transaction_id past 0, the hardcoded 0 no longer matches - so the very next touch of that same account, later in the SAME transaction that just loaded it (e.g. a transaction's own EXTCODESIZE(CALLER) right after its own validation warmed the caller), gets misread by mark_warm_with_transaction_id as a new transaction touching the account for the first time, charging it the cold price instead of the EIP-2929-guaranteed warm one. Confirmed via grep this conversion has exactly one call site in the whole workspace, so the fix is fully contained: stamp account.transaction_id with self.transaction_id right after construction, regardless of which sub-branch built it. crates/ee-tests/src/revm_tests.rs: Added test_sender_extcodesize_stays_warm_after_prior_committed_tx - an unrelated transaction commits first, then a transaction's own EXTCODESIZE(CALLER) must cost exactly 21104 gas (warm), not 23604 (cold). Confirmed it fails without the fix and passes with it. Added pre_existing_account_first_load_stamps_real_transaction_id (in crates/context/src/journal/inner.rs) as a lower-level, general-purpose regression test proving this isn't specific to CALLER/EXTCODESIZE: any pre-existing account's first load in a non-first transaction of a shared journal must be stamped with the real current transaction_id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Addressed a comment --------- Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…s test coverage (#39) * fix(block-meta): correct 63/64 forwarding-rule boundary and harden its test coverage Follow-up on fix/gas-cap-blockPrologue. Verifying that fix surfaced two real problems and a coverage gap that needed closing before it's safe to merge: the genesis-config boundary check had an off-by-one, and no test anywhere actually exercised the new 63/64 boundary logic - which meant an existing test had silently started failing once the bound was introduced. - configs.rs: now block-prologue-gas-cap at genesis configuration time is caped by BLOCK_METADATA_GAS_LIMIT. - configs.rs: refresh the monitorCycleEnd gas-benchmark table with current MonitorCycleEndGasTest figures (a storage-layout change since roughly halved the per-task cost) and note the actual 731-task safe ceiling found by that suite's boundary scan, while intentionally keeping MAX_SUPPORTED_AUTOMATION_TASKS at 200 for headroom. - BlockMeta.sol: - enabled guards complying 63/64 forwarding rule. - updated memory layout by moving caps to slot 0 - BlockMeta.t.sol: fix testRegisterSucceedsAfterDeregisterFreesBudget, which now reverted unexpectedly because it registered a full-DEFAULT_GAS entry against a cap sized to exactly DEFAULT_GAS, exceeding the new 63/64 bound. Add dedicated boundary tests for register/updateExecutionOrder/ setBlockPrologueGasCap. - ConfigFacet.sol: document that updateConfigBuffer's task-capacity parameters aren't validated against BlockMeta's gas caps on-chain (the two contracts have no direct reference to each other by design, to keep task capacity growth possible without a BlockMeta upgrade), so operators must verify monitorCycleEnd's worst-case gas cost against BlockMeta's registered limit for it before raising capacity. - block_metadata.rs: introduced a new gas-limit property to allow custom value at creation time rather than hardcoded one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Defined constant values as variables * Addressed review comments * Rephased the note for BlockMeta::setBlockPrologueGasCap --------- Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…gate and foundation-owner validation (#40) * fix(inspector,supra-extension): close two High-severity issues (bluealloy#3449, bluealloy#3477) Closes bluealloy#3477: InspectorHandler::inspect_run_without_catch_error unconditionally ran post_execution regardless of ExecutionMode, so ReadOnly/System/Genesis/ AutomatedGasless transactions executed through the inspector path incorrectly applied gas refund, EIP-7623 floor enforcement, caller reimbursement, and beneficiary reward -- diverging from the non-inspector Handler::run_without_catch_error path, which already gates this behind execution_mode().charges_gas(). Add that same gate to the inspector path, plus tests confirming ReadOnly mode skips gas accounting (balance/nonce unchanged) and that the default User mode still applies it (balance decreases). Closes bluealloy#3449: GenesisTransactionGeneratorConfig::is_valid accepted a zero foundation_threshold, duplicate foundation_owners, and owners that are the zero address or one of Supra's reserved addresses -- any of which can produce a foundation multisig wallet that is unusable, or effectively controlled by a reserved/system address, at genesis. Add the three missing checks, plus tests for each new rejection path and a boundary case just outside the reserved address range. * chore(inspector,supra-extension): address review comments Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…val, bound registration inputs, and fix cycle/event accounting (#41) Addresses Issue-3453 and Issue-3444. - removeRegisteredTask now requires cycle state STARTED. - Task registration validates max-gas-amount before predicate verification. - Task registration input sizes (payloadTx length, predicate length, auxData combined length and entry count) are now owner-configurable via ConfigFacet.updateDataLengthCaps, with sensible defaults. - cancelTasks, cancelSystemTasks, stopTasks, stopSystemTasks, and onCycleSuspend now emit only the tasks actually processed. - sysGasCommittedForThisCycle is reset to zero on cycle suspension. - Removed redundant storage resets in moveToStartedState/ moveToReadyState. - Added regression coverage for sysGasCommittedForThisCycle's cycle-boundary accounting, transitionState reset after STARTED/READY, active-task cancellation, and cycleLockedFees/refund-failure accounting behavior (the latter two confirmed intentional and left as-is). - Introduced TaskMetadataLW copy-avoidance optimization utilized in task bookkeeping flow. Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…mpaction; congestion-exponent cap (#42) * [Issue-3445] fix(automation-registry): order-independent task-list compaction; congestion-exponent cap Addresses the EVM pass-3 Solidity audit findings against the automation registry contracts (epic bluealloy#3046, WS 5.1). See issue bluealloy#3445 for background. LibCore.sol: - Add RegistryState.orderedTaskIds, an append-only mirror of task registration order. LibCore.buildAliveOrderedTaskIds compacts it into the ascending list of currently-alive tasks in O(n), independent of how many tasks have since been removed or in what order. This replaces the previous full-registry sort at cycle-end (onCycleEndInternal, tryMoveToSuspendedState). - LibCore.insertionSort is removed. Sorting a caller-submitted task batch (dropOrChargeTasks, onCycleSuspend) is now the downstream (VM_SIGNER processTasks submitter's) responsibility: LibCore.requireSortedAscending reverts immediately on an out-of-order batch instead of silently sorting it. - TransitionState.survivedTaskIds accumulates the surviving task set incrementally across every processTasks batch of a cycle transition. RegistryState.activeTaskIds (now a plain array, not an EnumerableSet.UintSet) is assigned directly from it at finalization, and RegistryState.orderedTaskIds is re-synced/cleared at the same point. LibCommon gains removeFromActiveTaskIds for the plain-array removal path. LibAccounting.sol: - calculateExponentiation no longer squares baseScaled after the last exponent bit has been consumed, since that result is never used. Config / ConfigFacet / LibCommon / DiamondInit / LibDiamondUtils: - Add a governance-configured Config.maxCongestionExponent (default 6), threaded through InitParams, ConfigFacet.updateConfigBuffer, and LibCommon.validateConfigParameters (rejects 0, and any congestionExponent above it). - Correct LibDiamondUtils.defaultInitParams' taskCapacity/sysTaskCapacity to 160/40, matching supra-extension's genesis generator defaults. supra-extension (generator.rs, configs.rs): - Add maxCongestionExponent to the InitParams ABI binding and to AutomationRegistryConfigV1, with matching validation and a default of 6, and thread it through setup_automation_registry. - Refresh the MAX_SUPPORTED_AUTOMATION_TASKS gas-benchmark table and boundary-scan figures for the new task-list compaction approach. Testing: new/updated coverage across solidity/supra_contracts/test/{MonitorCycleEndGas,CoreFacet, ConfigFacet,AutomationFeeMultiplier,DiamondInit,BaseDiamondTest}.t.sol and crates/supra-extension/src/contracts/configs.rs, including boundary scans confirming monitorCycleEnd's task-list compaction cost no longer depends on task ordering. * Updating task processing to fail if specified task with index does not exist * test(automation-registry): add cycle-transition gas benchmarks and downstream guide Adds Foundry gas benchmarks for the three automation-registry cycle-transition flows (normal FINISHED->STARTED, mid-cycle STARTED->SUSPENDED, and a FINISHED->STARTED transition with expiring tasks), covering processTasks batch costs that the existing MonitorCycleEndGas.t.sol benchmark doesn't measure. Task counts are overridable via env vars for custom-N runs. Also adds a downstream gas_limit sizing guide summarizing all three scenarios and documenting how monitorCycleEnd's BlockMeta::blockPrologue registration relates (and is not automatically kept in sync) with the registry's task capacity. * fix(automation-registry): fix reverse-sorted test's storage-slot drift, address gas guide review feedback _setOrderedTaskIdsDescending's slot derivation is corrected to match AppStorage's current field layout, and the reverse-sorted boundary scan now asserts expectedTasksToBeProcessed comes back strictly descending, so a future AppStorage layout shift fails the test loudly instead of leaving it silently vacuous. Also addresses review feedback on the gas benchmark guide and related comments: - States plainly that processTasks records currently get a flat gas_limit (TX_GAS_LIMIT_CAP) rather than per-record variable sizing, with the measured worst case and resulting headroom margin, so a future sizing change has a documented baseline to check against. - Removes review/commit provenance and pre-fix-behavior narration from test comments, and a rot-prone line-number reference, per the contribution standard. Documented expected policy on `AutomationRegistryRecord` and its affiliated items update. * docs(automation-registry): refresh gas guide figures from a benchmark re-run Re-ran CycleTransitionGasTest for all three scenarios; figures shifted by tens of gas per batch, within the guide's own documented variance. Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…wment The Supra EVM cannot mint, so native SUPRA on the EVM side is becoming a mirror of value escrowed on the Move side, arriving only by crossing. Endowing the handler at genesis has no place in that model, and the handler's own test suite already asserts the invariant an endowment breaks: its balance equals ERC20Supra's total supply, which holds only while every unit of native it holds was deposited in exchange for tokens. The initial_native_token configuration field and the parameter chain that carried it to the proxy deployment are removed rather than set to zero, so the concept cannot return by accident. The proxy now deploys through GenesisTransaction::create, which is byte-identical apart from carrying no value, and check_multisig_setup asserts the deployed handler holds nothing. Refs Entropy-Foundation/smr-moonshot#3662, Entropy-Foundation/smr-moonshot#3474
Nothing in the repository declared a compiler, so the one used was whatever the machine happened to provide, and the set of toolchains that actually build the fork is both narrow and undeclared: the workspace `rust-version` of 1.88.0 is not sufficient in practice, while the local rustup default of 1.87.0 is rejected outright. A contributor therefore meets errors that read as code defects rather than environment ones, and a future move of `stable` can break the fork with no change on our side and nothing to pin back to. Pin 1.97.1, the version `smr-moonshot` pins, so the fork compiles under the toolchain its consumer uses. The pin covers the components and the cross-compilation target the workflows ask for, since rustup installs those from the file. Two jobs deliberately need a different compiler: the test matrix, which sweeps the MSRV, stable and nightly, and the book, whose rustdoc invocation passes `-Zunstable-options`. Both now set `RUSTUP_TOOLCHAIN`, which rustup ranks above `rust-toolchain.toml`; without that the pin would silently collapse the matrix onto one compiler and break the book build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…in-3675 Pin the toolchain this fork builds with (smr-moonshot#3675)
…stroys A `SELFDESTRUCT` that names the destroyed account as its own beneficiary zeroes the balance without crediting anyone, so that value leaves circulation. Supra's EVM native balance mirrors value escrowed outside the EVM and cannot be minted, so the host needs the exact amount in order to account for it; `SelfDestructResult::had_value` only says whether the balance was non-zero. Accumulate the destroyed amount on `JournalInner` at the point where the journal decides to zero the balance, and subtract it again whenever the journal entry that recorded it is reverted, so a reverted frame or a discarded transaction reports nothing. The total is not cleared at the transaction boundary, so it is still readable after execution; the host drains it with `take_selfdestruct_burn` once per transaction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rain contract `JournalEntryTr::selfdestruct_burn` reports a Supra-local concept, so give it a zero default: an entry type with no notion of a destroyed balance, or one arriving from an upstream sync, then needs no change. `JournalEntry` overrides it, so nothing is lost. Document the two contracts the accumulator places on its caller rather than on the journal. The revert paths subtract with `saturating_sub`, which makes a mid-transaction drain quiet instead of rejected, so the field is only correct if it is drained between transactions; say so where a caller will read it. And the journal has no notion of a block, so record that the total covers whatever span the caller chooses to drain over, which for Supra is a block because it runs one journal per block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…urn total A revert that tries to take back more than the accumulator holds can only mean the total was drained while the destruction it covers was still revertible, so the saturation is itself the symptom. Assert against it at both revert sites, which turns the drain contract from a documented requirement into one that fails loudly wherever an embedder would introduce the violation. Release behaviour is unchanged: the subtraction still saturates, because on a consensus-critical path a caller that breaks the contract should cost accounting accuracy rather than halt the node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[bluealloy#3662] Stop endowing the handler at genesis, and report the value a self-destruct destroys
… contracts at genesis (#45) * feat(supra-extension): predeploy canonical EVM singleton contracts at genesis Extend the genesis transaction generator to deploy Multicall3, the ERC-2470 SingletonFactory, CreateX, and the ERC-1820 Registry at their canonical addresses, alongside the existing CREATE2 factory. Ecosystem tooling (Foundry, hardhat-deploy, ERC-777, account-abstraction stacks, etc.) expects these well-known contracts at fixed addresses on any EVM chain, so they're predeployed the same way the CREATE2 factory already is rather than relying on users/tooling to deploy them after genesis. Each contract's init-code is embedded as a static binary asset, extracted verbatim from its canonical historical deployment transaction (sender, nonce, and init-code independently verified by decoding the real presigned transactions and re-deriving each address) rather than compiled from vendored Solidity source, since these are third-party contracts we don't own or modify and any compiler/ optimizer difference risks producing bytecode that isn't byte-identical to what's deployed elsewhere. All five predeploys (Create2Factory + the four new ones) are moved into a dedicated `canonical_singletons` module, separate from Supra's own system/application genesis contracts, and are unconditional regardless of the `full_set` config flag since none relate to that feature set. The new `GenesisTransactionTags` variants are appended after the last existing named variant rather than interleaved, since `Ord`/serde's enum encoding key off declaration order rather than the explicit discriminant. Also fixes a pre-existing clippy::derive_ord_xor_partial_ord violation on `ContractCustomTag` (manual `Ord` alongside a derived `PartialOrd`) that was blocking a clean clippy run independent of this change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fixed automation task payload decoding * Addressed review coments: - Embeded Create2Factory init-code as a binary asset - Add a keccak256 hash assertion per predeploy as a tripwire against accidental corruption of the embedded init-code assets. For CreateX, the expected hash is the value pcaversaccio/createx's own README publishes and tells the community to verify against before trusting a CreateX deployment on any chain; for the other four, no such value is published by the upstream project, so those hashes are self-computed and pinned as a regression guard rather than an externally-published reference. - Ordered canonical singleton predeploys before Supra's own genesis contracts - Corrected GenesisTransactionTags ordering comment - Updated automation payload decoding test to cover decoding with non-zero value payload - Made Ord and Eq contracts of ContractCustomTag to agree on equality - Fixed formatting of the files Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…46) * [bluealloy#3448] Harden MultiSignatureWallet governance and two-step the beacon - Bind each confirmation to the owner incarnation and threshold under which it was cast. - Add a multisig-gated cancelTransaction and a permissionless removeExpiredTransaction for pending-transaction lifecycle management. - Bound submission timeouts via a new, multisig-settable maxTimeoutDuration. - Confirming, executing, or revoking an expired transaction now reverts. - Remove the dead numConfirmations field and trim the OwnersAdded/OwnersRemoved event payloads to actual entries. - Switch MultisigBeacon to two-step ownership transfer (Ownable2Step) instead of single-step Ownable. See bluealloy#3448. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * [bluealloy#3448] Address PR #46 review: beacon renounce, docs, and cleanup - Disable MultisigBeacon.renounceOwnership, closing the freeze-forever path Ownable2Step alone didn't cover. - Reimplement MultisigBeacon on UpgradeableBeacon + Ownable2Step with override forwarders instead of hand-rolling the beacon, tracking upstream OZ. - Sync IMultiSignatureWallet's NatSpec for confirm/execute/revoke with the revert-on-expiry behaviour, and make notExpired's comment precise about its distinct error. - Gate revokeConfirmation on raw membership so an owner can still clear a stale confirmation instead of being permanently stuck as an unrevocable member. - Extract _recordConfirmation to stop submitTransaction and confirmTransaction's stamping logic from drifting apart, and call validNumberOfConfirmations directly in executeTransaction instead of re-running guards hasValidNumberOfConfirmations already repeats. - Mark the array-trim and CREATE assembly blocks memory-safe, required for correctness under this project's via_ir = true. - Add test coverage for all of the above, including exact OwnersAdded/OwnersRemoved event payloads. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fixed cilppy and compile errors --------- Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* [bluealloy#3451] Harden Automation Registry diamond proxy (ownership, init, storage, loupe) Addresses smr-moonshot#3451, the EVM-readiness audit's diamond-proxy hardening pass. All of this is genesis-immutable once deployed, so it lands as pre-genesis source changes: - LibDiamond.setContractOwner now rejects the zero address, closing the unguarded genesis-owner and transferOwnership paths (IERC173's docs no longer advertise renouncing ownership as supported). - DiamondInit now inherits OpenZeppelin's Initializable, so init() can only ever run once. - AppStorage moves from implicit slot 0 to a namespaced ERC-7201-style slot (mirroring LibDiamond's own DIAMOND_STORAGE_POSITION pattern), and every facet/DiamondInit now fetches it via LibAppStorage.appStorage() instead of declaring it as a plain state variable, so its storage location no longer depends on what a future facet's inheritance chain happens to declare. - isInitialized() moves off Diamond.sol onto DiamondLoupeFacet as a normally-routed selector, via a new IRegistryStatus interface (kept separate from IDiamondLoupe so its well-known EIP-2535 interfaceId is unaffected). - DiamondInit now also registers ERC-165 support for the registry's own facet interfaces (ICoreFacet/IConfigFacet/IRegistryFacet/IRegistryStatus). - LibDiamond.diamondCut now emits its DiamondCut event after running the initializer, and a stale comment describing removeFunctions is fixed. - CoreFacet's processTasks/monitorCycleEnd/removeRegisteredTask carry NatSpec noting they must never be removed via diamondCut (the node's VM-signer decoder hardcodes their selectors) — a documented governance constraint rather than an on-chain guard, by design. - Adds script/check_facet_selectors.sh + CheckFacetSelectors.s.sol, cross-checking each facet's hand-maintained getSelectors() against its compiled ABI. - Regenerates the gas figures in AUTOMATION_REGISTRY_GAS_GUIDE.md and configs.rs's monitorCycleEnd table, both measurably shifted by the storage-layout change above. Full Foundry suite (467 tests) and `cargo check -p revm-supra-extension` pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Addressed review comments * [bluealloy#3451] Fix isInitialized routing regression, add isAutomationReadyEnabled Addresses smr-moonshot#3451 review follow-up. - Fixes a regression that had reintroduced isInitialized() directly on IDiamondLoupe with an invalid `override`, breaking the build and changing type(IDiamondLoupe).interfaceId away from the well-known EIP-2535 value again. - Rewrites the DiamondInit.sol/test doc comments the CONTRIBUTING.md vulnerability-disclosure convention flagged, to intended-behaviour form with a bare issue reference. - Fixes check_facet_selectors.sh's `set -euo pipefail` handling so a facet reporting zero selectors is reported as a clear FAIL instead of aborting the script, and restores its executable bit. - Adds a test proving a `reinitializer(2)` upgrade initializer can run after DiamondInit.init() has consumed Initializable's version 1. - Fixes MonitorCycleEndGas.t.sol's inverted comment about which part of its storage-slot derivation is fixed vs. field-order-dependent, and derives the base via LibAppStorage.registryState()'s own slot instead of repeating it as a literal. - Adds CoreFacet.isAutomationReadyEnabled(), a combined isInitialized() && isAutomationEnabled() readiness check, routed through the diamond so it can't drift from whichever facet serves isInitialized() after a future upgrade. - Simplifies isInitialized() to read a dedicated DiamondStorage.initialized flag set once at the end of DiamondInit.init(), instead of reusing the ERC-165 interface registry for an unrelated purpose. - Substitutes isAutomationReadyEnabled for isAutomationEnabled in SupraContractsBindings.sol and regenerates the Rust ABI bindings. - Keeps the gas-benchmark figures in AUTOMATION_REGISTRY_GAS_GUIDE.md and configs.rs in sync with the above. Verified: full Foundry suite, check_facet_selectors.sh, cargo check/test -p revm-supra-extension, and RUSTFLAGS=-Dwarnings cargo clippy -p revm-supra-extension (confirmed its remaining failures pre-date this change). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * [bluealloy#3451] Fix check_facet_selectors.sh's silent-skip hole and restore its +x bit Follow-up to the latest PR review pass on smr-moonshot#3451. - check_facet_selectors.sh derived its "expected facet" set from the same SELECTOR output it was validating, so a facet whose getSelectors() reports zero entries (or that's simply missing from CheckFacetSelectors.s.sol's run()) never entered the comparison at all and the run exited 0. The expected set is now derived independently, by scanning src/facets/ for contracts that actually implement IFacetSelectors, so both of those cases are now a FAIL. Verified against three reproductions: a facet reporting zero selectors, a facet omitted from CheckFacetSelectors.s.sol's run() entirely, and a single omitted selector. - The script's executable bit still wasn't making it into the commit despite the prior message saying it was restored; staged it directly this time and confirmed via `git ls-files -s`. - Removes three imports (ICoreFacet, IConfigFacet, IRegistryFacet) left behind in DiamondInit.sol after the interface registrations that used them were removed; nothing else in the file referenced them. Verified: full Foundry suite, check_facet_selectors.sh (including the three reproductions above), and cargo check -p revm-supra-extension. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * [bluealloy#3451] Fix check_facet_selectors.sh's facet-detection formatting fragility Follow-up to the latest PR review pass on smr-moonshot#3451. The expected-facet set was derived by grepping each facet's `contract X is ... IFacetSelectors` declaration as source text, which only matched when that declaration sat on a single physical line. A multi-line inheritance list (or a commented-out declaration) would silently drop a real facet from the expected set. Switches to compiled-ABI introspection (`forge inspect <contract> methods`, checking for getSelectors()) instead, which is immune to source formatting entirely, can't be fooled by a comment, and scans src/facets/ recursively rather than one level deep. Also tightens the accompanying comment's wording, which stated the derivation was independent of any hand-maintained list without immediately noting the one documented exception (DiamondCutFacet) sitting right below it. Verified against the reviewer's exact reproduction (reformatting CoreFacet's declaration to multi-line) plus all three prior negative controls (zero selectors, a facet omitted from CheckFacetSelectors.s.sol's run(), a single omitted selector) and the full Foundry suite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Aregnaz Harutyunyan <> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
No description provided.