Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .agents/skills/emv-maintainer/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
name: evm-maintainer
description: Maintain the EVM precompiles in backwards compatible way with API versioning.
---

# EVM Precompile Maintainer

You are the maintainer of EVM precompiles. EVM precompiles in subtensor should expose everything that's available to client applications to EVM smart contracts: Extrinsics, state maps and variables in read-only mode, RPCs, and events that originate from hooks. These events should be reported to the subscribed smart contracts as callbacks. Your job is to make sure that this requirement holds with every update, but at the same updating something should not break things that existed before because some existing deployed smart contracts may rely on the existing ABIs. Read the notes below and then execute steps.

## Reference routing

- Before classifying or implementing any precompile change, including an
additive function, runtime adaptation, bug fix, deprecation, or disablement,
read [ABI versioning](references/abi-versioning.md).
- When reviewing hook events or callback precompiles, read
[Event subscriptions](references/event-subscriptions.md).
- Before implementing or reviewing precompile coverage and tests, read
[Coverage and testing](references/coverage-and-testing.md).

## Backwards compatibility

Treat every released precompile as a permanent public API. Preserve the ability
of deployed contracts, including immutable and externally audited wrappers, to
keep working across runtime upgrades without changing their source code,
bytecode, configured precompile addresses, or calldata.

Compatibility covers observable behavior, not merely the continued existence
of a four-byte selector. Preserve the documented meaning of the call whenever
that meaning can still be represented honestly and safely.

For each affected released function:

1. Preserve the old interface and meaning through the existing implementation
or a bounded adapter whenever possible.
2. Add a versioned function when the new behavior needs different inputs,
outputs, or semantics. Keep the old address and selector routed.
3. Use soft deprecation, which marks a function as deprecated while preserving
its released behavior, by default. Never fabricate data or silently
reinterpret an old field to avoid a compatibility decision.
4. If hard deprecation may be necessary, stop and follow the mainnet release
warning and lifecycle process in
[ABI versioning](references/abi-versioning.md). A general request to update
precompiles does not authorize an early compatibility break.
5. Prove that legacy callers still work and that unrelated precompiles and ABIs
are unchanged by following
[Coverage and testing](references/coverage-and-testing.md).

## Notes on coding precompiles

- Never allow direct writing of state maps or variables to precompile callers.
- Keep every precompile path O(1) in CPU and memory.
- Follow [ABI versioning](references/abi-versioning.md) for every released
interface.
- Do not use Ethereum reserved precompile addresses for subtensor functionality.
- Follow the code style and established patterns in existing precompiles.
- Represent Substrate account IDs in EVM space as 32-byte public keys.
- Multiply Subtensor balances by `10^9` to match EVM's 18-decimal convention,
and divide by the same factor before passing balances to Subtensor pallets.
Comment on lines +88 to +89

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.

[HIGH] Do not apply 18-decimal scaling to every precompile balance

This blanket rule remains unsafe: released precompiles can define different units, and applying 10^9 universally can change economic values by that factor or double-convert an existing interface. Require each function to preserve its documented ABI units and make any conversion explicit and tested.

Suggested change
- Multiply Subtensor balances by `10^9` to match EVM's 18-decimal convention,
and divide by the same factor before passing balances to Subtensor pallets.
- Preserve each released function's documented balance units, precision, scaling,
and rounding. For new interfaces, define and test conversions per function; do not assume every balance uses 18-decimal EVM units.

- Follow [Event subscriptions](references/event-subscriptions.md) for callback
interfaces, charging, bounds, and delivery.

## Step 1 - Review current precompiles vs. subtensor functionality

Comment on lines +91 to +92

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.

[HIGH] Do not apply 18-decimal scaling to every precompile balance

This contradicts the deployed Staking V2 contract: explicit amount_rao and amount_alpha parameters are passed to the pallet in native units without BalanceConverter. Following this instruction would multiply or divide economic values by 10⁹ and could break new functions or adapters. Scaling must follow each released function's unit contract, not a blanket rule.

Suggested change
## Step 1 - Review current precompiles vs. subtensor functionality
- Preserve each released function's documented unit contract. Use `BalanceConverter`
only for interfaces defined in 18-decimal EVM units; pass parameters defined
in native rao or alpha units through without decimal scaling.

- All extrinsics should be exposed to precompile callers for the following pallets:

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.

[HIGH] Do not apply 18-decimal scaling to every precompile balance

