From 64f9f63bd8189567dd8b15c72ef4887329c35860 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 10 Jan 2025 13:52:08 +0100 Subject: [PATCH 1/5] feat: update logic and tests --- docs/modules/ROOT/pages/components.adoc | 4 +-- .../governance/src/governor/governor.cairo | 2 +- packages/presets/src/erc20.cairo | 2 +- packages/test_common/src/mocks/erc20.cairo | 8 ++--- packages/test_common/src/mocks/vesting.cairo | 2 +- packages/test_common/src/mocks/votes.cairo | 2 +- .../token/src/common/erc2981/erc2981.cairo | 5 ++- packages/token/src/erc20.cairo | 2 +- packages/token/src/erc20/erc20.cairo | 34 +++++++++++++++++-- .../token/src/tests/erc20/test_erc20.cairo | 9 +++-- .../src/tests/erc20/test_erc20_permit.cairo | 2 +- 11 files changed, 52 insertions(+), 20 deletions(-) diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index e2e4c8748..7d6ad92e1 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -286,7 +286,7 @@ mod MyContract { === Immutable Config :erc2981-component: xref:/api/token_common.adoc#ERC2981Component[ERC2981Component] -:SRC-107: https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md[SRC-107] +:SRC-107: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-107.md[SRC-107] While initializers help set up the component's initial state, some require configuration that may be defined as constants, saving gas by avoiding the necessity of reading from storage each time the variable needs to be used. The @@ -397,7 +397,7 @@ mod MyContract { ==== `validate` function -:validate-section: https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#validate-function[validate section of the SRC-107] +:validate-section: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-107.md#validate-function[validate section of the SRC-107] The `ImmutableConfig` trait may also include a `validate` function with a default implementation, which asserts that the configuration is correct, and must not be overridden by the implementing contract. For more information diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 8d69b2054..56a0c04be 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -1041,7 +1041,7 @@ pub mod GovernorComponent { /// Implementation of the default Governor ImmutableConfig. /// /// See -/// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation +/// https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-107.md#defaultconfig-implementation /// /// The `DEFAULT_PARAMS` is set to an empty span of felts. pub impl DefaultConfig of GovernorComponent::ImmutableConfig { diff --git a/packages/presets/src/erc20.cairo b/packages/presets/src/erc20.cairo index a4b4ecf6b..1127b6a10 100644 --- a/packages/presets/src/erc20.cairo +++ b/packages/presets/src/erc20.cairo @@ -12,7 +12,7 @@ #[starknet::contract] pub mod ERC20Upgradeable { use openzeppelin_access::ownable::OwnableComponent; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use openzeppelin_upgrades::UpgradeableComponent; use openzeppelin_upgrades::interface::IUpgradeable; use starknet::{ClassHash, ContractAddress}; diff --git a/packages/test_common/src/mocks/erc20.cairo b/packages/test_common/src/mocks/erc20.cairo index de3c88e37..b47ad7e6f 100644 --- a/packages/test_common/src/mocks/erc20.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -1,6 +1,6 @@ #[starknet::contract] pub mod DualCaseERC20Mock { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -41,7 +41,7 @@ pub mod DualCaseERC20Mock { #[starknet::contract] pub mod SnakeERC20Mock { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -82,7 +82,7 @@ pub mod SnakeERC20Mock { /// This is used to test that the hooks are called with the correct arguments. #[starknet::contract] pub mod SnakeERC20MockWithHooks { - use openzeppelin_token::erc20::ERC20Component; + use openzeppelin_token::erc20::{ERC20Component, DefaultConfig}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -161,7 +161,7 @@ pub mod SnakeERC20MockWithHooks { #[starknet::contract] pub mod DualCaseERC20PermitMock { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use starknet::ContractAddress; diff --git a/packages/test_common/src/mocks/vesting.cairo b/packages/test_common/src/mocks/vesting.cairo index 50100ad60..295c1869a 100644 --- a/packages/test_common/src/mocks/vesting.cairo +++ b/packages/test_common/src/mocks/vesting.cairo @@ -129,7 +129,7 @@ pub mod StepsVestingMock { #[starknet::contract] pub mod ERC20OptionalTransferPanicMock { use openzeppelin_token::erc20::interface::IERC20; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; diff --git a/packages/test_common/src/mocks/votes.cairo b/packages/test_common/src/mocks/votes.cairo index 9faf3b40d..7553c1e52 100644 --- a/packages/test_common/src/mocks/votes.cairo +++ b/packages/test_common/src/mocks/votes.cairo @@ -1,7 +1,7 @@ #[starknet::contract] pub mod ERC20VotesMock { use openzeppelin_governance::votes::VotesComponent; - use openzeppelin_token::erc20::ERC20Component; + use openzeppelin_token::erc20::{ERC20Component, DefaultConfig}; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use starknet::ContractAddress; diff --git a/packages/token/src/common/erc2981/erc2981.cairo b/packages/token/src/common/erc2981/erc2981.cairo index c212276b2..a4ffc2908 100644 --- a/packages/token/src/common/erc2981/erc2981.cairo +++ b/packages/token/src/common/erc2981/erc2981.cairo @@ -12,7 +12,7 @@ /// /// Royalty is specified as a fraction of sale price. The denominator is set by the contract by /// using the Immutable Component Config pattern. See -/// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md +/// https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-107.md /// /// IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its /// payment. See https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the @@ -418,14 +418,13 @@ pub mod ERC2981Component { /// Implementation of the default ERC2981Component ImmutableConfig. /// /// See -/// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation +/// https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-107.md#defaultconfig-implementation /// /// The default fee denominator is set to `DEFAULT_FEE_DENOMINATOR`. pub impl DefaultConfig of ERC2981Component::ImmutableConfig { const FEE_DENOMINATOR: u128 = ERC2981Component::DEFAULT_FEE_DENOMINATOR; } - #[cfg(test)] mod tests { use openzeppelin_test_common::mocks::erc2981::ERC2981Mock; diff --git a/packages/token/src/erc20.cairo b/packages/token/src/erc20.cairo index e3a8368f7..331d25433 100644 --- a/packages/token/src/erc20.cairo +++ b/packages/token/src/erc20.cairo @@ -2,5 +2,5 @@ pub mod erc20; pub mod interface; pub mod snip12_utils; -pub use erc20::{ERC20Component, ERC20HooksEmptyImpl}; +pub use erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; pub use interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; diff --git a/packages/token/src/erc20/erc20.cairo b/packages/token/src/erc20/erc20.cairo index 58608bbc3..50bf1cfe5 100644 --- a/packages/token/src/erc20/erc20.cairo +++ b/packages/token/src/erc20/erc20.cairo @@ -26,6 +26,10 @@ pub mod ERC20Component { use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + // This default decimals is only used when the DefaultConfig + // is in scope in the implementing contract. + pub const DEFAULT_DECIMALS: u8 = 18; + #[storage] pub struct Storage { pub ERC20_name: ByteArray, @@ -76,6 +80,14 @@ pub mod ERC20Component { pub const INVALID_PERMIT_SIGNATURE: felt252 = 'ERC20: invalid permit signature'; } + /// Constants expected to be defined at the contract level used to configure the component + /// behaviour. + /// + /// - `DECIMALS`: Returns the number of decimals the token uses. + pub trait ImmutableConfig { + const DECIMALS: u8; + } + // // Hooks // @@ -181,7 +193,10 @@ pub mod ERC20Component { #[embeddable_as(ERC20MetadataImpl)] impl ERC20Metadata< - TContractState, +HasComponent, +ERC20HooksTrait, + TContractState, + +HasComponent, + impl Immutable: ImmutableConfig, + +ERC20HooksTrait, > of interface::IERC20Metadata> { /// Returns the name of the token. fn name(self: @ComponentState) -> ByteArray { @@ -195,7 +210,7 @@ pub mod ERC20Component { /// Returns the number of decimals used to get its user representation. fn decimals(self: @ComponentState) -> u8 { - 18 + Immutable::DECIMALS } } @@ -224,7 +239,10 @@ pub mod ERC20Component { #[embeddable_as(ERC20MixinImpl)] impl ERC20Mixin< - TContractState, +HasComponent, +ERC20HooksTrait, + TContractState, + +HasComponent, + +ImmutableConfig, + +ERC20HooksTrait, > of interface::IERC20Mixin> { // IERC20 fn total_supply(self: @ComponentState) -> u256 { @@ -548,5 +566,15 @@ pub mod ERC20Component { } } +/// Implementation of the default ERC20Component ImmutableConfig. +/// +/// See +/// https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-107.md#defaultconfig-implementation +/// +/// The default decimals is set to `DEFAULT_DECIMALS`. +pub impl DefaultConfig of ERC20Component::ImmutableConfig { + const DECIMALS: u8 = ERC20Component::DEFAULT_DECIMALS; +} + /// An empty implementation of the ERC20 hooks to be used in basic ERC20 preset contracts. pub impl ERC20HooksEmptyImpl of ERC20Component::ERC20HooksTrait {} diff --git a/packages/token/src/tests/erc20/test_erc20.cairo b/packages/token/src/tests/erc20/test_erc20.cairo index 314e1b2d0..3a9e9e295 100644 --- a/packages/token/src/tests/erc20/test_erc20.cairo +++ b/packages/token/src/tests/erc20/test_erc20.cairo @@ -5,12 +5,17 @@ use crate::erc20::ERC20Component::{ERC20MetadataImpl, InternalImpl}; use openzeppelin_test_common::erc20::ERC20SpyHelpers; use openzeppelin_test_common::mocks::erc20::{DualCaseERC20Mock, SnakeERC20MockWithHooks}; use openzeppelin_testing::constants::{ - DECIMALS, NAME, OWNER, RECIPIENT, SPENDER, SUPPLY, SYMBOL, VALUE, ZERO, + NAME, OWNER, RECIPIENT, SPENDER, SUPPLY, SYMBOL, VALUE, ZERO, }; use openzeppelin_testing::events::EventSpyExt; use snforge_std::{EventSpy, spy_events, start_cheat_caller_address, test_address}; use starknet::ContractAddress; +// Custom implementation of the ERC20Component ImmutableConfig used for testing. +impl ERC20ImmutableConfig of ERC20Component::ImmutableConfig { + const DECIMALS: u8 = 6; +} + // // Setup // @@ -52,7 +57,7 @@ fn test_initializer() { assert_eq!(state.name(), NAME()); assert_eq!(state.symbol(), SYMBOL()); - assert_eq!(state.decimals(), DECIMALS); + assert_eq!(state.decimals(), ERC20ImmutableConfig::DECIMALS); assert_eq!(state.total_supply(), 0); } diff --git a/packages/token/src/tests/erc20/test_erc20_permit.cairo b/packages/token/src/tests/erc20/test_erc20_permit.cairo index 42ea0d7c4..989daf624 100644 --- a/packages/token/src/tests/erc20/test_erc20_permit.cairo +++ b/packages/token/src/tests/erc20/test_erc20_permit.cairo @@ -1,6 +1,6 @@ use core::hash::{HashStateExTrait, HashStateTrait}; use core::poseidon::PoseidonTrait; -use crate::erc20::ERC20Component; +use crate::erc20::{ERC20Component, DefaultConfig}; use crate::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; use crate::erc20::ERC20Component::{ERC20PermitImpl, SNIP12MetadataExternalImpl}; use crate::erc20::snip12_utils::permit::{PERMIT_TYPE_HASH, Permit}; From 1ba88f6766ced344453829dcfa2d22e0ce0fd03f Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 10 Jan 2025 14:04:33 +0100 Subject: [PATCH 2/5] feat: update doc examples --- README.md | 2 +- docs/modules/ROOT/pages/access.adoc | 6 +++--- docs/modules/ROOT/pages/components.adoc | 8 +++----- docs/modules/ROOT/pages/erc20.adoc | 4 ++-- docs/modules/ROOT/pages/governance/votes.adoc | 2 +- docs/modules/ROOT/pages/index.adoc | 14 ++++++++++++-- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4bbab1e00..c41b76482 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ For example, this is how to write an ERC20-compliant contract: ```cairo #[starknet::contract] mod MyToken { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index 752c7ffc9..aa6d07efa 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -176,7 +176,7 @@ const MINTER_ROLE: felt252 = selector!("MINTER_ROLE"); mod MyContract { use openzeppelin_access::accesscontrol::AccessControlComponent; use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; use super::MINTER_ROLE; @@ -267,7 +267,7 @@ const BURNER_ROLE: felt252 = selector!("BURNER_ROLE"); mod MyContract { use openzeppelin_access::accesscontrol::AccessControlComponent; use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; use super::{MINTER_ROLE, BURNER_ROLE}; @@ -390,7 +390,7 @@ mod MyContract { use openzeppelin_access::accesscontrol::AccessControlComponent; use openzeppelin_access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; use super::{MINTER_ROLE, BURNER_ROLE}; diff --git a/docs/modules/ROOT/pages/components.adoc b/docs/modules/ROOT/pages/components.adoc index 7d6ad92e1..e90dd2c8e 100644 --- a/docs/modules/ROOT/pages/components.adoc +++ b/docs/modules/ROOT/pages/components.adoc @@ -483,7 +483,7 @@ The following snippet leverages the `before_update` hook to include this behavio mod MyToken { use openzeppelin_security::pausable::PausableComponent::InternalTrait; use openzeppelin_security::pausable::PausableComponent; - use openzeppelin_token::erc20::ERC20Component; + use openzeppelin_token::erc20::{ERC20Component, DefaultConfig}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -535,7 +535,7 @@ The using contract just needs to bring the implementation into scope like this: ---- #[starknet::contract] mod MyToken { - use openzeppelin_token::erc20::ERC20Component; + use openzeppelin_token::erc20::{ERC20Component, DefaultConfig}; use openzeppelin_token::erc20::ERC20HooksEmptyImpl; (...) @@ -559,7 +559,7 @@ Here's the setup: #[starknet::contract] mod ERC20Pausable { use openzeppelin_security::pausable::PausableComponent; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; // Import the ERC20 interfaces to create custom implementations use openzeppelin_token::erc20::interface::{IERC20, IERC20CamelOnly}; use starknet::ContractAddress; @@ -651,8 +651,6 @@ This is why the contract defined the `ERC20Impl` from the component in the previ Creating a custom implementation of an interface must define *all* methods from that interface. This is true even if the behavior of a method does not change from the component implementation (as `total_supply` exemplifies in this example). -TIP: The ERC20 documentation provides another custom implementation guide for {custom-decimals}. - === Accessing component storage There may be cases where the contract must read or write to an integrated component's storage. diff --git a/docs/modules/ROOT/pages/erc20.adoc b/docs/modules/ROOT/pages/erc20.adoc index b73124f12..3a4ac4e2c 100644 --- a/docs/modules/ROOT/pages/erc20.adoc +++ b/docs/modules/ROOT/pages/erc20.adoc @@ -23,7 +23,7 @@ Here's what that looks like: ---- #[starknet::contract] mod MyToken { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -164,7 +164,7 @@ impl ERC20MetadataImpl of interface::IERC20Metadata { For more complex scenarios, such as a factory deploying multiple tokens with differing values for decimals, a flexible solution might be appropriate. -TIP: Note that we are not using the MixinImpl in this case, since we need to customize the IERC20Metadata implementation. +TIP: Note that we are not using the MixinImpl or the DefaultConfig in this case, since we need to customize the IERC20Metadata implementation. [,cairo] ---- diff --git a/docs/modules/ROOT/pages/governance/votes.adoc b/docs/modules/ROOT/pages/governance/votes.adoc index 67df15525..9445673f7 100644 --- a/docs/modules/ROOT/pages/governance/votes.adoc +++ b/docs/modules/ROOT/pages/governance/votes.adoc @@ -33,7 +33,7 @@ Here's an example of how to structure a simple ERC20Votes contract: #[starknet::contract] mod ERC20VotesContract { use openzeppelin_governance::votes::VotesComponent; - use openzeppelin_token::erc20::ERC20Component; + use openzeppelin_token::erc20::{ERC20Component, DefaultConfig}; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use starknet::ContractAddress; diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 9db691184..8e2255f32 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -61,18 +61,28 @@ Install the library by declaring it as a dependency in the project's `Scarb.toml openzeppelin = "0.20.0" ---- -WARNING: Make sure the tag matches the target release. +The previous example would import the entire library. We can also add each package as a separate dependency to +improve the building time by not including modules that won't be used: + +[,text] +---- +[dependencies] +openzeppelin_access = "0.20.0" +openzeppelin_token = "0.20.0" +---- == Basic usage This is how it looks to build an ERC20 contract using the xref:erc20.adoc[ERC20 component]. Copy the code into `src/lib.cairo`. +TIP: If you added the entire library as a dependency, use `openzeppelin::token` instead of `openzeppelin_token` for the imports. + [,cairo] ---- #[starknet::contract] mod MyERC20Token { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); From f78cc8b8a9cd1d50505959102a395e7d49d3e9e7 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 10 Jan 2025 14:16:01 +0100 Subject: [PATCH 3/5] feat: update customizing decimals guide --- docs/modules/ROOT/pages/erc20.adoc | 56 +++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/docs/modules/ROOT/pages/erc20.adoc b/docs/modules/ROOT/pages/erc20.adoc index 3a4ac4e2c..5cf2deae4 100644 --- a/docs/modules/ROOT/pages/erc20.adoc +++ b/docs/modules/ROOT/pages/erc20.adoc @@ -125,6 +125,8 @@ Some notable differences, however, can still be found, such as: == Customizing decimals +:SRC-107: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-107.md[SRC-107] + :floating-point: https://en.wikipedia.org//wiki/Floating-point_arithmetic[floating-point numbers] :eip-discussion: https://github.com/ethereum/EIPs/issues/724[EIP discussion] @@ -136,28 +138,56 @@ In the actual contract, however, the supply would still be the integer `1234`. In other words, *the decimals field in no way changes the actual arithmetic* because all operations are still performed on integers. Most contracts use `18` decimals and this was even proposed to be compulsory (see the {eip-discussion}). -The Contracts for Cairo `ERC20` component includes a `decimals` function that returns `18` by default to save on gas fees. -For those who want an ERC20 token with a configurable number of decimals, the following guide shows two ways to achieve this. - -NOTE: Both approaches require creating a custom implementation of the `IERC20Metadata` interface. -=== The static approach +=== The static approach (SRC-107) -The simplest way to customize `decimals` consists of returning the target value from the `decimals` method. -For example: +The Contracts for Cairo `ERC20` component leverages {SRC-107} to allow for a static and configurable number of decimals. +To use the default `18` decimals, you can use the `DefaultConfig` implementation by just importing it: [,cairo] ---- -#[abi(embed_v0)] -impl ERC20MetadataImpl of interface::IERC20Metadata { - fn decimals(self: @ContractState) -> u8 { - // Change the `3` below to the desired number of decimals - 3 - } +#[starknet::contract] +mod MyToken { + // Importing the DefaultConfig implementation would make decimals 18 by default. + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; (...) } +---- +To customize this value, you can implement the ImmutableConfig trait locally in the contract. +The following example shows how to set the decimals to `6`: + +[,cairo] +---- +mod MyToken { + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + (...) + + // Custom implementation of the ERC20Component ImmutableConfig. + impl ERC20ImmutableConfig of ERC20Component::ImmutableConfig { + const DECIMALS: u8 = 6; + } +} ---- === The storage approach From 1799712a140cd796eeda78e2bcb730c72e60ca9a Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 10 Jan 2025 14:18:42 +0100 Subject: [PATCH 4/5] feat: update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0390606f0..4e9764f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (Breaking) - Bump scarb to v2.9.2 (#1239) +- Add SRC-107 to ERC20Component (#1294) + - `decimals` are now configurable using the ImmutableConfig trait ### Fixed From 4d877d07ade67f48fb8063db43cb351fba736f38 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 10 Jan 2025 14:25:17 +0100 Subject: [PATCH 5/5] feat: format files --- packages/presets/src/erc20.cairo | 2 +- packages/test_common/src/mocks/erc20.cairo | 8 ++++---- packages/test_common/src/mocks/vesting.cairo | 2 +- packages/test_common/src/mocks/votes.cairo | 2 +- packages/token/src/erc20.cairo | 2 +- packages/token/src/tests/erc20/test_erc20.cairo | 4 +--- packages/token/src/tests/erc20/test_erc20_permit.cairo | 2 +- 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/presets/src/erc20.cairo b/packages/presets/src/erc20.cairo index 1127b6a10..0a1c77b5d 100644 --- a/packages/presets/src/erc20.cairo +++ b/packages/presets/src/erc20.cairo @@ -12,7 +12,7 @@ #[starknet::contract] pub mod ERC20Upgradeable { use openzeppelin_access::ownable::OwnableComponent; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; + use openzeppelin_token::erc20::{DefaultConfig, ERC20Component, ERC20HooksEmptyImpl}; use openzeppelin_upgrades::UpgradeableComponent; use openzeppelin_upgrades::interface::IUpgradeable; use starknet::{ClassHash, ContractAddress}; diff --git a/packages/test_common/src/mocks/erc20.cairo b/packages/test_common/src/mocks/erc20.cairo index b47ad7e6f..85e6b59b6 100644 --- a/packages/test_common/src/mocks/erc20.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -1,6 +1,6 @@ #[starknet::contract] pub mod DualCaseERC20Mock { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; + use openzeppelin_token::erc20::{DefaultConfig, ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -41,7 +41,7 @@ pub mod DualCaseERC20Mock { #[starknet::contract] pub mod SnakeERC20Mock { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; + use openzeppelin_token::erc20::{DefaultConfig, ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -82,7 +82,7 @@ pub mod SnakeERC20Mock { /// This is used to test that the hooks are called with the correct arguments. #[starknet::contract] pub mod SnakeERC20MockWithHooks { - use openzeppelin_token::erc20::{ERC20Component, DefaultConfig}; + use openzeppelin_token::erc20::{DefaultConfig, ERC20Component}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -161,7 +161,7 @@ pub mod SnakeERC20MockWithHooks { #[starknet::contract] pub mod DualCaseERC20PermitMock { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; + use openzeppelin_token::erc20::{DefaultConfig, ERC20Component, ERC20HooksEmptyImpl}; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use starknet::ContractAddress; diff --git a/packages/test_common/src/mocks/vesting.cairo b/packages/test_common/src/mocks/vesting.cairo index 295c1869a..f318c1ce3 100644 --- a/packages/test_common/src/mocks/vesting.cairo +++ b/packages/test_common/src/mocks/vesting.cairo @@ -129,7 +129,7 @@ pub mod StepsVestingMock { #[starknet::contract] pub mod ERC20OptionalTransferPanicMock { use openzeppelin_token::erc20::interface::IERC20; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; + use openzeppelin_token::erc20::{DefaultConfig, ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; diff --git a/packages/test_common/src/mocks/votes.cairo b/packages/test_common/src/mocks/votes.cairo index 7553c1e52..b6f4c93a8 100644 --- a/packages/test_common/src/mocks/votes.cairo +++ b/packages/test_common/src/mocks/votes.cairo @@ -1,7 +1,7 @@ #[starknet::contract] pub mod ERC20VotesMock { use openzeppelin_governance::votes::VotesComponent; - use openzeppelin_token::erc20::{ERC20Component, DefaultConfig}; + use openzeppelin_token::erc20::{DefaultConfig, ERC20Component}; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use starknet::ContractAddress; diff --git a/packages/token/src/erc20.cairo b/packages/token/src/erc20.cairo index 331d25433..4087dc429 100644 --- a/packages/token/src/erc20.cairo +++ b/packages/token/src/erc20.cairo @@ -2,5 +2,5 @@ pub mod erc20; pub mod interface; pub mod snip12_utils; -pub use erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig}; +pub use erc20::{DefaultConfig, ERC20Component, ERC20HooksEmptyImpl}; pub use interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; diff --git a/packages/token/src/tests/erc20/test_erc20.cairo b/packages/token/src/tests/erc20/test_erc20.cairo index 3a9e9e295..b4ed0816d 100644 --- a/packages/token/src/tests/erc20/test_erc20.cairo +++ b/packages/token/src/tests/erc20/test_erc20.cairo @@ -4,9 +4,7 @@ use crate::erc20::ERC20Component::{ERC20CamelOnlyImpl, ERC20Impl}; use crate::erc20::ERC20Component::{ERC20MetadataImpl, InternalImpl}; use openzeppelin_test_common::erc20::ERC20SpyHelpers; use openzeppelin_test_common::mocks::erc20::{DualCaseERC20Mock, SnakeERC20MockWithHooks}; -use openzeppelin_testing::constants::{ - NAME, OWNER, RECIPIENT, SPENDER, SUPPLY, SYMBOL, VALUE, ZERO, -}; +use openzeppelin_testing::constants::{NAME, OWNER, RECIPIENT, SPENDER, SUPPLY, SYMBOL, VALUE, ZERO}; use openzeppelin_testing::events::EventSpyExt; use snforge_std::{EventSpy, spy_events, start_cheat_caller_address, test_address}; use starknet::ContractAddress; diff --git a/packages/token/src/tests/erc20/test_erc20_permit.cairo b/packages/token/src/tests/erc20/test_erc20_permit.cairo index 989daf624..37c889631 100644 --- a/packages/token/src/tests/erc20/test_erc20_permit.cairo +++ b/packages/token/src/tests/erc20/test_erc20_permit.cairo @@ -1,9 +1,9 @@ use core::hash::{HashStateExTrait, HashStateTrait}; use core::poseidon::PoseidonTrait; -use crate::erc20::{ERC20Component, DefaultConfig}; use crate::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; use crate::erc20::ERC20Component::{ERC20PermitImpl, SNIP12MetadataExternalImpl}; use crate::erc20::snip12_utils::permit::{PERMIT_TYPE_HASH, Permit}; +use crate::erc20::{DefaultConfig, ERC20Component}; use openzeppelin_test_common::mocks::erc20::DualCaseERC20PermitMock; use openzeppelin_test_common::mocks::erc20::DualCaseERC20PermitMock::SNIP12MetadataImpl; use openzeppelin_testing as utils;