Skip to content

Expose native UNIT as an ERC20 precompile - #86

Merged
motoZ-crypto merged 1 commit into
masterfrom
motoz/balances-erc20
May 14, 2026
Merged

Expose native UNIT as an ERC20 precompile#86
motoZ-crypto merged 1 commit into
masterfrom
motoz/balances-erc20

Conversation

@motoZ-crypto

@motoZ-crypto motoZ-crypto commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add a new balances-erc20 precompile that exposes the native UNIT token through an ERC20 interface at 0x0000000000000000000000000000000000000802
  • implement ERC20 metadata, balance queries, transfers, approvals, transferFrom, WETH-style deposit handling, withdraw to arbitrary AccountId32, and EIP-2612 permit flows
  • register the precompile in the runtime, add the new workspace crate, refresh related dependencies, and cover the integration with unit and runtime tests

Summary by CodeRabbit

  • New Features
    • Added native token ERC20 precompile support at address 0x0802 with standard token operations (transfer, approve, balanceOf, etc.)
    • Implemented EIP-2612 permit functionality enabling gasless token approvals via cryptographic signatures
    • Added cross-domain transfer helpers for moving native funds between EVM and Substrate

Review Change Stack

@motoZ-crypto motoZ-crypto added this to the Alpha-0.2 milestone May 14, 2026
@motoZ-crypto motoZ-crypto self-assigned this May 14, 2026
@motoZ-crypto motoZ-crypto added T-enhancement New feature or request A-evm Frontier EVM integration labels May 14, 2026
@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a complete ERC-20 precompile that exposes the chain's native balance token through standard ERC20 methods and EIP-2612 gasless approvals, integrates it into the runtime's EVM execution layer, and provides comprehensive unit and integration tests.

Changes

ERC-20 Balances Precompile with EIP-2612

Layer / File(s) Summary
Workspace configuration and crate manifest
Cargo.toml, precompiles/balances-erc20/Cargo.toml
Adds precompiles/balances-erc20 to workspace members, updates workspace dependencies (derive_more, hex-literal, similar-asserts), and establishes the new crate manifest with core EVM and cryptographic dependencies.
EIP-2612 permit and EIP-712 signature logic
precompiles/balances-erc20/src/eip2612.rs
Implements EIP-2612 permit() for gasless approvals, including EIP-712 domain separator/struct hash computation, secp256k1 signature recovery via ecrecover, nonce increment for replay prevention, and approval event emission.
ERC20 precompile implementation with storage
precompiles/balances-erc20/src/lib.rs
Implements standard ERC20 methods (balanceOf, transfer, approve, transferFrom, totalSupply, decimals, name, symbol), cross-domain withdraw() helper, WETH9-style deposit() refund semantics, deterministic storage maps for allowances and nonces, and EIP-2612 delegation.
Mock runtime and test harness setup
precompiles/balances-erc20/src/mock.rs
Constructs a complete FRAME test runtime with System, Timestamp, Balances, and EVM pallets; configures pallet\_evm with the ERC20 precompile at 0x0802; provides NativeErc20Metadata for UNIT token and ExtBuilder for test setup.
ERC20 method unit tests
precompiles/balances-erc20/src/tests.rs (lines 1–672)
Validates ERC20 read/write methods, dispatcher routing, fallback handling, selector pinning, multi-account scenarios, allowance round-trips with U256::MAX, and WETH-style deposit refund with zero-value rejection.
EIP-2612 permit tests
precompiles/balances-erc20/src/tests.rs (lines 673–900)
Tests nonces queries, domain separator computation, valid permit allowance setting with log emission, invalid nonce/signature/deadline rejections, and millisecond-boundary deadline handling.
Runtime precompile dispatch and integration test
runtime/Cargo.toml, runtime/src/configs/evm.rs, runtime/tests/evm.rs
Adds dependency on the new precompile crate, defines NativeErc20Metadata in the runtime, updates the precompile registry to include address 0x0802, adds required trait bounds for timestamp/account/balance conversions, and validates end-to-end balanceOf/transfer execution via Runner::call.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • motoZ-crypto/crypto-node#82: Updates the Frontier EVM precompile registry in runtime/src/configs/evm.rs to coordinate precompile address allocation; this PR adds the balances-erc20 precompile to that same registry.

Suggested labels

A-runtime

Poem

🐰 A token once locked in the Substrate deep,
Now wakes as ERC20 with secrets to keep.
With permits and nonces, it flows swift and free—
From native to virtual, a bridge you will see!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Expose native UNIT as an ERC20 precompile' directly and accurately summarizes the main change: implementing an ERC20 interface for the native UNIT token as a precompile at 0x0802.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch motoz/balances-erc20

Comment @coderabbitai help to get the list of available commands and usage tips.

@deepsource-io

deepsource-io Bot commented May 14, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 2aab0c5...9411143 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Rust May 14, 2026 7:46p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
precompiles/balances-erc20/src/lib.rs (1)

318-349: ⚡ Quick win

Zero-value handling is inconsistent across operations.

The deposit() function rejects zero-value calls (lines 328-330) to help catch mistakes, but withdraw() (lines 294-297) and do_transfer() (lines 395-398) both silently allow zero-value operations. This creates an inconsistency:

  • Standard ERC20 allows zero-value transfers
  • WETH9 allows zero-value deposits
  • This precompile rejects zero-value deposits but allows zero-value withdrawals/transfers

