Skip to content
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

feat: reimplement erc165 for erc20 #591

Merged
merged 7 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed (Breaking)

- Refactor `Erc20Permit` extension to be a composition of `Erc20` and `Nonces` contracts. #574
- Remove `IErc165` implementations for ERC-20 contracts to align with Solidity versions. #570
- Replace `VestingWallet::receive_ether` with dedicated `receive` function. #529
- Extract `IAccessControl` trait from `AccessControl` contract. #527
- Bump Stylus SDK to v0.8.1 #587
Expand Down
29 changes: 27 additions & 2 deletions contracts/src/token/erc20/extensions/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ use alloc::{string::String, vec, vec::Vec};

use openzeppelin_stylus_proc::interface_id;
use stylus_sdk::{
alloy_primitives::FixedBytes,
prelude::*,
stylus_proc::{public, storage},
};

use crate::utils::Metadata;
use crate::utils::{
introspection::erc165::{Erc165, IErc165},
Metadata,
};

/// Number of decimals used by default on implementors of [`Metadata`].
pub const DEFAULT_DECIMALS: u8 = 18;
Expand Down Expand Up @@ -73,14 +77,35 @@ impl IErc20Metadata for Erc20Metadata {
}
}

impl IErc165 for Erc20Metadata {
fn supports_interface(interface_id: FixedBytes<4>) -> bool {
<Self as IErc20Metadata>::INTERFACE_ID
== u32::from_be_bytes(*interface_id)
|| Erc165::supports_interface(interface_id)
}
}

#[cfg(all(test, feature = "std"))]
mod tests {
use super::{Erc20Metadata, IErc20Metadata};
use super::{Erc20Metadata, IErc165, IErc20Metadata};

#[motsu::test]
fn interface_id() {
let actual = <Erc20Metadata as IErc20Metadata>::INTERFACE_ID;
let expected = 0xa219a025;
assert_eq!(actual, expected);
}

#[motsu::test]
fn supports_interface() {
assert!(Erc20Metadata::supports_interface(
<Erc20Metadata as IErc20Metadata>::INTERFACE_ID.into()
));
assert!(Erc20Metadata::supports_interface(
<Erc20Metadata as IErc165>::INTERFACE_ID.into()
));

let fake_interface_id = 0x12345678u32;
assert!(!Erc20Metadata::supports_interface(fake_interface_id.into()));
}
}
33 changes: 29 additions & 4 deletions contracts/src/token/erc20/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! [`Erc20`] applications.
use alloc::{vec, vec::Vec};

use alloy_primitives::{Address, U256};
use alloy_primitives::{Address, FixedBytes, U256};
use openzeppelin_stylus_proc::interface_id;
use stylus_sdk::{
call::MethodError,
Expand All @@ -16,8 +16,9 @@ use stylus_sdk::{
stylus_proc::{public, SolidityError},
};

use crate::utils::math::storage::{
AddAssignChecked, AddAssignUnchecked, SubAssignUnchecked,
use crate::utils::{
introspection::erc165::{Erc165, IErc165},
math::storage::{AddAssignChecked, AddAssignUnchecked, SubAssignUnchecked},
};

pub mod extensions;
Expand Down Expand Up @@ -572,13 +573,20 @@ impl Erc20 {
}
}

impl IErc165 for Erc20 {
fn supports_interface(interface_id: FixedBytes<4>) -> bool {
<Self as IErc20>::INTERFACE_ID == u32::from_be_bytes(*interface_id)
|| Erc165::supports_interface(interface_id)
}
}

#[cfg(all(test, feature = "std"))]
mod tests {
use alloy_primitives::{uint, Address, U256};
use motsu::prelude::Contract;
use stylus_sdk::prelude::TopLevelStorage;

use super::{Erc20, Error, IErc20};
use super::{Erc20, Error, IErc165, IErc20};

unsafe impl TopLevelStorage for Erc20 {}

Expand Down Expand Up @@ -936,5 +944,22 @@ mod tests {
let actual = <Erc20 as IErc20>::INTERFACE_ID;
let expected = 0x36372b07;
assert_eq!(actual, expected);

let actual = <Erc20 as IErc165>::INTERFACE_ID;
let expected = 0x01ffc9a7;
assert_eq!(actual, expected);
}

#[motsu::test]
fn supports_interface() {
assert!(Erc20::supports_interface(
<Erc20 as IErc20>::INTERFACE_ID.into()
));
assert!(Erc20::supports_interface(
<Erc20 as IErc165>::INTERFACE_ID.into()
));

let fake_interface_id = 0x12345678u32;
assert!(!Erc20::supports_interface(fake_interface_id.into()));
}
}
1 change: 1 addition & 0 deletions examples/erc20-permit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct Erc20PermitExample {
#[borrow]
pub erc20_permit: Erc20Permit<Eip712>,
}

#[storage]
struct Eip712 {}

Expand Down
9 changes: 7 additions & 2 deletions examples/erc20/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ extern crate alloc;

use alloc::vec::Vec;

use alloy_primitives::{Address, U256};
use alloy_primitives::{Address, FixedBytes, U256};
use openzeppelin_stylus::{
token::erc20::{
extensions::{capped, Capped, Erc20Metadata, IErc20Burnable},
Erc20, IErc20,
},
utils::Pausable,
utils::{introspection::erc165::IErc165, Pausable},
};
use stylus_sdk::prelude::*;

Expand Down Expand Up @@ -105,6 +105,11 @@ impl Erc20Example {
self.erc20.transfer_from(from, to, value).map_err(|e| e.into())
}

fn supports_interface(interface_id: FixedBytes<4>) -> bool {
Erc20::supports_interface(interface_id)
|| Erc20Metadata::supports_interface(interface_id)
}

/// WARNING: These functions are intended for **testing purposes** only. In
/// **production**, ensure strict access control to prevent unauthorized
/// pausing or unpausing, which can disrupt contract functionality. Remove
Expand Down
2 changes: 2 additions & 0 deletions examples/erc20/tests/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ sol!(
function pause() external;
function unpause() external;

function supportsInterface(bytes4 interface_id) external view returns (bool supportsInterface);

error EnforcedPause();
error ExpectedPause();

Expand Down
53 changes: 53 additions & 0 deletions examples/erc20/tests/erc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1362,3 +1362,56 @@ async fn error_when_transfer_from(alice: Account, bob: Account) -> Result<()> {

Ok(())
}

// ============================================================================
// Integration Tests: ERC-165 Support Interface
// ============================================================================

#[e2e::test]
async fn supports_interface(alice: Account) -> Result<()> {
let contract_addr = alice
.as_deployer()
.with_default_constructor::<constructorCall>()
.deploy()
.await?
.address()?;
let contract = Erc20::new(contract_addr, &alice.wallet);

let invalid_interface_id: u32 = 0xffffffff;
let supports_interface = contract
.supportsInterface(invalid_interface_id.into())
.call()
.await?
.supportsInterface;

assert!(!supports_interface);

let erc20_interface_id: u32 = 0x36372b07;
let supports_interface = contract
.supportsInterface(erc20_interface_id.into())
.call()
.await?
.supportsInterface;

assert!(supports_interface);

let erc165_interface_id: u32 = 0x01ffc9a7;
let supports_interface = contract
.supportsInterface(erc165_interface_id.into())
.call()
.await?
.supportsInterface;

assert!(supports_interface);

let erc20_metadata_interface_id: u32 = 0xa219a025;
let supports_interface = contract
.supportsInterface(erc20_metadata_interface_id.into())
.call()
.await?
.supportsInterface;

assert!(supports_interface);

Ok(())
}
Loading