Skip to content

feat: broadcast substrate transfers and swaps as evm logs#1436

Open
mrq1911 wants to merge 20 commits into
masterfrom
synth-evm-logs
Open

feat: broadcast substrate transfers and swaps as evm logs#1436
mrq1911 wants to merge 20 commits into
masterfrom
synth-evm-logs

Conversation

@mrq1911
Copy link
Copy Markdown
Member

@mrq1911 mrq1911 commented May 1, 2026

summary

broadcast substrate-side token movements and trades through the same evm tooling that already works against hydration's evm (eth_getLogs, eth_getTransactionReceipt, etherscan, the graph, dex aggregators).

tres pieces:

  • inline emit from precompiles when an event-producing op runs inside a real eth tx — log lands on the real tx
  • pallet-synthetic-logs: buffers (bucket, emitter, log) entries and on on_finalize builds one synthetic pallet_ethereum::Transaction per execution-context bucket (extrinsic / hook+origin), so eth-rpc serves substrate-origin events like any other tx
  • bucket = frame_system::Phase + pallet_broadcast::ExecutionContext first frame; encoded into the synth tx's nonce so indexers can reverse it back to the substrate origin. canonical tx.hash() is used (no domain-separated digest) — frontier indexes by it

phase 1: orml-tokens transfers + native HDX transfers → erc-20 Transfer log
phase 2: pallet_broadcast::Swapped3 → uniswap v2 Swap log on a stable per-pool address (=filler's evm address)

modified deps

Copilot AI review requested due to automatic review settings May 1, 2026 13:28
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

New crates:

  • pallet-synthetic-logs: v0.2.0

Crate versions that have been updated:

  • runtime-integration-tests: v1.80.0 -> v1.80.1
  • pallet-broadcast: v1.7.0 -> v1.8.0
  • pallet-circuit-breaker: v1.6.3 -> v1.6.4
  • pallet-claims: v3.6.0 -> v3.6.1
  • pallet-collator-rewards: v1.3.0 -> v1.3.1
  • pallet-currencies: v4.1.1 -> v4.1.2
  • pallet-dca: v1.18.1 -> v1.18.2
  • pallet-democracy: v4.5.0 -> v4.5.1
  • pallet-dispenser: v0.4.0 -> v0.4.1
  • pallet-duster: v3.8.0 -> v3.8.1
  • pallet-dynamic-evm-fee: v1.3.0 -> v1.3.1
  • pallet-evm-accounts: v1.6.1 -> v1.6.2
  • pallet-hsm: v1.7.0 -> v1.7.1
  • pallet-lbp: v4.13.0 -> v4.13.1
  • pallet-liquidation: v2.4.2 -> v2.4.3
  • pallet-liquidity-mining: v4.7.0 -> v4.7.1
  • pallet-nft: v7.3.0 -> v7.3.1
  • pallet-omnipool: v7.3.1 -> v7.3.2
  • pallet-omnipool-liquidity-mining: v3.4.0 -> v3.4.1
  • pallet-otc: v2.4.0 -> v2.4.1
  • pallet-otc-settlements: v1.4.1 -> v1.4.2
  • pallet-route-executor: v2.12.0 -> v2.12.1
  • pallet-signet: v1.3.0 -> v1.3.1
  • pallet-stableswap: v7.3.0 -> v7.3.1
  • pallet-staking: v4.3.1 -> v4.3.2
  • pallet-transaction-multi-payment: v10.5.0 -> v10.5.1
  • pallet-transaction-pause: v1.3.0 -> v1.3.1
  • pallet-xyk: v8.2.0 -> v8.2.1
  • pallet-xyk-liquidity-mining: v1.8.0 -> v1.8.1
  • pallet-evm-precompile-call-permit: v0.3.0 -> v0.3.1
  • hydradx-adapters: v1.13.1 -> v1.13.2
  • hydradx-runtime: v412.0.0 -> v413.0.0

Runtime version has been increased.

@mrq1911 mrq1911 marked this pull request as draft May 1, 2026 13:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds infrastructure to expose substrate-originated transfers and swaps as Ethereum-style logs by buffering them into synthetic Ethereum transactions, making them visible through standard eth JSON-RPC log/receipt queries.

Changes:

  • Introduces pallet-synthetic-logs to buffer EVM-shaped logs during block execution and flush them into pallet_ethereum::Pending on on_finalize.
  • Emits ERC-20 Transfer logs from (a) orml-tokens mutation hooks (synthetic txs) and (b) the multicurrency precompile (inline EVM tx logs), with in-EVM suppression to avoid double emission.
  • Adds a pallet_broadcast OnTrade hook and a runtime handler that translates eligible Swapped3 trades into UniswapV2-style Swap logs.

Reviewed changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
runtime/hydradx/src/lib.rs Bumps runtime spec_version and wires pallet_synthetic_logs into the runtime.
runtime/hydradx/src/evm/swap_logs.rs Adds trade→UniswapV2 Swap log translation into synthetic log buffer.
runtime/hydradx/src/evm/runner.rs Adds an EVM execution-frame context flag (is_in_evm) to suppress substrate-side synthetic log hooks during EVM calls.
runtime/hydradx/src/evm/precompiles/multicurrency.rs Emits ERC-20 Transfer log inline from multicurrency precompile calls.
runtime/hydradx/src/evm/mod.rs Exposes new EVM log modules and configures pallet_synthetic_logs.
runtime/hydradx/src/evm/erc20_logs.rs Adds orml-tokens hooks to emit synthetic ERC-20 Transfer logs for substrate-side mutations.
runtime/hydradx/src/assets.rs Hooks orml-tokens mutation points (transfer/deposit/slash) and broadcast trades into the new log emitters.
runtime/hydradx/Cargo.toml Bumps runtime crate version and adds dependencies/features for environmental + pallet-synthetic-logs.
runtime/adapters/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/xyk/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/synthetic-logs/src/tests.rs Adds unit tests for helper encoders/constants in pallet-synthetic-logs.
pallets/synthetic-logs/src/lib.rs Implements the synthetic log buffer, bucketing, and synthetic tx insertion into pallet_ethereum::Pending.
pallets/synthetic-logs/Cargo.toml Adds new pallet crate manifest and feature flags.
pallets/stableswap/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/route-executor/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/otc/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/otc-settlements/src/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/omnipool/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/omnipool-liquidity-mining/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/liquidation/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/lbp/src/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/hsm/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/dca/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/circuit-breaker/src/tests/mock.rs Updates pallet_broadcast::Config to include OnTrade.
pallets/broadcast/src/tests/mock.rs Updates broadcast pallet test runtime config to include OnTrade.
pallets/broadcast/src/lib.rs Adds OnTrade hook trait + runtime config wiring; invokes hook before emitting Swapped3.
pallets/broadcast/Cargo.toml Bumps pallet-broadcast crate version due to config trait change.
integration-tests/src/evm.rs Updates mock precompile handle log() to no-op to accommodate inline log emission.
Cargo.toml Adds pallets/synthetic-logs to workspace members and dependencies.
Cargo.lock Locks new crate and updated dependency graph versions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread runtime/hydradx/src/evm/runner.rs Outdated
Comment on lines +171 to +175
let result = EVM_CONTEXT::using_once(&mut true, || {
R::call(
source,
target,
input,
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

EVM_CONTEXT::using_once(&mut true, || { ... }) takes a mutable reference to a temporary literal, which does not compile (cannot borrow a temporary as mutable). Introduce a local let mut in_evm = true; and pass &mut in_evm (apply the same change to the other using_once(&mut true, ...) calls below).

Copilot uses AI. Check for mistakes.
Comment thread pallets/synthetic-logs/src/lib.rs Outdated
None => groups.push((bucket, vec![log])),
}
}
// stable bucket sort: Hook(Init,*) < Extrinsic(i) < Hook(Final,*)
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The comment about bucket ordering mentions Hook(Init,*) and Hook(Final,*), but Bucket does not encode init vs finalize (and current_bucket() explicitly collapses hook phases). Please update this comment to reflect the actual ordering implemented by bucket_sort_key() so future readers don’t infer non-existent behavior.

Suggested change
// stable bucket sort: Hook(Init,*) < Extrinsic(i) < Hook(Final,*)
// stable bucket sort: all hook buckets sort before extrinsics;
// within hooks, `origin: None` sorts before `origin: Some(_)`;
// extrinsics sort by index.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +21
use crate::Runtime;
use hydradx_traits::evm::InspectEvmAccounts;
use pallet_broadcast::types::{Asset, ExecutionType, Fee, Filler, TradeOperation};
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

InspectEvmAccounts is imported but not used in this module. Please remove the unused import to avoid warnings (especially if CI treats warnings as errors).

Copilot uses AI. Check for mistakes.
use crate::evm::precompiles::erc20_mapping::HydraErc20Mapping;
use crate::Runtime;
use frame_support::sp_runtime::DispatchResult;
use hydradx_traits::evm::{Erc20Mapping, InspectEvmAccounts};
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

InspectEvmAccounts is imported but not used in this module. Please remove the unused import to avoid warnings (especially if CI treats warnings as errors).

Suggested change
use hydradx_traits::evm::{Erc20Mapping, InspectEvmAccounts};
use hydradx_traits::evm::Erc20Mapping;

Copilot uses AI. Check for mistakes.
Comment thread pallets/synthetic-logs/src/lib.rs
@mrq1911 mrq1911 requested a review from enthusiastmartin May 5, 2026 03:08
@mrq1911 mrq1911 marked this pull request as ready for review May 5, 2026 03:08
@mrq1911 mrq1911 changed the title feat: surface substrate transfers and swaps as evm logs feat: broadcast substrate transfers and swaps as evm logs May 5, 2026
Comment thread pallets/synthetic-logs/src/lib.rs
Comment thread pallets/synthetic-logs/src/lib.rs
return;
}
let owner = evm_address_of(who);
push_transfer_log(asset, reserved_address_of(owner), H160::zero(), amount);
Copy link
Copy Markdown
Contributor

@dmoka dmoka May 5, 2026

Choose a reason for hiding this comment

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

slash hook receives the to-be-slashed amount and not the clamped one so if we pass it on to the log, then the log might overstates the amount.

Here is a test to reproduce it:

#[test]
fn orml_tokens_on_slash_log_amount_matches_actually_slashed() {
	TestNet::reset();
	Hydra::execute_with(|| {
		let held = <Tokens as MultiCurrency<AccountId>>::free_balance(DAI, &ALICE.into());
		assert!(held > 0, "test net must seed alice with some DAI for this scenario");
		let overshoot = UNITS;
		let requested = held + overshoot;

		SyntheticLogsPending::<Runtime>::kill();

		let unslashed =
			<Tokens as orml_traits::currency::MultiCurrency<AccountId>>::slash(DAI, &ALICE.into(), requested);

		// Sanity: orml reports the un-slashable remainder, and Alice's balance is now 0.
		assert_eq!(
			unslashed, overshoot,
			"orml should report the un-slashable remainder"
		);
		let post_balance = <Tokens as MultiCurrency<AccountId>>::free_balance(DAI, &ALICE.into());
		assert_eq!(post_balance, 0, "alice should hold zero DAI after the partial slash");

		let asset_addr = HydraErc20Mapping::asset_address(DAI);
		let (_, _, log) = buffered_logs()
			.into_iter()
			.find(|(_, emitter, log)| *emitter == asset_addr && log.topics[2] == h160_to_h256(H160::zero()))
			.expect("synth log for DAI slash");

		let logged_amount = U256::from_big_endian(&log.data);
		assert_eq!(
			logged_amount,
			U256::from(held),
			"synth Transfer amount must equal what was actually slashed ({held}), not the requested amount ({requested})",
		);
	});
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nice, will fix

let nonce = bucket_nonce(bucket);

let signature = ethereum::eip2930::TransactionSignature::new(false, SYNTH_SIG_RS, SYNTH_SIG_RS)
.expect("synthetic signature constants are within valid ECDSA range; qed");
Copy link
Copy Markdown
Contributor

@dmoka dmoka May 5, 2026

Choose a reason for hiding this comment

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

I would try to avoid using expect as it panics

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It shouldn’t ever panic with those constants, but i will change it.

fn current_bucket() -> Bucket {
use frame_system::Phase;
let phase_key = frame_support::storage::storage_prefix(b"System", b"ExecutionPhase");
let phase: Phase = frame_support::storage::unhashed::get::<Phase>(&phase_key).unwrap_or_default();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe using defensive_unwrap_or_default() ?

max_fee_per_gas: U256::zero(),
gas_limit: U256::zero(),
action: TransactionAction::Call(SENTINEL_ADDRESS),
value: U256::from(group_index),
Copy link
Copy Markdown
Contributor

@dmoka dmoka May 5, 2026

Choose a reason for hiding this comment

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

it will produce synth txs with value 1,2,3,etc wei-s which might confuse indexers tracking native token flow?!

maybe input field is better to set uniqeness

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point

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.

3 participants