Skip to content

feat: add Precompiles trait #689

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
29 changes: 17 additions & 12 deletions contracts/src/token/erc20/extensions/permit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ use stylus_sdk::{block, call::MethodError, function_selector, prelude::*};
use crate::{
token::erc20::{self, Erc20},
utils::{
cryptography::{
ecdsa::{self, ECDSAInvalidSignature, ECDSAInvalidSignatureS},
eip712::IEip712,
},
cryptography::eip712::IEip712,
nonces::{INonces, Nonces},
precompiles::{
primitives::ecrecover::{
self, ECDSAInvalidSignature, ECDSAInvalidSignatureS,
},
Precompiles,
},
},
};

Expand Down Expand Up @@ -104,11 +107,13 @@ impl From<erc20::Error> for Error {
}
}

impl From<ecdsa::Error> for Error {
fn from(value: ecdsa::Error) -> Self {
impl From<ecrecover::Error> for Error {
fn from(value: ecrecover::Error) -> Self {
match value {
ecdsa::Error::InvalidSignature(e) => Error::InvalidSignature(e),
ecdsa::Error::InvalidSignatureS(e) => Error::InvalidSignatureS(e),
ecrecover::Error::InvalidSignature(e) => Error::InvalidSignature(e),
ecrecover::Error::InvalidSignatureS(e) => {
Error::InvalidSignatureS(e)
}
}
}
}
Expand Down Expand Up @@ -184,9 +189,9 @@ pub trait IErc20Permit: INonces {
/// * [`ERC2612ExpiredSignature`] - If the `deadline` param is from the
/// past.
/// * [`ERC2612InvalidSigner`] - If signer is not an `owner`.
/// * [`ecdsa::Error::InvalidSignatureS`] - If the `s` value is grater than
/// [`ecdsa::SIGNATURE_S_UPPER_BOUND`].
/// * [`ecdsa::Error::InvalidSignature`] - If the recovered address is
/// * [`ecrecover::Error::InvalidSignatureS`] - If the `s` value is grater
/// than [`ecrecover::SIGNATURE_S_UPPER_BOUND`].
/// * [`ecrecover::Error::InvalidSignature`] - If the recovered address is
/// [`Address::ZERO`].
/// * [`erc20::Error::InvalidSpender`] - If the `spender` address is
/// [`Address::ZERO`].
Expand Down Expand Up @@ -243,7 +248,7 @@ impl<T: IEip712 + StorageType> Erc20Permit<T> {

let hash: B256 = self.eip712.hash_typed_data_v4(struct_hash);

let signer: Address = ecdsa::recover(self, hash, v, r, s)?;
let signer: Address = self.ecrecover(hash, v, r, s)?;

if signer != owner {
return Err(ERC2612InvalidSigner { signer, owner }.into());
Expand Down
4 changes: 1 addition & 3 deletions contracts/src/utils/cryptography/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ use stylus_sdk::{
prelude::*,
};

use crate::utils::cryptography::ecdsa;

/// Address of the `ecrecover` EVM precompile.
pub const ECRECOVER_ADDR: Address =
address!("0000000000000000000000000000000000000001");
Expand Down Expand Up @@ -66,7 +64,7 @@ pub enum Error {
InvalidSignatureS(ECDSAInvalidSignatureS),
}

impl MethodError for ecdsa::Error {
impl MethodError for Error {
fn encode(self) -> alloc::vec::Vec<u8> {
self.into()
}
Expand Down
1 change: 1 addition & 0 deletions contracts/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod math;
pub mod metadata;
pub mod nonces;
pub mod pausable;
pub mod precompiles;
pub mod structs;

pub use metadata::Metadata;
Expand Down
111 changes: 111 additions & 0 deletions contracts/src/utils/precompiles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! `ArbOS` precompiles wrapper enabling easier invocation.
use alloy_primitives::{Address, B256};
use primitives::ecrecover::Error;
use stylus_sdk::prelude::*;

use crate::utils::cryptography::ecdsa::recover;

/// Precompile primitives.
pub mod primitives {
/// The ecrecover precompile primitives.
///
/// This module provides the cryptographic primitives needed for the
/// `ecrecover` precompile, which recovers the signer address from an
/// ECDSA signature and message hash.
///
/// Re-exports selected ECDSA types and constants specifically relevant
/// to the ecrecover operation.
pub mod ecrecover {
pub use crate::utils::cryptography::ecdsa::{
ECDSAInvalidSignature, ECDSAInvalidSignatureS, EcRecoverData,
Error, ECRECOVER_ADDR, SIGNATURE_S_UPPER_BOUND,
};
}
}
Comment on lines +8 to +24
Copy link
Collaborator Author

@0xNeshi 0xNeshi Jun 5, 2025

Choose a reason for hiding this comment

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

Didn't remove utils::cryptography::ecdsa for two reasons:

  1. The original crate can be expanded to include additional ECDSA-related operations, which have nothing to do with ecrecover precompile; the crate would be useful to devs as a standalone.
  2. Backward compatibility.

The mod primitives module is used to make it clear to devs where to find relevant precompile-related types, constants, enums etc.
In this specific case, the reason for this reexport is that it should be easier for the average Stylus developer to understand that the relevant ecrecover primitives are in the same module, i.e. precompiles::primitives::ecrecover, than that they should actually import them from a completely different module utils::cryptography::ecdsa.


/// Trait providing access to Arbitrum precompiles for Stylus contracts.
///
/// This trait wraps complex precompile invocations to provide a clean,
/// ergonomic interface for calling Arbitrum's built-in cryptographic functions
/// and utilities from within Stylus smart contracts.
///
/// Precompiles are pre-deployed contracts at fixed addresses that implement
/// commonly used cryptographic operations and other utilities. They execute
/// natively in the Arbitrum runtime for better performance compared to
/// implementing these operations in contract code.
///
/// See: <https://docs.arbitrum.io/build-decentralized-apps/precompiles/overview>
///
/// # Usage
///
/// Implement this trait for your contract storage type to gain access to
/// precompile functionality:
///
/// ```rust,ignore
/// use openzeppelin_stylus::utils::cryptography::Precompiles;
///
/// #[storage]
/// #[entrypoint]
/// struct MyContract {
/// // your fields...
/// }
///
/// // The `Precompiles` trait is automatically implemented for all
/// // contracts annotated with `#[entrypoint]` or that implement
/// // `stylus_sdk::prelude::TopLevelStorage`.
///
/// #[public]
/// impl MyContract {
/// fn verify_signature(&mut self, msg_hash: B256, sig: (u8, B256, B256)) -> Result<Address, Error> {
/// let (v, r, s) = sig;
/// self.ecrecover(msg_hash, v, r, s)
/// }
/// }
/// ```
///
/// # Error Handling
///
/// Precompile methods return `Result` types to handle both invalid inputs and
/// precompile execution failures. Always handle these errors appropriately
/// in your contract logic.
pub trait Precompiles: TopLevelStorage {
/// Returns the address that signed a hashed message (`hash`).
///
/// # Arguments
///
/// * `&mut self` - Write access to the contract's state. given address.
/// * `hash` - Hash of the message.
/// * `v` - `v` value from the signature.
/// * `r` - `r` value from the signature.
/// * `s` - `s` value from the signature.
///
/// # Errors
///
/// * [`Error::InvalidSignatureS`] - If the `s` value is grater than
/// [`primitives::ecrecover::SIGNATURE_S_UPPER_BOUND`].
/// * [`Error::InvalidSignature`] - If the recovered address is
/// [`Address::ZERO`].
///
/// # Panics
///
/// * If the `ecrecover` precompile fails to execute.
fn ecrecover(
&mut self,
hash: B256,
v: u8,
r: B256,
s: B256,
) -> Result<Address, Error>;
}

impl<T: TopLevelStorage> Precompiles for T {
fn ecrecover(
&mut self,
hash: B256,
v: u8,
r: B256,
s: B256,
) -> Result<Address, Error> {
recover(self, hash, v, r, s)
}
}
6 changes: 4 additions & 2 deletions examples/ecdsa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ extern crate alloc;
use alloc::vec::Vec;

use alloy_primitives::{Address, B256};
use openzeppelin_stylus::utils::cryptography::ecdsa;
use openzeppelin_stylus::utils::{
cryptography::ecdsa, precompiles::Precompiles,
};
use stylus_sdk::prelude::*;

#[entrypoint]
Expand All @@ -20,6 +22,6 @@ impl ECDSAExample {
r: B256,
s: B256,
) -> Result<Address, ecdsa::Error> {
ecdsa::recover(self, hash, v, r, s)
self.ecrecover(hash, v, r, s)
}
}
Loading