Users expecting WETH9-compatible behavior may be surprised. Consider either:

  1. Allowing zero-value deposits for consistency, or
  2. Documenting this deliberate deviation prominently in user-facing docs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@precompiles/balances-erc20/src/lib.rs` around lines 318 - 349, The deposit()
function currently reverts on zero-value while withdraw() and do_transfer()
allow it; make deposit() behavior consistent by removing the zero-value revert
in deposit() (the branch checking value.is_zero()) and instead treat a
zero-value deposit as a no-op (return Ok(()) or let execution proceed without
transferring funds but still record logs if desired), aligning deposit() with
withdraw() and do_transfer(); reference deposit(), withdraw(), and do_transfer()
when updating the logic so tests and docs can be adjusted if needed.
precompiles/balances-erc20/src/eip2612.rs (1)

54-59: 💤 Low value

Large deadline values become effectively non-expiring.

If a permit is signed with deadline > u64::MAX (line 56), the conversion uses unwrap_or(u64::MAX), making the permit valid until the year ~584 billion. While this is far-future and safe (doesn't panic), it could be surprising if tooling accidentally generates oversized deadlines.

Consider documenting this behavior or capping deadlines more explicitly if non-expiring permits are not intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@precompiles/balances-erc20/src/eip2612.rs` around lines 54 - 59, The current
permit deadline conversion silently maps out-of-range values to u64::MAX (see
the conversion of `deadline` into `deadline_secs` near `now_ms`/`now_secs` using
`unwrap_or(u64::MAX)`), which makes oversized deadlines effectively
non-expiring; change this to either (a) explicitly reject overflowed/invalid
deadlines by returning an error instead of using `unwrap_or`, or (b) clamp the
converted deadline to a well-documented MAX_DEADLINE constant and add a comment
documenting that behavior; update the conversion logic where
`deadline.try_into().unwrap_or(u64::MAX)` is used (and any related checks before
`revert("Permit expired")`) and ensure unit tests reflect the chosen behavior.
precompiles/balances-erc20/src/tests.rs (1)

698-700: ⚡ Quick win

Use an independent EIP-712 vector for at least one permit path assertion.

These expectations are derived from the same helper functions used by runtime logic, so a shared hashing bug can still pass. Add one fixed external vector (known domain separator + digest/signature outcome) to decouple the test oracle.

Also applies to: 725-732

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@precompiles/balances-erc20/src/tests.rs` around lines 698 - 700, The test
currently computes the domain separator and digest using the same helpers as the
runtime (compute_domain_separator, compute_permit_struct_hash,
compute_eip712_digest with PRECOMPILE_ADDRESS and NativeErc20Metadata::NAME), so
add at least one assertion that uses an independent, fixed EIP‑712 test vector:
hardcode a known domain separator and expected digest/signature outcome for a
specific owner/spender/value/nonce/deadline and assert equality against the
computed values; update the same pattern used around the other permit assertions
to use this external vector to ensure a hashing bug in the helpers cannot make
the test pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@precompiles/balances-erc20/src/eip2612.rs`:
- Around line 54-59: The current permit deadline conversion silently maps
out-of-range values to u64::MAX (see the conversion of `deadline` into
`deadline_secs` near `now_ms`/`now_secs` using `unwrap_or(u64::MAX)`), which
makes oversized deadlines effectively non-expiring; change this to either (a)
explicitly reject overflowed/invalid deadlines by returning an error instead of
using `unwrap_or`, or (b) clamp the converted deadline to a well-documented
MAX_DEADLINE constant and add a comment documenting that behavior; update the
conversion logic where `deadline.try_into().unwrap_or(u64::MAX)` is used (and
any related checks before `revert("Permit expired")`) and ensure unit tests
reflect the chosen behavior.

In `@precompiles/balances-erc20/src/lib.rs`:
- Around line 318-349: The deposit() function currently reverts on zero-value
while withdraw() and do_transfer() allow it; make deposit() behavior consistent
by removing the zero-value revert in deposit() (the branch checking
value.is_zero()) and instead treat a zero-value deposit as a no-op (return
Ok(()) or let execution proceed without transferring funds but still record logs
if desired), aligning deposit() with withdraw() and do_transfer(); reference
deposit(), withdraw(), and do_transfer() when updating the logic so tests and
docs can be adjusted if needed.

In `@precompiles/balances-erc20/src/tests.rs`:
- Around line 698-700: The test currently computes the domain separator and
digest using the same helpers as the runtime (compute_domain_separator,
compute_permit_struct_hash, compute_eip712_digest with PRECOMPILE_ADDRESS and
NativeErc20Metadata::NAME), so add at least one assertion that uses an
independent, fixed EIP‑712 test vector: hardcode a known domain separator and
expected digest/signature outcome for a specific
owner/spender/value/nonce/deadline and assert equality against the computed
values; update the same pattern used around the other permit assertions to use
this external vector to ensure a hashing bug in the helpers cannot make the test
pass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a61d7046-5102-4ea0-bbf9-a4f816dcd11f

📥 Commits

Reviewing files that changed from the base of the PR and between 2aab0c5 and 9411143.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • Cargo.toml
  • precompiles/balances-erc20/Cargo.toml
  • precompiles/balances-erc20/src/eip2612.rs
  • precompiles/balances-erc20/src/lib.rs
  • precompiles/balances-erc20/src/mock.rs
  • precompiles/balances-erc20/src/tests.rs
  • runtime/Cargo.toml
  • runtime/src/configs/evm.rs
  • runtime/tests/evm.rs

@motoZ-crypto
motoZ-crypto merged commit 6865289 into master May 14, 2026
2 of 4 checks passed
@motoZ-crypto
motoZ-crypto deleted the motoz/balances-erc20 branch July 5, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-evm Frontier EVM integration T-enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant