Skip to content

Evm automation feature implementation - #23

Draft
aregng wants to merge 95 commits into
mainfrom
feature/evm_automation
Draft

Evm automation feature implementation#23
aregng wants to merge 95 commits into
mainfrom
feature/evm_automation

Conversation

@aregng

@aregng aregng commented May 7, 2026

Copy link
Copy Markdown

No description provided.

zwu-supra and others added 30 commits October 24, 2025 13:57
* add customization required for evm automation

* rm some spaces and cleanup
…e_to_revm

Add tx hash precompile to revm at 0x53555001
-removed BlockBasedCounter
-updated BlockMeta with deregister and enumerableSet
-updated testcases
Aregnaz Harutyunyan and others added 4 commits June 12, 2026 11:16
* 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 <>

@isaacdoidge isaacdoidge left a comment

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.

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_hash field is not user-forgeable on the consensus path: EvmExecutor::convert_to_tx_env always sets it to txn.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 User execution mode, the engine delta is behaviorally equivalent to upstream revm v29 (gas, nonce, balance, refunds, EIP-3607 all gated behind ExecutionMode, default User). The one un-gated new rule is validate_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 ByPassBypass 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)

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.

[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);

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.

[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 {

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.

[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]

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.

[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)?;

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.

[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;

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.

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(|| {

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.

[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();

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.

[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().

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Internally the call checks and asserts the success, so the errors will cause early termination on this call.

Comment thread Cargo.toml
# 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" }

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.

[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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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 {

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.

[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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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 isaacdoidge left a comment

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.

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 {

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.

[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 isaacdoidge left a comment

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.

One more from a second core-engine pass (verified against the diff).

Comment thread crates/handler/src/frame.rs Outdated
context
.journal_mut()
.nonce_bump_journal_entry(inputs.caller);
if should_update_nonce {

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.

[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.

aregng and others added 21 commits July 14, 2026 19:27
…33)

* Addressed review comments left on main feature/evm_automation branch

* Addressed followup comments

---------

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>
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.

4 participants