feat(precompiles)!: migrate to new API
Motivation
Improve the devex by introducing an API that feels like native Rust and, at the same time, closer to the usual Solidity patterns.
This PR closes #860 and achieves the final goal of #856
Solution
The type system changes of this PR have been proposed in its own PR #986 and have already been extensively reviewed. The feedback from the several review rounds has been incorporated and re-reviewed.
However, PR #986 can't be directly merged into main because it uses a new storage access pattern, and it also changes the precompiles API. These breaking changes made it impossible to merge the storage type system changes without a full migration.
Reviewing this PR
If you haven't been involved in the storage type system PR review, you are more than welcome (even encouraged) to have a look at the files under crates/precompiles/src/storage/ or at the #986 PR (which is around 8k diff, although most of it are tests).
With that being said, the main focus of this PR review should be the migration of the precompiles to the new API, as well as their integration within the rest of the crates in the monorepo.
Out of the ~10k diff (excluding the storage type system changes), most of the diff are because of tests.
Please review super carefully the implementation changes, as they should preserve the logic untouched.
Using the new storage API
unlike previously, where we had to hold a mutable reference to the storage provider (type implementing trait PrecompileStorageProvider), we now use the scoped_tls crate to generate a scoped thread-local variable. In practice, this means that our storage interactions will be done using the zero-sized StorageContext need to use the following pattern:
let mut provider = EvmPrecompileStorageProvider::new();
// we must always "enter" to setup the storage provider as a thread-local variable
StorageContext::enter(&mut provider, || {
// inside the closure, we can access all the `trait PrecompileStorageProvider` fns via `StorageContext`
// which performs them atomically. Thus, precompiles don't need to hold storage refs, and we won't have
// mutable ref borrow conflicts anymore.
let current_time = StorageContext.timestamp();
let spec = StorageContext.sepc();
let path_usd = PathUSD::new();
path_usd.grant_role_internal(admin, *ISSUER_ROLE)?;
let token = TIP20Token::new(1);
token.grant_role_internal(admin, *ISSUER_ROLE)?;
});
Using the new precompiles API
precompiles now use handlers for interacting with storage, rather than low-level helper functions like before. In pratice, this means that we can consume them with a solidity-like devex:
#[contract]
pub struct TIP20Token {
// ...
total_supply: U256,
balances: Mapping<Address, U256>,
allowances: Mapping<Address, Mapping<Address, U256>>,
// ...
}
impl TIP20Token {
fn get_allowance(&self, owner: Address, spender: Address) -> Result<U256> {
self.allowances.at(owner).at(spender).read()
}
fn set_allowance(&mut self, owner: Address, spender: Address, amount: U256) -> Result<()> {
self.allowances.at(owner).at(spender).write(amount)
}
}
handlers have powerful APIs provide us better qol:
// Base handler for all storage operations. Other handlers delegate to `Slot`.
struct Slot<T> {
pub fn read(&self) -> Result<T>
pub fn write(&mut self, value: T) -> Result<()>
pub fn delete(&mut self) -> Result<()>
}
// Returns a handler for the given key.
// For nested mappings (Mapping<K1, Mapping<K2, V>>), returns another Mapping.
struct Mapping<K, V> {
pub fn at(&self, key: K) -> <V as StorableType>::Handler
}
// Generated by `#[derive(Storable)]`, its fields are handlers.
// This means that we can load each field individually
struct MyStructHandler {
pub foo: Slot<Address>,
pub bar: Mapping<u64, U256>,
}
impl Handler<MyStruct> for MyStructHandler {
pub fn read(&self) -> Result<MyStruct>
pub fn write(&mut self, value: MyStruct) -> Result<()>
pub fn delete(&mut self) -> Result<()>
}
// Fixed-size array. Element access is checked (returns an option).
struct ArrayHandler<T, N> {
pub fn read(&self) -> Result<[T; N]>
pub fn write(&mut self, value: [T; N]) -> Result<()>
pub fn delete(&mut self) -> Result<()>
pub fn at(&self, index: usize) -> Option<<T as StorableType>::Handler>
pub const fn len(&self) -> usize
}
// Dynamic array with stack-like ops. Supports multi-slot element types.
struct VecHandler<T> {
pub fn read(&self) -> Result<Vec<T>>
pub fn write(&mut self, value: Vec<T>) -> Result<()>
pub fn delete(&mut self) -> Result<()>
pub fn at(&self, index: usize) -> <T as StorableType>::Handler
pub fn len(&self) -> Result<usize>
pub fn push(&self, value: T) -> Result<()>
pub fn pop(&self) -> Result<Option<T>>
}
TODO
- [x] migrate precompiles to the new storage API
- [X] update other crates to the new storage API
- [x] ensure all tests pass
- [ ] ensure gas snapshot tests pass
@0xrusowsky must be a member of the Tempo team on Vercel to deploy. - Click here to add @0xrusowsky to the team. - If you initiated this build, request access.
Learn more about collaboration on Vercel and other options here.
@klkvr must be a member of the Tempo team on Vercel to deploy. - Click here to add @klkvr to the team. - If you initiated this build, request access. - If you're already a member of the Tempo team, make sure that your Vercel account is connected to your GitHub account.
Learn more about collaboration on Vercel and other options here.
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
| Project | Deployment | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| tempo-docs | Preview | Dec 11, 2025 10:10pm |