This blanket rule conflicts with deployed interfaces such as BalancePrecompile::getFreeBalance, which returns the underlying Rao value directly. Following it would multiply some established results by 10⁹ or divide inputs that already use runtime units, breaking ABI semantics and potentially causing severe economic miscalculations. Require preservation of each released function's existing unit convention, and use the EVM balance converter only where that function's ABI calls for 18-decimal values.

Suggested change
- All extrinsics should be exposed to precompile callers for the following pallets:
- Preserve each released function's documented units, scaling, precision, and rounding.
Use the runtime EVM balance converter only where that function's ABI requires 18-decimal values.

- subtensor
Comment on lines +94 to +95

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.

[HIGH] Do not apply 18-decimal scaling to every precompile balance

This blanket rule conflicts with released interfaces that explicitly use rao (for example, staking parameters named amount_rao) and with the repository documentation stating that precompile amounts are rao rather than 18-decimal wei. Following it would multiply returned values or divide inputs by 10^9, breaking existing contracts and potentially changing transferred/staked economic amounts. Require unit handling to follow each released ABI; only apply scaling where that specific interface already defines 18-decimal values.

Suggested change
precompile callers for the following pallets:
- subtensor
- Preserve the units, precision, scaling, and rounding defined by each released ABI.
Do not apply a blanket decimal conversion: use 18-decimal scaling only where
that specific interface already requires it, and preserve rao-denominated fields.

Comment on lines +94 to +95

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.

[HIGH] Do not apply 18-decimal scaling to every precompile balance

This blanket rule conflicts with the skill's compatibility requirements and existing released ABIs: for example, IBalance.getFreeBalance and staking calls explicitly expose amounts in rao, while the runtime's 10^9 converter applies to native EVM account balances. Following this instruction would multiply existing precompile results or divide their inputs by 10^9, breaking callers and potentially changing transferred or staked value. Require each function to preserve its released units; use scaling only where that function's established ABI explicitly requires 18-decimal EVM units.

Suggested change
precompile callers for the following pallets:
- subtensor
- Preserve the units and scaling defined by each released precompile ABI; several
existing interfaces expose balances directly in rao. Apply `10^9` conversion
only where the specific interface explicitly uses 18-decimal EVM units.

- admin-util
- balances
- proxy
- scheduler
- drand
- crowdloan
- timestamp
- swap
- All runtime API RPCs for the subtensor pallet should be exposed as a callable precompile function with similar interface
- All events emitted from hooks (such as on_initialize or on_finalize) should be exposed as callbacks.

Use [Coverage and testing](references/coverage-and-testing.md) to build the
inventory and distinguish deployed, partial, proposed, and missing coverage.

## Step 2 — Determine the diff

Determine the diff between current branch and the most recent main branch (may need to pull it locally if it is outdated). See how this diff affects EVM precompiles:

- Does it remove or change any functions that precompiles rely on? Does it change function signatures or underlying functionality?
- Does it add any new functionality (extrinsics, RPCs, state maps and variables, hook events)?

## Step 3 - Handle changed functions

Apply the backwards-compatibility decision rule above and the detailed
[ABI versioning](references/abi-versioning.md) process. Preserve released
behavior through a bounded adapter and add a versioned function for new
behavior. If preservation is impossible, dishonest, unbounded, or unsafe, stop
and report the release blocker; do not implement an immediate compatibility
break as an ordinary precompile update.

## Step 4 - Handle added functions

Determine the category under which the new functionality needs to be added and add to the corresponding existing precompile. You may create a new precompile too if the category does not fall into any existing ones.

## Step 5 - Update precompile documentation

Update the Solidity interface, generated ABI, NatSpec, registry metadata, SDK
copies, and public precompile documentation together. Verify their agreement
and ensure unrelated precompile artifacts remain unchanged.
249 changes: 249 additions & 0 deletions .agents/skills/emv-maintainer/references/abi-versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# ABI versioning and lifecycle

## Contents

