Celadon audit - #214
Draft
panos-xyz wants to merge 14 commits into
Draft
Celadon audit#214panos-xyz wants to merge 14 commits into
panos-xyz wants to merge 14 commits into
Conversation
) Bumps the actions-weekly group with 1 update: [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `taiki-e/install-action` from 2.87.5 to 2.87.11 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](taiki-e/install-action@v2.87.5...v2.87.11) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.87.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-weekly ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Add MorphTx version 2 (0x7f || 0x02 || rlp), which carries an EIP-7702 authorization list on top of the v1 fields and is activated by the new Onyx timestamp fork. - primitives: encode the authorization list after memo in both the payload and the signature hash; an empty list is valid and behaves like v1, a non-empty list forbids CREATE, v0/v1 must not carry one; the Compact codec stays backward compatible; JSON always emits authorizationList for v2 ([] when empty) and never for v0/v1 - chainspec: add the Onyx hardfork (onyxTime), mapped to OSAKA - consensus/txpool: reject v2 before Onyx; the upstream pool's authority and delegation limits apply to v2 through Transaction::authorization_list - revm: apply v2 authorization lists through the same path and refund accounting as 0x04, enforce the static EIP-7702 rules, and size the L1 data fee of simulated transactions with the list - rpc: build v2 from requests carrying authorizations and reject invalid combinations as parameter errors - statetest: model MorphTx with authorizations as v2, add the onyx fork
cargo-deny now fails on RUSTSEC-2026-0285: rustls before 0.23.45 accepts TLS 1.3 handshake messages across encryption level boundaries. rustls 0.23.45 requires aws-lc-rs ^1.18 and rustls-webpki ^0.103.14, so those are bumped as well.
- primitives: `decode_fields` (behind `rlp_decode_fields`) now requires the RLP list to be consumed exactly for every version, matching `Decodable::decode`, so surplus elements are rejected instead of left unread; add regression tests - node tests: pin the pre-Onyx v2 rejection to the "not yet active" error - statetest: document that the presence of `authorizationList` (even an empty one) selects v2, the same convention used to select 0x04
Drop the `version` selector from `MorphTransactionRequest` and derive the version from the content: V1 is the baseline (the request layer no longer produces V0) and a non-empty `authorizationList` selects V2. An absent, `null` or empty list are the same thing, and a legacy `version` key is ignored like any other unknown key. The rule lives in primitives as `TxMorph::inferred_version` / `with_inferred_version`, so library users derive the version the same way the RPC layer does instead of filling it in by hand. The CREATE rejection now reads "MorphTx with an authorization list cannot create a contract" on every layer. The e2e simulation tests select a MorphTx with a memo instead of the removed key and check that a legacy `version` key leaves the estimate unchanged. Claude-Session: https://claude.ai/code/session_01WYbNZVUBHa4qCoRK46taTS
The fork that activates MorphTx v2 was carried under the placeholder name Onyx. It is now named Celadon, so rename it everywhere on this branch: - `MorphHardfork::Onyx` -> `MorphHardfork::Celadon`, together with `is_onyx` / `is_onyx_active_at_timestamp` and the test schedule `HardforkSchedule::PreOnyx` - genesis key `onyxTime` -> `celadonTime` (no alias is kept: the old key never shipped in a bundled chainspec, and an unknown key is ignored, so a private devnet genesis has to switch to the new key) - the pre-fork rejection now reads `MorphTx version 2 is not yet active (celadon fork not reached)` - the statetest fork name `Onyx` -> `Celadon` (`osaka` still maps to it) - test names and comments No behaviour change besides those names. Claude-Session: https://claude.ai/code/session_01WYbNZVUBHa4qCoRK46taTS
* fix(revm): align call-mode fee-token execution with go-ethereum A fee token registered without a `balanceSlot` takes the EVM-call path: the protocol resolves the payer's balance with `balanceOf` and moves the fee with an ERC20 `transfer`. morph-reth diverged from morph-geth on that path in ways that change `gasUsed`, receipts and state roots, so a follower rejects blocks a geth block producer accepts. Measured against the golden fixtures added in the next commit, main fails 8 of 12 case templates on both Emerald and Jade: - The fee `transfer()` frame's SSTORE refund was discarded, so a fee that clears the payer's balance slot earned the user no refund (the receipt `gasUsed` mismatch reported in #207). go-ethereum runs that call through `evm.Call` inside `buyAltTokenGas()` before `StateDB.Prepare`, which does not reset the refund counter, so the refund reaches `refundGas()`. The net counter is now carried on `MorphEvm` and recorded before the EIP-3529 cap, so it can also be cancelled by a negative refund from main execution. - Internal calls ran with a default transaction environment and their own storage, so `balanceOf`/`transfer` saw ORIGIN = 0x0 and an effective gas price of 0. A guard on either read the wrong value and the payer's balance resolved to zero, rejecting an affordable transaction. Internal frames now keep the outer transaction's ORIGIN and GASPRICE, run in the executing block's environment, and `balanceOf` executes as a genuine static frame under the same 200k gas allowance geth uses. - A successful transfer whose return value or balance delta failed a later business check was rolled back with its logs. go-ethereum keeps the state and logs and only reports the failure. The frame now owns its checkpoint: a VM revert still rolls back, a business failure does not, and a database failure stays fatal rather than becoming a verdict about the token. - Zero fees skipped neither `transfer(0)` nor the initial `balanceOf`; geth skips both transfer modes but still performs the balance query. Nonce and cache updates, and call-mode access-list/transient cleanup, still happen. `TokenFeeInfo::effective_fee_limit` replaces the two hand-rolled fee-limit clamps the execution and pool paths each carried, so they cannot drift. `MorphEvm::from_env` gives execution and pool queries one constructor. The receipt builder now reads registry metadata with `load_storage_only`, which never builds a temporary EVM to resolve a balance it does not use. The pool's single call site is adapted to the new `load_for_caller` signature while keeping its previous behaviour: admission still evaluates a call-mode `balanceOf` under the hardfork's defaults. Threading the real head environment through admission is txpool work and is deliberately not part of this change. * test(statetest): add geth-derived golden fixtures for fee-token calls Twelve cases across Emerald and Jade, with state roots, logs roots and transaction gas generated by morph-geth 5744b8f66. Every case registers its token with `balanceSlot = 0`, so all twelve exercise the EVM-call path: deduction clearing the payer's ERC20 balance slot, the one-unit balance control, main-frame revert and OOG, a main call that restores the cleared slot, negative-refund cancellation, ORIGIN and GASPRICE guards, static-call violations, a successful transfer whose refund returns false, a refund that reverts, and zero-fee storage warmth. Note that "balance slot" in these case names is the ERC20's own storage slot that the fee `transfer()` clears, not the registry's optional `balanceSlot` field; the storage-slot fee path is not covered by these fixtures. Replaying them against main (v1.3.0) fails 8 of the 12 templates on both forks. With the preceding commit all 24 outcomes pass. * docs(revm): document the transaction-id invariant behind the mid-tx finalize The call-mode fee path commits its deduction by calling `evm.finalize()` mid-transaction, then re-marks every account and slot cold to reproduce the warmth go-ethereum's `StateDB.Prepare` would leave behind. Nothing explained why the whole journal is discarded there, or which part of that reset the correctness depends on. The load-bearing property is that the reset must not advance the transaction id. Warming a slot runs through `EvmStorageSlot::mark_warm_with_transaction_id`, which re-baselines the EIP-2200 `original_value` to the present value whenever the slot's id differs from the journal's. Had that fired on the slot the deduction just cleared, the main frame's SSTORE would be a create rather than a recreate and the `SubRefund` cancelling the deduction's `+4800` would be lost — measured on `main_restores_cleared_slot`, 23_291 gas becomes 38_391. It cannot fire here because ids stay equal across execution. revm advances the id only when a transaction finishes — `commit_tx()` from `execution_result`, or `discard_tx()` on the error path — both after the main frame; `ExecuteEvm::finalize` then resets it to ZERO before the next transaction. `finalize()` at this point is therefore idempotent for the id, while `commit_tx()` would leave the deduction-warmed slots holding 0 against a journal holding 1. Verified by substitution: swapping `commit_tx()` in fails `main_restores_cleared_slot` with a state root mismatch. * fix(revm): return the fee frames' shared memory to the transaction The fee-token frames are top-level frames that run in the middle of a transaction, so neither of revm's truncation points covers them: `free_child_context` only releases a child frame's region, and `LocalContext::clear` only runs once the whole transaction is done. The frames therefore left their bytes on the context's shared buffer and the main transaction frame started on top of them. Measured on a call-mode MorphTx: the main frame entered with `MSIZE == 32`, and `MLOAD(0)` returned 9_000_000, the payer's post-fee token balance left behind by the internal `balanceOf`. go-ethereum allocates a fresh `Memory` for every interpreter run (core/vm/interpreter.go), so both read zero there, and both read zero here on the ETH-fee control. Any contract that reads memory it never wrote, or branches on MSIZE, produced a different result on morph-reth than on morph-geth. Carve each fee frame's memory out above whatever the buffer already holds, and release it on the way out, including on the error path. The frames keep writing into the context's buffer rather than one of their own: a nested call hands its callee a `CallInput::SharedBuffer` range, and while a contract callee resolves that range against its own frame memory, a precompile callee resolves it against the context's buffer (`CallInput::as_bytes`). A private buffer would hand every precompile called from a fee frame empty calldata — `fee_token_frames_reach_a_precompile_through_memory` fails with `InsufficientTokenBalance { available: 0 }` under that variant. Gas is unaffected, since memory expansion is charged from the per-frame `Gas` counter, and the leak did not cross transactions, since `local_mut().clear()` runs at the end of each one. The 24 geth-derived golden fixtures cannot see any of this: ten of their twelve templates call a codeless EOA and the other two call bytecode that writes before it reads. * docs(revm): correct what the pool's balanceOf query aligns with The comment justified setting the query's ORIGIN to the queried account by claiming go-ethereum's pool does the same. It does not: `getBalanceFunc` builds its EVM on an empty `vm.TxContext{}` (core/tx_pool.go:341), so its ORIGIN is the zero address — and go-ethereum's own execution layer resolves the same `balanceOf` with ORIGIN set to the sender, so its pool disagrees with its own execution. Setting ORIGIN to the account is still the right call, for the opposite reason to the one recorded: admission exists to predict what the builder will be able to include, so it follows this client's execution layer rather than the other client's pool. Say that, and record the one input the query still cannot match — GASPRICE, which stays at the `TxEnv` default of zero because the effective price depends on the next block's base fee. * test(node): run the e2e fee-token path in EVM-call mode Every registered fee token on mainnet now has its `balanceSlot` cleared, so the fee is moved by real `balanceOf` and `transfer` calls into the token contract. The e2e genesis registered `balanceSlot = 2` instead, which put all 128 integration tests on the direct-storage path: the mode that is scheduled to be disabled by a hardfork, and the one production does not use. The node-level behaviour of the mode production does run — receipts, log ordering, pool admission, the replay RPCs — had no integration coverage at all, while the 24 statetest golden fixtures cover only its state effects. Give the test token the ERC20 runtime the gas-regression test already carried inline, and clear the registry's `balanceSlot`. The runtime keeps `balanceOf` at slot 1, so `test_token_balance_slot` still derives the same slot independently and remains a test oracle rather than a second copy of the code under test. Both gas regressions hold unchanged at 48_128 and 50_428: the fee frames run on their own 200k budget, and the deduction books no SSTORE refund while the payer keeps a balance. What does change is the receipt, which now carries the fee deduction and the fee reimbursement around the transaction's own transfer. Assert that ordering — deduction, main, refund — since it is what go-ethereum produces and what indexers read. * fix(revm): make the fee path's load-bearing invariants local and true Six items a review of this branch turned up, each verified against revm 42 and go-ethereum before being acted on. `reimburse_caller_token_fee`'s slot branch reaches `sload`/`sstore` on the token directly, which panic rather than error when the account is absent from `journal.state` (`sload_assume_account_present` -> `ColdLoadSkipped` -> `unwrap_db_error`). It relied on the deduction having loaded it, but the deduction skips both transfer modes for a zero fee. The two cannot disagree today — `eth_to_token_amount` rounds up, so a zero token fee means a zero ETH fee, which returns before the transfer — but that proof lives in another function. Load it where it is needed instead; today the load is a no-op. `evm_call` has the same shape of hidden dependency: its `CallValue::Transfer` frame runs `Journal::transfer_loaded`, whose zero-value path is `self.state.get_mut(&to).unwrap()`. An ordinary CALL is safe because the opcode's `load_acc_and_calc_gas` loaded the account; an internal call has no opcode, so `internal_call_code` is the only load. Its doc comment described itself purely as a warmth-preserving code read. Say what it is also for. `MorphBlockExecutor::hardfork` became write-only when `get_morph_tx_fields` stopped taking a hardfork, leaving a doc comment claiming it is "reused in `commit_transaction`". Removing it leaves `spec` dead as well — it existed only to compute it. Drop both, and the constructor argument with them. Two comments claimed things that are not true of the pinned revm or of the code they describe. `load_token_fee_info` blamed a "30M gas limit" on the previous path, which went through `system_call_one` and so already capped at go-ethereum's 200k; the divergence was the environment and the sender. And `ExecutionResult::Revert` does carry a `logs` field in revm 42 — the fee logs need their side channel because the mid-transaction `finalize()` clears the journal's logs, not because the variant cannot hold them. The pool restated the fee-limit clamp by hand under a "Match REVM semantics" comment, although `TokenFeeInfo::effective_fee_limit` was added to be the one copy. Use it. Finally, `transfer_erc20_with_evm`'s affordability check built its error message with `ok_or`, rendering two U256s and allocating a String on every successful call-mode fee transfer; `ok_or_else` defers it. * test(statetest): cover uninitialized memory and the direct-slot path The twelve golden cases all target either a codeless EOA or bytecode that writes before it reads, and all twelve register the token in EVM-call mode. Two consensus-relevant behaviours were therefore invisible to them. `main_reads_uninitialized_memory` commits MSIZE and MLOAD(0) to storage from the transaction's own frame, before writing either. go-ethereum allocates a fresh `Memory` for every interpreter run, so both read zero and neither SSTORE changes state; a client whose fee frames leave their bytes on the transaction's shared memory writes two non-zero slots and misses the root. Verified to have teeth: reverting the fee frames to a checkpoint-zero `SharedMemory` fails it with a state root mismatch. `slot_deduct_keep` and `slot_deduct_clear` are the first coverage of the registry's direct-slot path, which has to keep working for replaying blocks produced before every mainnet token moved to the call path. `slot_deduct_clear` is byte-for-byte the transaction `deduct_clear` runs and costs 21_000 against its 16_800: clearing the payer's balance through a real `transfer` books a `+4800` SSTORE refund that reaches the transaction, while `SetState` books nothing. That 4_200 is the only way the two modes bill differently, and it is now pinned from both sides. Roots and logs hashes come from morph-geth 4012f174b, which reproduces all twelve existing cases unchanged. * test(node): assert the fee logs survive a reverting main frame `receipt.rs` caches the fee `Transfer` events outside the journal because go-ethereum's `StateDB.logs` is not part of the state snapshot/revert mechanism: when the main frame reverts, the deduction's log must still be in the receipt. Nothing asserted that. Every reverting golden case used a token that emits no logs, so its expected `logs` hash is the empty hash and a client that dropped `pre_fee_logs` on the floor would produce the same value. The property decides the receipt's logs and therefore the block's receipts root. `morph_tx_v0_token_fee_still_charged_on_revert` already reverts the main frame against the real ERC20 test token and already runs through `MorphBlockExecutor` and the production receipt builder. Assert the two fee transfers it must carry, in go-ethereum's order, and that the deduction moved a non-zero fee. Verified to have teeth: not extending `pre_fee_logs` in `build_receipt` fails it. One comment described the code wrongly and is corrected: - The call-mode deduction comment said re-marking accounts and slots cold "reproduces the warmth `Prepare` would have left behind". It does not, and must not: the coinbase and access list are re-warmed later by upstream `pre_execution::load_accounts`, which runs after this deduction because the deduction happens in `validate()`. Say so, and say what breaks if a future change reorders those phases. `load_token_fee_info`'s claim that the old path "capped at `SYSTEM_CALL_GAS_LIMIT`, which is go-ethereum's 200k" reads wrong, because revm's `SYSTEM_CALL_GAS_LIMIT` is 30_000_000. It is right, though: this crate defines its own 200_000 in `exec.rs` and sets it in the `SystemCallEvm` impl, shadowing revm's. Name that shadowing, since the bare constant reads as a mistake and invites exactly the wrong "fix". `expectException` stays presence-only, which reads like an oversight. It is deliberate: go-ethereum's own statetest harness returns early on `len(ExpectException) > 0` under a standing "TODO check error string", so matching the text here would make this runner stricter than the client the fixtures come from. A comment now records that. * refactor(revm): keep the fee-token helpers' invariants inside them Four cleanups from a review of this branch. None of them changes execution. `transfer_erc20_with_slot` needs the token account in `journal.state`, because the journal's `sload`/`sstore` panic rather than error when it is absent, and both callers loaded it themselves with a comment apiece saying why. The helper now loads and touches the token ahead of its checkpoint. That is exactly what the deduction did before. The refund's extra touch is a no-op: a refund only runs after a non-zero deduction, which already touched the token in the same transaction. The slot-path golden fixtures pass unchanged, and the unit test that exercises the helper no longer pre-loads the token. `reimburse_caller_token_fee` built its missing-cache error with `ok_or`, allocating the message on every token refund. It now uses `ok_or_else`. `TokenRegistryEntry`, its `load` and its `load_for_caller` had become `pub` and re-exported with no user outside this crate, and `load_for_caller` hands back a `TokenFeeInfo` without going through `ensure_usable`. They are `pub(crate)` again. The pool keeps using `TokenFeeInfo::load_for_caller`. The handler and token-fee tests each carried an identical database that fails storage reads of one token. A single copy now lives in the token-fee test module, which is `pub(crate)` so the handler tests can use it. * docs(evm): give the real reason fee logs are kept out of the result The receipt builder said fee logs are cached apart from the journal because revm's `ExecutionResult::Revert` carries no logs. In revm 42 it does. The real reason is that the call-mode deduction runs a mid-transaction `finalize()` that clears the journal's logs, so the handler moves them out first, and it drains the refund's logs the same way. `result` then holds only the main frame's logs, which a revert has already discarded. The same wrong claim was corrected in `handler.rs` earlier on this branch; this is the copy that was left behind. * test(statetest): replay a mainnet slot-mode fee-token transaction Slot mode is being retired on mainnet, but blocks that already ran it must keep replaying identically, and only two synthetic golden cases exercised the registry's direct-slot path. The new case replays transaction 0x9ebfdac9040d7c2a8739ffdaae8baf5e7aa22fdb48585f80592de4b4cf39ed44 from block 26836567: a V0 MorphTx paying its fee in token 1 through the direct slot, sent by an EIP-7702-delegated account holding no ETH, whose call transfers that same token. One transaction covers the deduction, the main frame writing the payer's already-debited balance slot, and the slot-mode refund. go-ethereum's state-test runner only signs with `secretKey` and fixes the chain id to 1, so the sender moves to the harness account, carrying its nonce, delegation code and re-keyed token balance, and the fee vault's balance is re-keyed to the harness vault. The prestate tracer reports zero for the balance slots the fee logic reads straight from state, so those, the registry entry and the L1 gas price oracle slots are taken from the parent block. That is exact here because the transaction is alone in its block. Roots come from morph-geth 4012f174b, which passes the fixture, as does this runner. The gas used equals the on-chain receipt's 51_257, and the logs root equals the on-chain logs with the sender topic substituted. Verified to have teeth: swapping the slot-mode refund's direction fails it with a state root mismatch.
# Conflicts: # crates/txpool/src/morph_tx_validation.rs
Rounding the prepaid alt-token fee up and the unused-gas refund up again under-collects: the chain gives back part of a token unit the caller never spent. From Celadon on, the deduction records the numerator its ceiling overcharged and the refund adds it back before rounding down, so the caller is charged the ceiling of the *net* fee. Before Celadon both halves keep rounding up, because that is what mainnet state was built from. Rounding down can reach zero, which the ceiling never did for a non-zero refund. go-ethereum's `TransferAltTokenHybrid` returns early on a zero amount, so the refund now does too: no `Transfer(.., 0)` log on the call path, no slot writes on the direct-slot path. This is not gated on the transaction being MorphTx v2 — it applies to every alt-fee transaction at the fork, so a client without it diverges on the first token-fee transaction after activation, not on the first v2 one. Ports go-ethereum#371 `886d7f40b` and `4d71e2b72`.
Nine golden roots from morph-geth 5a0d0d771: three consecutive gas limits on each of Emerald, Jade and Celadon. The fee token is registered with `priceRatio = 3` against `scale = 1` and the transaction carries one non-zero calldata byte, so neither the prepaid fee nor the transaction's gas cost is a multiple of the ratio. That is the only shape where rounding both halves up independently disagrees with charging the ceiling of the net fee: Celadon collects ceil(21_016 / 3) = 7_006 on all three limits, while Emerald and Jade collect 7_005 on two of them and land on a second state root. The 21_016 also pins the gas: morph does not apply the EIP-7623 calldata floor, which would bill 21_040 and miss every root in the fixture.
`cached_alt_fee_rounding_credit` was the one per-transaction cache the reset at the top of `validate_against_state_and_deduct_caller` left alone. That is safe today: the credit is only read next to `cached_token_fee_info`, and the same deduction writes both. Clearing it with the rest keeps that true without depending on where the reads happen. No behaviour change. The new test fails if the reset is removed. Claude-Session: https://claude.ai/code/session_01WYbNZVUBHa4qCoRK46taTS
…fixture The fixture ran one calldata length, whose net fee of 21_016 gas leaves a remainder of 1 modulo the price ratio of 3. For that remainder the prepaid rounding credit never carries into the refund, so a client that rounds the refund down but drops the credit lands on all nine roots: the fixture pinned the fork gate and the rounding direction, not the credit. Run one, two and three non-zero calldata bytes (21_016, 21_032 and 21_048 gas, remainders 1, 2 and 0) against the same three gas limits. With the credit dropped, Celadon over-collects a token unit on three of the six new Celadon cases and misses their roots. The 27 state and logs roots come from morph-geth 5a0d0d771 (go-ethereum#371) and `evm statetest` reads them back from this file. The nine roots that were already here are unchanged. Claude-Session: https://claude.ai/code/session_01WYbNZVUBHa4qCoRK46taTS
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueWarning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
PR title 不符合 Conventional Commit 规范。 合法格式示例:
可用类型:
Breaking Changes 用 |
imbl 7.0.0 depends on imbl-sized-chunks 0.1.3, whose Chunk and InlineArray removal methods can double-free or use-after-free when an element's Drop panics (RUSTSEC-2026-0292). imbl 7.0.2 moves to the fixed imbl-sized-chunks 0.2.0 and drops bitmaps 3.2.1. The dependency comes in through reth-transaction-pool; only Cargo.lock changes.
CodeQL's rust/hard-coded-cryptographic-value query treats an argument bound to a parameter named `nonce` as a cryptographic nonce. Every test that passed a literal account or authorization nonce to these helpers therefore raised a critical alert, each a false positive that had to be dismissed by hand, and every new test added more. Rename the parameters to `tx_nonce` and `auth_nonce`, as the txpool test helpers already do; the query stopped reporting those after the same rename. The nonces the helpers put into transactions and authorizations are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit diff from the v1.3.0 release to the MorphTx v2 / Celadon upgrade (feat/morphtx-v2-eip7702). Covers #209 (CI), #210 (call-mode fee-token alignment), and #211 (MorphTx v2 + EIP-7702 + Celadon hardfork + alt-token refund rounding).