Skip to content

Commit 97fd57b

Browse files
authoredMar 6, 2025··
feat: reimplement erc165 for erc20 (#591)
<!-- Thank you for your interest in contributing to OpenZeppelin! Consider opening an issue for discussion prior to submitting a PR. New features will be merged faster if they were first discussed and designed with the team. Describe the changes introduced in this pull request. Include any context necessary for understanding the PR's purpose. --> Reverts change introduced in #570 <!-- Fill in with issue number --> Towards #592 #### PR Checklist <!-- Before merging the pull request all of the following must be completed. Feel free to submit a PR or Draft PR even if some items are pending. Some of the items may not apply. --> - [x] Tests - [x] Documentation - [x] Changelog
1 parent 2afef78 commit 97fd57b

File tree

9 files changed

+138
-26
lines changed

9 files changed

+138
-26
lines changed
 

‎CHANGELOG.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323

2424
-
2525

26-
## [v0.2.0-alpha.4] - 2025-03-05
26+
## [v0.2.0-alpha.4] - 2025-03-06
2727

2828
### Added
2929

@@ -34,7 +34,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
### Changed (Breaking)
3535

3636
- Refactor `Erc20Permit` extension to be a composition of `Erc20` and `Nonces` contracts. #574
37-
- Remove `IErc165` implementations for ERC-20 contracts to align with Solidity versions. #570
3837
- Replace `VestingWallet::receive_ether` with dedicated `receive` function. #529
3938
- Extract `IAccessControl` trait from `AccessControl` contract. #527
4039
- Bump Stylus SDK to v0.8.1 #587

‎contracts/src/token/erc20/extensions/metadata.rs

+27-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ use alloc::{string::String, vec, vec::Vec};
44

55
use openzeppelin_stylus_proc::interface_id;
66
use stylus_sdk::{
7+
alloy_primitives::FixedBytes,
78
prelude::*,
89
stylus_proc::{public, storage},
910
};
1011

11-
use crate::utils::Metadata;
12+
use crate::utils::{
13+
introspection::erc165::{Erc165, IErc165},
14+
Metadata,
15+
};
1216

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

80+
impl IErc165 for Erc20Metadata {
81+
fn supports_interface(interface_id: FixedBytes<4>) -> bool {
82+
<Self as IErc20Metadata>::INTERFACE_ID
83+
== u32::from_be_bytes(*interface_id)
84+
|| Erc165::supports_interface(interface_id)
85+
}
86+
}
87+
7688
#[cfg(all(test, feature = "std"))]
7789
mod tests {
78-
use super::{Erc20Metadata, IErc20Metadata};
90+
use super::{Erc20Metadata, IErc165, IErc20Metadata};
7991

8092
#[motsu::test]
8193
fn interface_id() {
8294
let actual = <Erc20Metadata as IErc20Metadata>::INTERFACE_ID;
8395
let expected = 0xa219a025;
8496
assert_eq!(actual, expected);
8597
}
98+
99+
#[motsu::test]
100+
fn supports_interface() {
101+
assert!(Erc20Metadata::supports_interface(
102+
<Erc20Metadata as IErc20Metadata>::INTERFACE_ID.into()
103+
));
104+
assert!(Erc20Metadata::supports_interface(
105+
<Erc20Metadata as IErc165>::INTERFACE_ID.into()
106+
));
107+
108+
let fake_interface_id = 0x12345678u32;
109+
assert!(!Erc20Metadata::supports_interface(fake_interface_id.into()));
110+
}
86111
}

‎contracts/src/token/erc20/mod.rs

+29-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! [`Erc20`] applications.
77
use alloc::{vec, vec::Vec};
88

9-
use alloy_primitives::{Address, U256};
9+
use alloy_primitives::{Address, FixedBytes, U256};
1010
use openzeppelin_stylus_proc::interface_id;
1111
use stylus_sdk::{
1212
call::MethodError,
@@ -16,8 +16,9 @@ use stylus_sdk::{
1616
stylus_proc::{public, SolidityError},
1717
};
1818

19-
use crate::utils::math::storage::{
20-
AddAssignChecked, AddAssignUnchecked, SubAssignUnchecked,
19+
use crate::utils::{
20+
introspection::erc165::{Erc165, IErc165},
21+
math::storage::{AddAssignChecked, AddAssignUnchecked, SubAssignUnchecked},
2122
};
2223

2324
pub mod extensions;
@@ -572,13 +573,20 @@ impl Erc20 {
572573
}
573574
}
574575

576+
impl IErc165 for Erc20 {
577+
fn supports_interface(interface_id: FixedBytes<4>) -> bool {
578+
<Self as IErc20>::INTERFACE_ID == u32::from_be_bytes(*interface_id)
579+
|| Erc165::supports_interface(interface_id)
580+
}
581+
}
582+
575583
#[cfg(all(test, feature = "std"))]
576584
mod tests {
577585
use alloy_primitives::{uint, Address, U256};
578586
use motsu::prelude::Contract;
579587
use stylus_sdk::prelude::TopLevelStorage;
580588

581-
use super::{Erc20, Error, IErc20};
589+
use super::{Erc20, Error, IErc165, IErc20};
582590

583591
unsafe impl TopLevelStorage for Erc20 {}
584592

@@ -936,5 +944,22 @@ mod tests {
936944
let actual = <Erc20 as IErc20>::INTERFACE_ID;
937945
let expected = 0x36372b07;
938946
assert_eq!(actual, expected);
947+
948+
let actual = <Erc20 as IErc165>::INTERFACE_ID;
949+
let expected = 0x01ffc9a7;
950+
assert_eq!(actual, expected);
951+
}
952+
953+
#[motsu::test]
954+
fn supports_interface() {
955+
assert!(Erc20::supports_interface(
956+
<Erc20 as IErc20>::INTERFACE_ID.into()
957+
));
958+
assert!(Erc20::supports_interface(
959+
<Erc20 as IErc165>::INTERFACE_ID.into()
960+
));
961+
962+
let fake_interface_id = 0x12345678u32;
963+
assert!(!Erc20::supports_interface(fake_interface_id.into()));
939964
}
940965
}

‎examples/ecdsa/tests/ecdsa.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![cfg(feature = "e2e")]
22

33
use abi::ECDSA;
4-
use alloy::primitives::{address, b256, uint, Address, Parity, B256};
4+
use alloy::primitives::{address, b256, uint, Address, B256};
55
use e2e::{Account, ReceiptExt, Revert};
66
use eyre::Result;
77
use openzeppelin_stylus::utils::cryptography::ecdsa::SIGNATURE_S_UPPER_BOUND;
@@ -108,10 +108,10 @@ async fn recovers_from_v_r_s(alice: Account) -> Result<()> {
108108
let contract = ECDSA::new(contract_addr, &alice.wallet);
109109

110110
let signature = alice.sign_hash(&HASH).await;
111-
let parity: Parity = signature.v().into();
112-
let v_byte = parity
113-
.y_parity_byte_non_eip155()
114-
.expect("should be non-EIP155 signature");
111+
112+
// converted to non-eip155 `v` value
113+
// see https://eips.ethereum.org/EIPS/eip-155
114+
let v_byte = signature.v() as u8 + 27;
115115

116116
let ECDSA::recoverReturn { recovered } = contract
117117
.recover(HASH, v_byte, signature.r().into(), signature.s().into())

‎examples/erc20-permit/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct Erc20PermitExample {
2020
#[borrow]
2121
pub erc20_permit: Erc20Permit<Eip712>,
2222
}
23+
2324
#[storage]
2425
struct Eip712 {}
2526

‎examples/erc20-permit/tests/erc20permit.rs

+13-11
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
#![cfg(feature = "e2e")]
1+
// #![cfg(feature = "e2e")]
22

33
use abi::Erc20Permit;
44
use alloy::{
5-
primitives::{keccak256, Address, Parity, B256, U256},
6-
signers::Signature,
5+
primitives::{keccak256, Address, B256, U256},
76
sol,
87
sol_types::SolType,
98
};
@@ -65,9 +64,12 @@ fn permit_struct_hash(
6564
)))
6665
}
6766