- [Establish the released baseline](#establish-the-released-baseline)
- [Preserve the external contract](#preserve-the-external-contract)
- [Reserve addresses and selectors](#reserve-addresses-and-selectors)
- [Version functions within a domain](#version-functions-within-a-domain)
- [Preserve old behavior through adapters](#preserve-old-behavior-through-adapters)
- [Classify changes](#classify-changes)
- [Apply the lifecycle model](#apply-the-lifecycle-model)
- [Stop an undeployed compatibility break](#stop-an-undeployed-compatibility-break)
- [Report lifecycle and availability](#report-lifecycle-and-availability)
- [Handle reversible disablement](#handle-reversible-disablement)

## Establish the released baseline

Before changing a precompile:

1. Determine which addresses, selectors, Solidity interfaces, and ABI files
have been deployed or published for production use. Inspect release history
and the deployed runtime, not only the working tree.
2. Inspect `precompiles/src/lib.rs`, the Rust implementation,
`precompiles/src/solidity/*.sol`, generated `*.abi` files, tests, public
documentation, SDK copies, and known integration contracts.
3. Compare the branch with the relevant base and identify every runtime change
that affects inputs, outputs, state changes, errors, authorization, units,
value handling, or gas and weight requirements.
4. Treat uncertain production status as released until evidence establishes
otherwise.
5. Distinguish released interfaces from explicit proposals. Allow an
unassigned, unpublished proposal to change during design review; freeze its
address, selectors, and observable behavior once released.

Do not infer compatibility from Rust names. Define the external contract as the
fixed EVM address plus accepted calldata, returned bytes, state effects,
authorization, charging, and success-or-revert behavior.

## Preserve the external contract

Preserve all observable properties of every released call:

- address and selector handling;
- function name, input types, input order, and ABI encoding;
- return types, tuple and struct field order, and ABI encoding;
- documented meaning, units, precision, scaling, rounding, and defaults;
- view, state-changing, payable, and static-call behavior;
- treatment of attached EVM value;
- caller-to-Substrate account mapping and dispatched origin;
- authorization and proxy behavior;
- state transitions and atomicity;
- success-versus-revert behavior and documented error payloads;
- bounded-input and complexity guarantees;
- callback selectors, event-mask assignments, filters, charging,
auto-unsubscription, sequencing, and delivery guarantees.

Return types do not contribute to a Solidity selector, but changing them under
an existing selector still breaks old callers because they decode the returned
bytes with the old ABI.

Allow internal Rust names, storage layouts, hashers, intermediate types, and
algorithms to change only when the implementation adapts them back to the
released behavior.

Allow runtime weight corrections, but preserve the complexity class and input
bounds. Do not introduce an unannounced increase large enough to make a
previously practical call unusable. Never replace bounded work with an
unbounded scan.

## Reserve addresses and selectors

Keep every released precompile address recognized by the precompile set.
Preserve compatible handling at that address: existing calldata must still
reach behavior that honors its released contract. The internal Rust type or
dispatch structure may change; the observable routing contract may not.

Keep every released selector reserved permanently, including after hard
deprecation. Route a hard-deprecated selector to its descriptive error. Never
allow a different function to claim it.

Before adding a function, calculate its selector from the canonical Solidity
signature and compare it with the complete selector set at the address. Reject
collisions even when the Solidity names differ.

Treat a new function as additive only when:

- its selector does not collide;
- old input and output encodings remain identical;
- unknown-selector and fallback behavior remain unchanged;
- old results and side effects remain unchanged; and
- no unrelated Solidity interface or ABI changes.

## Version functions within a domain

Prefer one fixed address for each coherent domain. Add versions at that address:

```text
functionName
functionNameV2
functionNameV3
```

Keep every earlier version routed. Use a new address only for a genuinely
different domain with an independent responsibility and lifecycle.

Continue supporting legacy addresses created under earlier per-contract
versioning. Do not use them as a precedent for creating a new address whenever
one function changes.

Do not attempt a return-type-only overload. Because return types do not
distinguish selectors, use a versioned name or a genuinely distinct input
signature.

When an audited integration expects a missing chain value or operation, prefer
adding the typed function it expects to the appropriate existing precompile.
Do not require changes to an audited wrapper when the precompile can satisfy
the wrapper's existing interface safely.

## Preserve old behavior through adapters

Adapt released calls to new runtime representations whenever the old result can
still be produced honestly with bounded, proportionate work:

- Reconstruct an old aggregate when one stored value becomes several.
- Return the original tuple when a struct gains fields; expose the extended
tuple through a new version.
- Update Rust storage access when names, keys, hashers, or map shapes change.
- Supply the exact old default when an extrinsic gains an option; expose the
option through a new version.
- Derive the documented old result when the runtime replaces its computation.
- Preserve legacy units, precision, scaling, and rounding in the old function;
expose a corrected convention through a new version.

Do not fabricate data to retain a byte shape. Do not reinterpret an old field
as a different concept. If an adapter cannot preserve the documented meaning,
make an explicit lifecycle decision.

## Classify changes

| Runtime change | Required treatment |
|---|---|
| Storage rename, hasher change, or map restructuring | Update the Rust implementation; preserve ABI and meaning. |
| Equivalent internal computation refactor | Keep the function and verify equivalent observable results. |
| Additional returned information | Keep the old subset; add a version for the richer result. |
| Input or return type/order change | Add a version with a new selector. |
| One concept splits into several | Reconstruct the old aggregate when honest; expose components through a version. |
| Extrinsic gains an option | Preserve the old default; expose the option through a version. |
| Entirely new operation or view | Add a selector to the appropriate domain. |
| Concept disappears without an honest representation | Reserve the selector and evaluate hard deprecation. |
| Bug fix changes observable semantics | Preserve the released behavior and add a corrected version unless retaining it is unsafe. |
| Urgent security or operational risk | Report the risk and consider whether reversible disablement should be recommended. |

For a security-critical behavior that cannot remain callable, stop and report
the compatibility break. Do not silently change or delete the selector.

## Apply the lifecycle model

Keep function lifecycle separate from precompile availability:

| Condition | Required call behavior |
|---|---|
| Active and enabled | Execute normally. |
| Soft-deprecated and enabled | Preserve the documented behavior and encoding. |
| Hard-deprecated and enabled | Keep routing the selector and return a descriptive precompile error. |
| Disabled | Return the precompile-disabled error regardless of function lifecycle. |

Use soft deprecation by default. Preserve the call, mark the Solidity function
with `@deprecated`, and publish replacement metadata without adding
deprecation-only work to every invocation.

Use hard deprecation only when old behavior cannot be represented honestly or
safely, for example because:

- the underlying concept no longer exists and has no representation;
- the semantics changed beyond what the old return type can describe; or
- preservation requires fabricated data, dead state, unbounded work, or an
unacceptable security risk.

Do not hard-deprecate because a replacement is newer, easier to maintain, or
more complete. First document why an adapter is impossible or disproportionate,
identify affected released functions and known callers, provide a replacement
when possible, and complete the agreed migration process.

## Stop an undeployed compatibility break

If the runtime change that makes old behavior impossible has not reached
mainnet, treat mainnet deployment as blocked by the compatibility break. Do not
interpret a request to update precompiles as authorization to deploy the break
or hard-deprecate affected functions immediately.

Stop and give the developer this prominent warning:

> **Mainnet compatibility warning:** This change would force hard deprecation
> of `<precompile address, function, and selector>` and break contracts that
> still call it. Do not deploy the incompatible runtime change to mainnet until
> `<replacement>` is available, the old function has been soft-deprecated for
> the agreed migration window, and the phase-out criteria have been satisfied.

State why an adapter cannot work, which released functions and known callers
are affected, what replacement is available or required, and which phase-out
steps remain. Continue only with non-breaking preparation such as adding the
replacement, tests, documentation, and lifecycle metadata. Preserve current
mainnet behavior throughout the migration window. Hard-deprecate only in the
later release that completes the planned phase-out.

## Report lifecycle and availability

Use this proposed registry shape as the compatibility target:

```solidity
struct PrecompileStatus {
bool isDeprecated;
bool isDisabled;
address newPrecompile;
bytes4 newSelector;
string message;
}
```

Interpret `isDeprecated` as soft or hard function deprecation. Interpret
`isDisabled` as current unavailability through a reversible operational switch.
Use `newPrecompile` and `newSelector` for the recommended replacement; zero
replacement fields mean that none is available. Use `message` for
human-readable status or migration guidance.

Do not infer deprecation from disablement. Do not clear deprecation when a
precompile is re-enabled. Do not describe the registry as callable until its
address and implementation are released.

Keep registry metadata, Solidity NatSpec, public documentation, and call
behavior consistent. Prefer static registry queries over emitting a log on
every deprecated call.

## Handle reversible disablement

Treat disablement as an external, reversible operational action, not a normal
deprecation step. An agent may identify a risk, verify the mechanism, and
recommend that responsible decision-makers consider it. An agent cannot
perform or authorize the action.

Require re-enablement to restore each function's previous active,
soft-deprecated, or hard-deprecated behavior. Never erase lifecycle metadata
when availability changes.

Before recommending disablement, verify that the address routes through
`PrecompileExt::try_execute` and uses the intended `PrecompileEnum` entry.
Check whether multiple addresses share that entry and report the complete
effect of a toggle. Do not claim an address is toggleable merely because the
general mechanism exists.
Loading
Loading