Skip to content

Add support for Celo's stateful transfer precompile #11209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

karlb
Copy link

@karlb karlb commented Aug 5, 2025

Motivation

The Celo blockchain makes use of a stateful transfer precompile. Implementing this in Anvil allows running more of our tests in Anvil.

This PR is clearly far from being mergeable, but it serves two purposes:

Solution

The PrecompilesMap doesn't support stateful precompiles, so we need to use a PrecompileProvider instead that has support for stateful precompiles. Changing the typing to only depend on the PrecompileProvider trait (which would allow passing PrecompilesMap or a statefule PrecompileProvider on a case-by-case bases) proved difficult, so I instead added a HybridPrecompileProvider which has support for stateful precompiles but can also pass along the precompiles from a PrecompilesMap.
The HybridPrecompileProvider implementation is also very limited due to additional typing problems described in the comment.

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

@karlb karlb changed the title Karlb/celo transfer precompile Add support for Celo's stateful transfer precompile Aug 5, 2025
Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

supportive, because this can serve as a reference for additional precompile support.

left some pointers, re reusing the PrecompilesMap and inject dynamically.

we can likely clean up a few more things as well,

perhaps rethink how and where we call

let mut evm = new_evm_with_inspector(&mut *self.db, &env, &mut inspector);
if self.odyssey {
inject_precompiles(&mut evm, vec![(P256VERIFY, P256VERIFY_BASE_GAS_FEE)]);
}
if let Some(factory) = &self.precompile_factory {
inject_precompiles(&mut evm, factory.precompiles());
}

ideally we only have these in one location

Comment on lines +604 to +606
/// Run a Celo chain
#[arg(long)]
pub celo: bool,
Copy link
Member

Choose a reason for hiding this comment

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

hmm, not convinced we want to add a new arg for every network that we can add custom precompiles for.

should we perhaps derive this from the chainid, although an additional celo flag doesnt really hurt

Copy link
Author

Choose a reason for hiding this comment

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

If we only rely on the chainid, new networks won't work with anvil until they have been explicitly supported. And getting PRs with config for short-lived and local networks sounds annoying to me. Adding a flag seems the lesser evil to me.

We can still automatically detect the flags for the most important networks based on the chainid. But if we need a flag anyway, that falls into the "nice to have" category.

Copy link
Author

Choose a reason for hiding this comment

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

We could also enable the transfer precompile by setting --hardfork cel2, but I'm not sure if adding chain-specific hardforks is better than adding a flag for the chain itself.

Copy link
Member

Choose a reason for hiding this comment

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

I believe this is now redundant, because we can now dynamically inject precompiles into the regular evm with PrecompilesMap via

evm.precompiles_mut().apply_precompile(&addr, move |_| {
Some(DynPrecompile::from(move |input: PrecompileInput<'_>| func(input.data, gas)))

Copy link
Author

@karlb karlb Aug 6, 2025

Choose a reason for hiding this comment

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

In its current form, this can only be used to inject stateless precompiles. The PrecompileWithAddress that is passed into this function does not take any context/state as parameter:

pub struct PrecompileWithAddress(pub Address, pub PrecompileFn);
pub type PrecompileFn = fn(&[u8], u64) -> PrecompileResult;

But now that PrecompilesMap has gained a settable lookup function, I can try to use that to include a stateful precompile. Unfortunately, the EvmInternals in the PrecompileInput don't provide access to the journal, so that I can't use the journal's transfer method like I do in this PR. I might be able to use load_account instead, since it returns a mutable Account.

Copy link
Author

Choose a reason for hiding this comment

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

I updated the code with this approach and it looks much better now.

@karlb karlb force-pushed the karlb/celo-transfer-precompile branch from 4465f77 to 64f1632 Compare August 6, 2025 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

2 participants