68-
fn extract_signature_v(signature: &Signature) -> u8 {
69-
let parity: Parity = signature.v().into();
70-
parity.y_parity_byte_non_eip155().expect("should be non-EIP155 signature")
67+
// I was unable to find a function in alloy that converts `v` into [non-eip155
68+
// value], so I implemented the logic manually.
69+
//
70+
// [non-eip155 value]: https://eips.ethereum.org/EIPS/eip-155
71+
fn to_non_eip155_v(v: bool) -> u8 {
72+
v as u8 + 27
7173
}
7274

7375
// ============================================================================
@@ -108,7 +110,7 @@ async fn error_when_expired_deadline_for_permit(
108110
bob_addr,
109111
balance,
110112
EXPIRED_DEADLINE,
111-
extract_signature_v(&signature),
113+
to_non_eip155_v(signature.v()),
112114
signature.r().into(),
113115
signature.s().into()
114116
))
@@ -157,7 +159,7 @@ async fn permit_works(alice: Account, bob: Account) -> Result<()> {
157159
bob_addr,
158160
balance,
159161
FAIR_DEADLINE,
160-
extract_signature_v(&signature),
162+
to_non_eip155_v(signature.v()),
161163
signature.r().into(),
162164
signature.s().into()
163165
))?;
@@ -242,7 +244,7 @@ async fn permit_rejects_reused_signature(
242244
bob_addr,
243245
balance,
244246
FAIR_DEADLINE,
245-
extract_signature_v(&signature),
247+
to_non_eip155_v(signature.v()),
246248
signature.r().into(),
247249
signature.s().into()
248250
))?;
@@ -252,7 +254,7 @@ async fn permit_rejects_reused_signature(
252254
bob_addr,
253255
balance,
254256
FAIR_DEADLINE,
255-
extract_signature_v(&signature),
257+
to_non_eip155_v(signature.v()),
256258
signature.r().into(),
257259
signature.s().into()
258260
))
@@ -317,7 +319,7 @@ async fn permit_rejects_invalid_signature(
317319
bob_addr,
318320
balance,
319321
FAIR_DEADLINE,
320-
extract_signature_v(&signature),
322+
to_non_eip155_v(signature.v()),
321323
signature.r().into(),
322324
signature.s().into()
323325
))

‎examples/erc20/src/lib.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ extern crate alloc;
33

44
use alloc::vec::Vec;
55

6-
use alloy_primitives::{Address, U256};
6+
use alloy_primitives::{Address, FixedBytes, U256};
77
use openzeppelin_stylus::{
88
token::erc20::{
99
extensions::{capped, Capped, Erc20Metadata, IErc20Burnable},
1010
Erc20, IErc20,
1111
},
12-
utils::Pausable,
12+
utils::{introspection::erc165::IErc165, Pausable},
1313
};
1414
use stylus_sdk::prelude::*;
1515

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

108+
fn supports_interface(interface_id: FixedBytes<4>) -> bool {
109+
Erc20::supports_interface(interface_id)
110+
|| Erc20Metadata::supports_interface(interface_id)
111+
}
112+
108113
/// WARNING: These functions are intended for **testing purposes** only. In
109114
/// **production**, ensure strict access control to prevent unauthorized
110115
/// pausing or unpausing, which can disrupt contract functionality. Remove

‎examples/erc20/tests/abi/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ sol!(
2525
function pause() external;
2626
function unpause() external;
2727

28+
function supportsInterface(bytes4 interface_id) external view returns (bool supportsInterface);
29+
2830
error EnforcedPause();
2931
error ExpectedPause();
3032

‎examples/erc20/tests/erc20.rs

+53
Original file line numberDiff line numberDiff line change
@@ -1362,3 +1362,56 @@ async fn error_when_transfer_from(alice: Account, bob: Account) -> Result<()> {
13621362

13631363
Ok(())
13641364
}
1365+
1366+
// ============================================================================
1367+
// Integration Tests: ERC-165 Support Interface
1368+
// ============================================================================
1369+
1370+
#[e2e::test]
1371+
async fn supports_interface(alice: Account) -> Result<()> {
1372+
let contract_addr = alice
1373+
.as_deployer()
1374+
.with_default_constructor::<constructorCall>()
1375+
.deploy()
1376+
.await?
1377+
.address()?;
1378+
let contract = Erc20::new(contract_addr, &alice.wallet);
1379+
1380+
let invalid_interface_id: u32 = 0xffffffff;
1381+
let supports_interface = contract
1382+
.supportsInterface(invalid_interface_id.into())
1383+
.call()
1384+
.await?
1385+
.supportsInterface;
1386+
1387+
assert!(!supports_interface);
1388+
1389+
let erc20_interface_id: u32 = 0x36372b07;
1390+
let supports_interface = contract
1391+
.supportsInterface(erc20_interface_id.into())
1392+
.call()
1393+
.await?
1394+
.supportsInterface;
1395+
1396+
assert!(supports_interface);
1397+
1398+
let erc165_interface_id: u32 = 0x01ffc9a7;
1399+
let supports_interface = contract
1400+
.supportsInterface(erc165_interface_id.into())
1401+
.call()
1402+
.await?
1403+
.supportsInterface;
1404+
1405+
assert!(supports_interface);
1406+
1407+
let erc20_metadata_interface_id: u32 = 0xa219a025;
1408+
let supports_interface = contract
1409+
.supportsInterface(erc20_metadata_interface_id.into())
1410+
.call()
1411+
.await?
1412+
.supportsInterface;
1413+
1414+
assert!(supports_interface);
1415+
1416+
Ok(())
1417+
}

0 commit comments

Comments
 (0)
Please sign in to comment.