From a055099ec076ade21f342f6cb829863f58cb329d Mon Sep 17 00:00:00 2001 From: immrsd Date: Thu, 5 Sep 2024 16:43:18 +0400 Subject: [PATCH 01/17] Add ERC20Permit component --- packages/token/src/erc20/extensions.cairo | 2 + .../src/erc20/extensions/erc20_permit.cairo | 2 + .../erc20_permit/erc20_permit.cairo | 166 ++++++++++++++++++ .../extensions/erc20_permit/interface.cairo | 58 ++++++ .../utils/src/cryptography/interface.cairo | 5 + 5 files changed, 233 insertions(+) create mode 100644 packages/token/src/erc20/extensions/erc20_permit.cairo create mode 100644 packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo create mode 100644 packages/token/src/erc20/extensions/erc20_permit/interface.cairo diff --git a/packages/token/src/erc20/extensions.cairo b/packages/token/src/erc20/extensions.cairo index e45cdcbf3..d39debc3e 100644 --- a/packages/token/src/erc20/extensions.cairo +++ b/packages/token/src/erc20/extensions.cairo @@ -1,3 +1,5 @@ pub mod erc20_votes; +pub mod erc20_permit; pub use erc20_votes::ERC20VotesComponent; +pub use erc20_permit::erc20_permit::ERC20PermitComponent; diff --git a/packages/token/src/erc20/extensions/erc20_permit.cairo b/packages/token/src/erc20/extensions/erc20_permit.cairo new file mode 100644 index 000000000..f1d24346d --- /dev/null +++ b/packages/token/src/erc20/extensions/erc20_permit.cairo @@ -0,0 +1,2 @@ +pub mod erc20_permit; +pub mod interface; diff --git a/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo b/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo new file mode 100644 index 000000000..8189683f9 --- /dev/null +++ b/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 (token/erc20/extensions/erc20_permit/erc20_permit.cairo) + +use starknet::ContractAddress; +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::poseidon::PoseidonTrait; +use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, StructHash, SNIP12Metadata}; + +/// # ERC20Permit Component +/// +/// The ERC20Permit component implements the EIP-2612 standard, facilitating token approvals via off-chain signatures. +/// This approach allows token holders to delegate their approval to spend tokens without executing an on-chain +/// transaction, reducing gas costs and enhancing usability. The message signed and the signature must follow the +/// SNIP-12 standard for hashing and signing typed structured data. +/// +/// To safeguard against replay attacks and ensure the uniqueness of each approval via `permit`, the data signed includes: +/// - The address of the owner +/// - The parameters specified in the `approve` function (spender and amount) +/// - The address of the token contract itself +/// - A nonce, which must be unique for each operation, incrementing after each use to prevent reuse of the signature +/// - The chain ID, which protects against cross-chain replay attacks +/// +/// EIP-2612: https://eips.ethereum.org/EIPS/eip-2612 +/// SNIP-12: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md +#[starknet::component] +pub mod ERC20PermitComponent { + use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; + use crate::erc20::ERC20Component::InternalTrait; + use crate::erc20::ERC20Component; + use crate::erc20::extensions::erc20_permit::interface::IERC20Permit; + use openzeppelin_utils::cryptography::interface::{INonces, ISNIP12Metadata}; + use openzeppelin_utils::cryptography::snip12::{ + StructHash, OffchainMessageHash, SNIP12Metadata, StarknetDomain + }; + use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; + use openzeppelin_utils::nonces::NoncesComponent; + use starknet::{ContractAddress, get_block_timestamp, get_contract_address, get_tx_info}; + + #[storage] + struct Storage {} + + pub mod Errors { + pub const EXPIRED_SIGNATURE: felt252 = 'ERC20Permit: expired signature'; + pub const INVALID_SIGNATURE: felt252 = 'ERC20Permit: invalid signature'; + } + + // + // External + // + + #[embeddable_as(ERC20PermitImpl)] + impl ERC20Permit< + TContractState, + +HasComponent, + impl ERC20: ERC20Component::HasComponent, + +ERC20Component::ERC20HooksTrait, + impl Nonces: NoncesComponent::HasComponent, + impl Metadata: SNIP12Metadata, + +Drop + > of IERC20Permit> { + /// Sets the allowance of the `spender` over `owner`'s tokens after validating the signature + /// generated off-chain and signed by the `owner`. + /// + /// Requirements: + /// + /// - `owner` is a deployed account contract. + /// - `spender` is not the zero address. + /// - `deadline` is a timestamp in the future. + /// - `signature` is a valid signature that can be validated with a call to `owner` account. + /// - `signature` must use the current nonce of the `owner`. + /// + /// Emits an `Approval` event. + fn permit( + ref self: ComponentState, + owner: ContractAddress, + spender: ContractAddress, + amount: u256, + deadline: u64, + signature: Array + ) { + assert(get_block_timestamp() <= deadline, Errors::EXPIRED_SIGNATURE); + + // Get current nonce and increment it + let mut nonces_component = get_dep_component_mut!(ref self, Nonces); + let nonce = nonces_component.use_nonce(owner); + + // Compute hash for permit + let permit = super::Permit { + token: get_contract_address(), spender, amount, nonce, deadline + }; + let permit_hash = permit.get_message_hash(owner); + + // Make a call to the account to validate permit signature + let is_valid_sig_felt = DualCaseAccount { contract_address: owner } + .is_valid_signature(permit_hash, signature); + + // Check the response is either 'VALID' or True (for backwards compatibility) + let is_valid_sig = is_valid_sig_felt == starknet::VALIDATED || is_valid_sig_felt == 1; + assert(is_valid_sig, Errors::INVALID_SIGNATURE); + + // Approve + let mut erc20_component = get_dep_component_mut!(ref self, ERC20); + erc20_component._approve(owner, spender, amount); + } + + /// Returns the current nonce of the `owner`. A nonce value must be + /// included whenever a signature for `permit` is generated. + fn nonces(self: @ComponentState, owner: ContractAddress) -> felt252 { + let nonces_component = get_dep_component!(self, Nonces); + nonces_component.nonces(owner) + } + + /// Returns the domain separator used in generating a message hash for `permit` signature. + /// The domain hashing logic follows SNIP-12 standard. + fn DOMAIN_SEPARATOR(self: @ComponentState) -> felt252 { + let domain = StarknetDomain { + name: Metadata::name(), + version: Metadata::version(), + chain_id: get_tx_info().unbox().chain_id, + revision: 1 + }; + domain.hash_struct() + } + } + + #[embeddable_as(SNIP12MetadataExternalImpl)] + impl SNIP12MetadataExternal< + TContractState, +HasComponent, impl Metadata: SNIP12Metadata + > of ISNIP12Metadata> { + /// Returns domain name and version used for generating a message hash for `permit` signature. + fn snip12_metadata(self: @ComponentState) -> (felt252, felt252) { + (Metadata::name(), Metadata::version()) + } + } +} + +// +// Hash helpers +// + +#[derive(Copy, Drop, Hash)] +pub struct Permit { + token: ContractAddress, + spender: ContractAddress, + amount: u256, + nonce: felt252, + deadline: u64, +} + +// Since there's no u64 type in SNIP-12, the type used for `deadline` parameter is u128 +// selector!( +// "\"Permit\"( +// \"token\":\"ContractAddress\", +// \"spender\":\"ContractAddress\", +// \"amount\":\"u256\", +// \"nonce\":\"felt\", +// \"deadline\":\"u128\" +// )" +// ); +pub const PERMIT_TYPE_HASH: felt252 = 0x2a8eb238e7cde741a544afcc79fe945d4292b089875fd068633854927fd5a96; + +impl StructHashImpl of StructHash { + fn hash_struct(self: @Permit) -> felt252 { + PoseidonTrait::new().update_with(PERMIT_TYPE_HASH).update_with(*self).finalize() + } +} diff --git a/packages/token/src/erc20/extensions/erc20_permit/interface.cairo b/packages/token/src/erc20/extensions/erc20_permit/interface.cairo new file mode 100644 index 000000000..72b7e0a6a --- /dev/null +++ b/packages/token/src/erc20/extensions/erc20_permit/interface.cairo @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 (token/erc20/extensions/erc20_permit/interface.cairo) + +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC20Permit { + fn permit( + ref self: TState, + owner: ContractAddress, + spender: ContractAddress, + amount: u256, + deadline: u64, + signature: Array + ); + fn nonces(self: @TState, owner: ContractAddress) -> felt252; + fn DOMAIN_SEPARATOR(self: @TState) -> felt252; +} + +#[starknet::interface] +pub trait ERC20PermitABI { + // IERC20 + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + + // IERC20CamelOnly + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + + // IERC20Metadata + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; + + // IERC20Permit + fn permit( + ref self: TState, + owner: ContractAddress, + spender: ContractAddress, + amount: u256, + deadline: u64, + signature: Array + ); + fn nonces(self: @TState, owner: ContractAddress) -> felt252; + fn DOMAIN_SEPARATOR(self: @TState) -> felt252; + + // ISNIP12Metadata + fn snip12_metadata(self: @TState) -> (felt252, felt252); +} diff --git a/packages/utils/src/cryptography/interface.cairo b/packages/utils/src/cryptography/interface.cairo index caace02ef..204dced03 100644 --- a/packages/utils/src/cryptography/interface.cairo +++ b/packages/utils/src/cryptography/interface.cairo @@ -7,3 +7,8 @@ use starknet::ContractAddress; pub trait INonces { fn nonces(self: @TState, owner: ContractAddress) -> felt252; } + +#[starknet::interface] +pub trait ISNIP12Metadata { + fn snip12_metadata(self: @TState) -> (felt252, felt252); +} From cfa836a17c1d9d8174df41ffb6206e407ef6a7f4 Mon Sep 17 00:00:00 2001 From: immrsd Date: Thu, 5 Sep 2024 17:01:01 +0400 Subject: [PATCH 02/17] Add ERC20Permit preset --- packages/presets/src/erc20_permit.cairo | 92 +++++++++++++++++++ packages/presets/src/lib.cairo | 2 + packages/token/src/erc20/extensions.cairo | 4 +- .../erc20_permit/erc20_permit.cairo | 27 +++--- 4 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 packages/presets/src/erc20_permit.cairo diff --git a/packages/presets/src/erc20_permit.cairo b/packages/presets/src/erc20_permit.cairo new file mode 100644 index 000000000..9c84b8611 --- /dev/null +++ b/packages/presets/src/erc20_permit.cairo @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 (presets/erc20_permit.cairo) + +/// # ERC20Permit Preset +/// +/// The ERC20Permit preset integrates standard ERC20 token functionality with the ERC20Permit +/// extension, as defined by EIP-2612. This preset allows for token approvals via off-chain +/// signatures, thus enhancing transaction efficiency by reducing the need for on-chain approval +/// transactions. +/// +/// This implementation features a fixed-supply model and the initial owner is specified in the +/// constructor. +/// +/// The preset implements SNIP12Metadata with hardcoded values. These values are part of a +/// signature (following SNIP-12 standard) used for ERC20Permit functionality. It's crucial that the +/// SNIP12Metadata name remains unique to avoid confusion and potential security issues. +/// +/// For more complex or custom contracts, use Wizard for Cairo +/// https://wizard.openzeppelin.com/cairo +#[starknet::contract] +pub mod ERC20Permit { + use openzeppelin_token::erc20::extensions::ERC20PermitComponent; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_utils::cryptography::nonces::NoncesComponent; + use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: ERC20PermitComponent, storage: erc20_permit, event: ERC20PermitEvent); + component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); + + // ERC20Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + // ERC20Permit + #[abi(embed_v0)] + impl ERC20PermitImpl = ERC20PermitComponent::ERC20PermitImpl; + + // SNIP12Metadata + #[abi(embed_v0)] + impl SNIP12MetadataExternalImpl = + ERC20PermitComponent::SNIP12MetadataExternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + erc20_permit: ERC20PermitComponent::Storage, + #[substorage(v0)] + nonces: NoncesComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + ERC20PermitEvent: ERC20PermitComponent::Event, + #[flat] + NoncesEvent: NoncesComponent::Event + } + + /// Sets the token `name` and `symbol`. + /// Mints `fixed_supply` tokens to `recipient`. + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + fixed_supply: u256, + recipient: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, fixed_supply); + } + + impl SNIP12MetadataImpl of SNIP12Metadata { + /// Returns token name to be used for SNIP-12 signature. + fn name() -> felt252 { + 'My unique token name' + } + + /// Returns token version to be used for SNIP-12 signature. + fn version() -> felt252 { + 'v1' + } + } +} diff --git a/packages/presets/src/lib.cairo b/packages/presets/src/lib.cairo index 55240efd2..b12509ef3 100644 --- a/packages/presets/src/lib.cairo +++ b/packages/presets/src/lib.cairo @@ -1,6 +1,7 @@ pub mod account; pub mod erc1155; pub mod erc20; +pub mod erc20_permit; pub mod erc721; pub mod eth_account; pub mod interfaces; @@ -11,6 +12,7 @@ pub mod universal_deployer; pub use account::AccountUpgradeable; pub use erc1155::ERC1155Upgradeable; pub use erc20::ERC20Upgradeable; +pub use erc20_permit::ERC20Permit; pub use erc721::ERC721Upgradeable; pub use eth_account::EthAccountUpgradeable; pub use universal_deployer::UniversalDeployer; diff --git a/packages/token/src/erc20/extensions.cairo b/packages/token/src/erc20/extensions.cairo index d39debc3e..034c2004b 100644 --- a/packages/token/src/erc20/extensions.cairo +++ b/packages/token/src/erc20/extensions.cairo @@ -1,5 +1,5 @@ -pub mod erc20_votes; pub mod erc20_permit; +pub mod erc20_votes; +pub use erc20_permit::erc20_permit::ERC20PermitComponent; pub use erc20_votes::ERC20VotesComponent; -pub use erc20_permit::erc20_permit::ERC20PermitComponent; diff --git a/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo b/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo index 8189683f9..647a29e5d 100644 --- a/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo +++ b/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo @@ -1,33 +1,35 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.16.0 (token/erc20/extensions/erc20_permit/erc20_permit.cairo) -use starknet::ContractAddress; use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; -use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, StructHash, SNIP12Metadata}; +use openzeppelin_utils::cryptography::snip12::StructHash; +use starknet::ContractAddress; /// # ERC20Permit Component /// -/// The ERC20Permit component implements the EIP-2612 standard, facilitating token approvals via off-chain signatures. -/// This approach allows token holders to delegate their approval to spend tokens without executing an on-chain -/// transaction, reducing gas costs and enhancing usability. The message signed and the signature must follow the -/// SNIP-12 standard for hashing and signing typed structured data. +/// The ERC20Permit component implements the EIP-2612 standard, facilitating token approvals via +/// off-chain signatures. This approach allows token holders to delegate their approval to spend +/// tokens without executing an on-chain transaction, reducing gas costs and enhancing usability. +/// The message signed and the signature must follow the SNIP-12 standard for hashing and signing +/// typed structured data. /// -/// To safeguard against replay attacks and ensure the uniqueness of each approval via `permit`, the data signed includes: +/// To safeguard against replay attacks and ensure the uniqueness of each approval via `permit`, the +/// data signed includes: /// - The address of the owner /// - The parameters specified in the `approve` function (spender and amount) /// - The address of the token contract itself -/// - A nonce, which must be unique for each operation, incrementing after each use to prevent reuse of the signature -/// - The chain ID, which protects against cross-chain replay attacks +/// - A nonce, which must be unique for each operation, incrementing after each use to prevent +/// reuse of the signature - The chain ID, which protects against cross-chain replay attacks /// /// EIP-2612: https://eips.ethereum.org/EIPS/eip-2612 /// SNIP-12: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md #[starknet::component] pub mod ERC20PermitComponent { - use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; use crate::erc20::ERC20Component::InternalTrait; use crate::erc20::ERC20Component; use crate::erc20::extensions::erc20_permit::interface::IERC20Permit; + use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; use openzeppelin_utils::cryptography::interface::{INonces, ISNIP12Metadata}; use openzeppelin_utils::cryptography::snip12::{ StructHash, OffchainMessageHash, SNIP12Metadata, StarknetDomain @@ -127,7 +129,7 @@ pub mod ERC20PermitComponent { impl SNIP12MetadataExternal< TContractState, +HasComponent, impl Metadata: SNIP12Metadata > of ISNIP12Metadata> { - /// Returns domain name and version used for generating a message hash for `permit` signature. + /// Returns domain name and version used for generating a message hash for permit signature. fn snip12_metadata(self: @ComponentState) -> (felt252, felt252) { (Metadata::name(), Metadata::version()) } @@ -157,7 +159,8 @@ pub struct Permit { // \"deadline\":\"u128\" // )" // ); -pub const PERMIT_TYPE_HASH: felt252 = 0x2a8eb238e7cde741a544afcc79fe945d4292b089875fd068633854927fd5a96; +pub const PERMIT_TYPE_HASH: felt252 = + 0x2a8eb238e7cde741a544afcc79fe945d4292b089875fd068633854927fd5a96; impl StructHashImpl of StructHash { fn hash_struct(self: @Permit) -> felt252 { From 9973d9ab2855edcc06b1f2ba393a9f8344a9616d Mon Sep 17 00:00:00 2001 From: immrsd Date: Tue, 10 Sep 2024 13:45:06 +0300 Subject: [PATCH 03/17] Add ERC20Pemit mock and update visibility modifiers --- CHANGELOG.md | 4 ++ packages/testing/src/constants.cairo | 6 ++ .../erc20_permit/erc20_permit.cairo | 10 +-- packages/token/src/tests/erc20.cairo | 1 + packages/token/src/tests/mocks.cairo | 1 + .../src/tests/mocks/erc20_permit_mocks.cairo | 72 +++++++++++++++++++ packages/utils/src/cryptography/snip12.cairo | 2 +- 7 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 packages/token/src/tests/mocks/erc20_permit_mocks.cairo diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a37440f..7f2c6b4ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- ERC20Permit component and preset + ## 0.16.0 (2024-08-30) ### Added diff --git a/packages/testing/src/constants.cairo b/packages/testing/src/constants.cairo index b005f7b47..04d534dcd 100644 --- a/packages/testing/src/constants.cairo +++ b/packages/testing/src/constants.cairo @@ -8,7 +8,9 @@ pub const SUPPLY: u256 = 2_000; pub const VALUE: u256 = 300; pub const FELT_VALUE: felt252 = 'FELT_VALUE'; pub const ROLE: felt252 = 'ROLE'; +pub const TIMESTAMP: u64 = 1704067200; // 2024-01-01 00:00:00 UTC pub const OTHER_ROLE: felt252 = 'OTHER_ROLE'; +pub const CHAIN_ID: felt252 = 'CHAIN_ID'; pub const TOKEN_ID: u256 = 21; pub const TOKEN_ID_2: u256 = 121; pub const TOKEN_VALUE: u256 = 42; @@ -63,6 +65,10 @@ pub fn CALLER() -> ContractAddress { contract_address_const::<'CALLER'>() } +pub fn CONTRACT_ADDRESS() -> ContractAddress { + contract_address_const::<'CONTRACT_ADDRESS'>() +} + pub fn OWNER() -> ContractAddress { contract_address_const::<'OWNER'>() } diff --git a/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo b/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo index 647a29e5d..05a831e3d 100644 --- a/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo +++ b/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo @@ -142,11 +142,11 @@ pub mod ERC20PermitComponent { #[derive(Copy, Drop, Hash)] pub struct Permit { - token: ContractAddress, - spender: ContractAddress, - amount: u256, - nonce: felt252, - deadline: u64, + pub token: ContractAddress, + pub spender: ContractAddress, + pub amount: u256, + pub nonce: felt252, + pub deadline: u64, } // Since there's no u64 type in SNIP-12, the type used for `deadline` parameter is u128 diff --git a/packages/token/src/tests/erc20.cairo b/packages/token/src/tests/erc20.cairo index 213861a92..aa7d68fa8 100644 --- a/packages/token/src/tests/erc20.cairo +++ b/packages/token/src/tests/erc20.cairo @@ -1,3 +1,4 @@ mod test_dual20; mod test_erc20; +mod test_erc20_permit; mod test_erc20_votes; diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo index 9574a4b7a..22f2aa689 100644 --- a/packages/token/src/tests/mocks.cairo +++ b/packages/token/src/tests/mocks.cairo @@ -2,6 +2,7 @@ pub(crate) mod account_mocks; pub(crate) mod erc1155_mocks; pub(crate) mod erc1155_receiver_mocks; pub(crate) mod erc20_mocks; +pub(crate) mod erc20_permit_mocks; pub(crate) mod erc20_votes_mocks; pub(crate) mod erc2981_mocks; pub(crate) mod erc721_enumerable_mocks; diff --git a/packages/token/src/tests/mocks/erc20_permit_mocks.cairo b/packages/token/src/tests/mocks/erc20_permit_mocks.cairo new file mode 100644 index 000000000..ae4d5d9ed --- /dev/null +++ b/packages/token/src/tests/mocks/erc20_permit_mocks.cairo @@ -0,0 +1,72 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC20PermitMock { + use crate::erc20::extensions::ERC20PermitComponent; + use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_utils::cryptography::nonces::NoncesComponent; + use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: ERC20PermitComponent, storage: erc20_permit, event: ERC20PermitEvent); + component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); + + // ERC20Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl InternalImpl = ERC20Component::InternalImpl; + + // ERC20Permit + #[abi(embed_v0)] + impl ERC20PermitComponentImpl = + ERC20PermitComponent::ERC20PermitImpl; + + // SNIP12Metadata + #[abi(embed_v0)] + impl SNIP12MetadataExternalImpl = + ERC20PermitComponent::SNIP12MetadataExternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + erc20_permit: ERC20PermitComponent::Storage, + #[substorage(v0)] + nonces: NoncesComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + ERC20PermitEvent: ERC20PermitComponent::Event, + #[flat] + NoncesEvent: NoncesComponent::Event + } + + /// Required for hash computation. + pub(crate) impl SNIP12MetadataImpl of SNIP12Metadata { + fn name() -> felt252 { + 'DAPP_NAME' + } + fn version() -> felt252 { + 'DAPP_VERSION' + } + } + + /// Sets the token `name` and `symbol`. + /// Mints `fixed_supply` tokens to `recipient`. + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + initial_supply: u256, + recipient: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + } +} diff --git a/packages/utils/src/cryptography/snip12.cairo b/packages/utils/src/cryptography/snip12.cairo index f227c62dc..1758abccd 100644 --- a/packages/utils/src/cryptography/snip12.cairo +++ b/packages/utils/src/cryptography/snip12.cairo @@ -32,7 +32,7 @@ pub trait OffchainMessageHash { fn get_message_hash(self: @T, signer: ContractAddress) -> felt252; } -impl StructHashStarknetDomainImpl of StructHash { +pub impl StructHashStarknetDomainImpl of StructHash { fn hash_struct(self: @StarknetDomain) -> felt252 { let hash_state = PoseidonTrait::new(); hash_state.update_with(STARKNET_DOMAIN_TYPE_HASH).update_with(*self).finalize() From 23ba5daea7d2fba68a3a076240727989f528fd09 Mon Sep 17 00:00:00 2001 From: immrsd Date: Tue, 10 Sep 2024 13:45:45 +0300 Subject: [PATCH 04/17] Add tests for ERC20Permit component --- .../src/tests/erc20/test_erc20_permit.cairo | 487 ++++++++++++++++++ 1 file changed, 487 insertions(+) create mode 100644 packages/token/src/tests/erc20/test_erc20_permit.cairo diff --git a/packages/token/src/tests/erc20/test_erc20_permit.cairo b/packages/token/src/tests/erc20/test_erc20_permit.cairo new file mode 100644 index 000000000..531410a22 --- /dev/null +++ b/packages/token/src/tests/erc20/test_erc20_permit.cairo @@ -0,0 +1,487 @@ +use core::poseidon::PoseidonTrait; +use core::hash::{HashStateTrait, HashStateExTrait}; +use openzeppelin_testing::constants; +use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; +use openzeppelin_testing as utils; +use crate::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; +use crate::erc20::extensions::ERC20PermitComponent::{ + ERC20PermitImpl, SNIP12MetadataExternalImpl +}; +use crate::erc20::extensions::erc20_permit::interface::{ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait}; +use crate::erc20::extensions::erc20_permit::erc20_permit::{Permit, PERMIT_TYPE_HASH}; +use openzeppelin_utils::cryptography::snip12::{ + StarknetDomain, StructHashStarknetDomainImpl +}; +use openzeppelin_utils::serde::SerializedAppend; +use snforge_std::signature::stark_curve::StarkCurveSignerImpl; +use snforge_std::{start_cheat_caller_address, start_cheat_block_timestamp, start_cheat_chain_id_global}; +use starknet::ContractAddress; + +// +// Test Data +// + +#[derive(Copy, Drop)] +struct TestData { + contract_address: ContractAddress, + owner: ContractAddress, + key_pair: StarkKeyPair, + spender: ContractAddress, + amount: u256, + deadline: u64, + token_supply: u256, + name: @ByteArray, + symbol: @ByteArray, + metadata_name: felt252, + metadata_version: felt252, + chain_id: felt252, + revision: felt252 +} + +fn TEST_DATA() -> TestData { + TestData { + contract_address: constants::CONTRACT_ADDRESS(), + owner: constants::OWNER(), + key_pair: constants::stark::KEY_PAIR(), + spender: constants::SPENDER(), + amount: constants::TOKEN_VALUE, + deadline: constants::TIMESTAMP, + token_supply: constants::SUPPLY, + name: @constants::NAME(), + symbol: @constants::SYMBOL(), + metadata_name: constants::DAPP_NAME, // As in DualCaseERC20PermitMock + metadata_version: constants::DAPP_VERSION, // As in DualCaseERC20PermitMock + chain_id: constants::CHAIN_ID, + revision: 1 // As in the current SNIP-12 implementation + } +} + +// +// Setup +// + +fn setup(data: TestData) -> ERC20PermitABIDispatcher { + start_cheat_chain_id_global(data.chain_id); + + utils::declare_and_deploy_at("DualCaseAccountMock", data.owner, array![data.key_pair.public_key]); + + let mut calldata = array![]; + calldata.append_serde(data.name.clone()); + calldata.append_serde(data.symbol.clone()); + calldata.append_serde(data.token_supply); + calldata.append_serde(data.owner); + utils::declare_and_deploy_at("DualCaseERC20PermitMock", data.contract_address, calldata); + + ERC20PermitABIDispatcher { contract_address: data.contract_address } +} + +// +// IERC20Permit +// + +#[test] +fn test_valid_permit_default_data() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + assert_valid_allowance(owner, spender, 0); + + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + mock.permit(owner, spender, amount, deadline, signature); + + assert_valid_allowance(owner, spender, amount); + assert_valid_nonce(owner, nonce + 1); +} + +#[test] +fn test_valid_permit_other_data() { + let mut data = TEST_DATA(); + data.spender = constants::OTHER(); + data.amount = constants::TOKEN_VALUE_2; + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + assert_valid_allowance(owner, spender, 0); + + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + mock.permit(owner, spender, amount, deadline, signature); + + assert_valid_allowance(owner, spender, amount); + assert_valid_nonce(owner, nonce + 1); +} + +#[test] +fn test_spend_permit() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + start_cheat_caller_address(mock.contract_address, spender); + + mock.permit(owner, spender, amount, deadline, signature); + mock.transfer_from(owner, spender, amount); + + assert_valid_balance(spender, amount); + assert_valid_balance(owner, data.token_supply - amount); + assert_valid_allowance(owner, spender, 0); + assert_valid_nonce(owner, nonce + 1); +} + +#[test] +fn test_spend_half_permit() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + start_cheat_caller_address(mock.contract_address, spender); + + mock.permit(owner, spender, amount, deadline, signature); + let transfer_amount = amount / 2; + mock.transfer_from(owner, spender, transfer_amount); + + assert_valid_balance(spender, transfer_amount); + assert_valid_balance(owner, data.token_supply - transfer_amount); + assert_valid_allowance(owner, spender, amount - transfer_amount); + assert_valid_nonce(owner, nonce + 1); +} + +#[test] +fn test_subsequent_permits() { + let mut data = TEST_DATA(); + let (owner, spender, amount_1, deadline) = ( + data.owner, data.spender, data.amount, data.deadline + ); + let mock = setup(data); + + let mut expected_owner_balance = data.token_supply; + let mut expected_spender_balance = 0; + start_cheat_caller_address(mock.contract_address, spender); + + // Permit 1 + let nonce_1 = mock.nonces(owner); + let signature_1 = prepare_permit_signature(data, nonce_1); + + mock.permit(owner, spender, amount_1, deadline, signature_1); + mock.transfer_from(owner, spender, amount_1); + + expected_owner_balance -= amount_1; + expected_spender_balance += amount_1; + assert_valid_balance(owner, expected_owner_balance); + assert_valid_balance(spender, expected_spender_balance); + assert_valid_allowance(owner, spender, 0); + assert_valid_nonce(owner, nonce_1 + 1); + + // Permit 2 + data.amount = constants::TOKEN_VALUE_2; + let amount_2 = data.amount; + let nonce_2 = mock.nonces(owner); + let signature_2 = prepare_permit_signature(data, nonce_2); + + mock.permit(owner, spender, amount_2, deadline, signature_2); + mock.transfer_from(owner, spender, amount_2); + + expected_owner_balance -= amount_2; + expected_spender_balance += amount_2; + assert_valid_balance(owner, expected_owner_balance); + assert_valid_balance(spender, expected_spender_balance); + assert_valid_allowance(owner, spender, 0); + assert_valid_nonce(owner, nonce_2 + 1); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_replay_attack() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let nonce = mock.nonces(owner); + start_cheat_caller_address(mock.contract_address, spender); + + // 1st call is fine + let signature = prepare_permit_signature(data, nonce); + mock.permit(owner, spender, amount, deadline, signature); + + // 2nd call must fail (nonce already used) + let signature = prepare_permit_signature(data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +fn test_domain_separator() { + let data = TEST_DATA(); + let mock = setup(data); + + let sn_domain = StarknetDomain { + name: data.metadata_name, + version: data.metadata_version, + chain_id: data.chain_id, + revision: data.revision + }; + let expected_domain_separator = sn_domain.hash_struct(); + assert_eq!(mock.DOMAIN_SEPARATOR(), expected_domain_separator); +} + +// +// SNIP12Metadata +// + +#[test] +fn test_permit_type_hash() { + let expected_type_hash = selector!( + "\"Permit\"(\"token\":\"ContractAddress\",\"spender\":\"ContractAddress\",\"amount\":\"u256\",\"nonce\":\"felt\",\"deadline\":\"u128\")" + ); + assert_eq!(PERMIT_TYPE_HASH, expected_type_hash); +} + +#[test] +fn test_snip12_metadata() { + let data = TEST_DATA(); + let mock = setup(data); + + let (metadata_name, metadata_version) = mock.snip12_metadata(); + assert_eq!(metadata_name, data.metadata_name, "Invalid metadata name"); + assert_eq!(metadata_version, data.metadata_version, "Invalid metadata version"); +} + +// +// Invalid signature +// + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_owner() { + let data = TEST_DATA(); + let (spender, amount, deadline) = (data.spender, data.amount, data.deadline); + let mock = setup(data); + + let another_account = constants::OTHER(); + utils::deploy_another_at(data.owner, another_account, array![data.key_pair.public_key]); + let nonce = mock.nonces(another_account); + let signature = prepare_permit_signature(data, nonce); + mock.permit(another_account, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_token_address() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + mock.permit(owner, spender, amount, deadline, signature); + + let mut modified_data = data; + modified_data.contract_address = constants::OTHER(); + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_spender() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let mut modified_data = data; + modified_data.spender = constants::OTHER(); + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_amount() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let mut modified_data = data; + modified_data.amount = constants::TOKEN_VALUE_2; + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_nonce() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let another_nonce = mock.nonces(owner) + 1; + let signature = prepare_permit_signature(data, another_nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_sig_r() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + let (sig_r, sig_s) = (*signature.at(0), *signature.at(1)); + let modified_signature = array![sig_r + 1, sig_s]; + mock.permit(owner, spender, amount, deadline, modified_signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_sig_s() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + let (sig_r, sig_s) = (*signature.at(0), *signature.at(1)); + let modified_signature = array![sig_r, sig_s + 1]; + mock.permit(owner, spender, amount, deadline, modified_signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_metadata_name() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let mut modified_data = data; + modified_data.metadata_name = 'ANOTHER_NAME'; + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_metadata_version() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let mut modified_data = data; + modified_data.metadata_version = 'ANOTHER_VERSION'; + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_signing_key() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let mut modified_data = data; + modified_data.key_pair = constants::stark::KEY_PAIR_2(); + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_chain_id() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let mut modified_data = data; + modified_data.chain_id = 'ANOTHER_CHAIN_ID'; + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_revision() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let mut modified_data = data; + modified_data.revision = 'ANOTHER_REVISION'; + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +// +// Expired signature +// + +#[test] +#[should_panic(expected: ('ERC20Permit: expired signature',))] +fn test_invalid_sig_bad_deadline() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let mock = setup(data); + + let timestamp_after_deadline = deadline + 1; + start_cheat_block_timestamp(mock.contract_address, timestamp_after_deadline); + let nonce = mock.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + mock.permit(owner, spender, amount, deadline, signature); +} + +// +// Helpers +// + +fn prepare_permit_signature( + data: TestData, nonce: felt252 +) -> Array { + let sn_domain = StarknetDomain { + name: data.metadata_name, + version: data.metadata_version, + chain_id: data.chain_id, + revision: data.revision + }; + let permit = Permit { + token: data.contract_address, spender: data.spender, amount: data.amount, nonce, deadline: data.deadline + }; + let msg_hash = PoseidonTrait::new() + .update_with('StarkNet Message') + .update_with(sn_domain.hash_struct()) + .update_with(data.owner) + .update_with(permit.hash_struct()) + .finalize(); + + data.key_pair.serialized_sign(msg_hash) +} + +fn assert_valid_nonce(account: ContractAddress, expected: felt252) { + let mock = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; + assert_eq!(mock.nonces(account), expected); +} + +fn assert_valid_allowance( + owner: ContractAddress, spender: ContractAddress, expected: u256 +) { + let mock = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; + assert_eq!(mock.allowance(owner, spender), expected); +} + +fn assert_valid_balance(account: ContractAddress, expected: u256) { + let mock = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; + assert_eq!(mock.balance_of(account), expected); +} From 87f18591cf7b13940178ff2c5b1cf18179f10e93 Mon Sep 17 00:00:00 2001 From: immrsd Date: Tue, 10 Sep 2024 13:59:16 +0300 Subject: [PATCH 05/17] Add tests for ERC20Permit preset --- packages/presets/src/tests.cairo | 2 + .../presets/src/tests/test_erc20_permit.cairo | 479 ++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 packages/presets/src/tests/test_erc20_permit.cairo diff --git a/packages/presets/src/tests.cairo b/packages/presets/src/tests.cairo index fa4c3e3c2..34786a20c 100644 --- a/packages/presets/src/tests.cairo +++ b/packages/presets/src/tests.cairo @@ -7,6 +7,8 @@ mod test_erc1155; #[cfg(test)] mod test_erc20; #[cfg(test)] +mod test_erc20_permit; +#[cfg(test)] mod test_erc721; #[cfg(test)] mod test_eth_account; diff --git a/packages/presets/src/tests/test_erc20_permit.cairo b/packages/presets/src/tests/test_erc20_permit.cairo new file mode 100644 index 000000000..bfdd0b2f2 --- /dev/null +++ b/packages/presets/src/tests/test_erc20_permit.cairo @@ -0,0 +1,479 @@ +use core::poseidon::PoseidonTrait; +use core::hash::{HashStateTrait, HashStateExTrait}; +use openzeppelin_testing::constants; +use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; +use openzeppelin_testing as utils; +use openzeppelin_token::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; +use openzeppelin_token::erc20::extensions::ERC20PermitComponent::{ + ERC20PermitImpl, SNIP12MetadataExternalImpl +}; +use openzeppelin_token::erc20::extensions::erc20_permit::interface::{ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait}; +use openzeppelin_token::erc20::extensions::erc20_permit::erc20_permit::Permit; +use openzeppelin_utils::cryptography::snip12::{ + StarknetDomain, StructHashStarknetDomainImpl +}; +use openzeppelin_utils::serde::SerializedAppend; +use snforge_std::signature::stark_curve::StarkCurveSignerImpl; +use snforge_std::{start_cheat_caller_address, start_cheat_block_timestamp, start_cheat_chain_id_global}; +use starknet::ContractAddress; + +// +// Test Data +// + +#[derive(Copy, Drop)] +struct TestData { + contract_address: ContractAddress, + owner: ContractAddress, + key_pair: StarkKeyPair, + spender: ContractAddress, + amount: u256, + deadline: u64, + token_supply: u256, + name: @ByteArray, + symbol: @ByteArray, + metadata_name: felt252, + metadata_version: felt252, + chain_id: felt252, + revision: felt252 +} + +fn TEST_DATA() -> TestData { + TestData { + contract_address: constants::CONTRACT_ADDRESS(), + owner: constants::OWNER(), + key_pair: constants::stark::KEY_PAIR(), + spender: constants::SPENDER(), + amount: constants::TOKEN_VALUE, + deadline: constants::TIMESTAMP, + token_supply: constants::SUPPLY, + name: @constants::NAME(), + symbol: @constants::SYMBOL(), + metadata_name: 'My unique token name', // As in ERC20Permit preset + metadata_version: 'v1', // As in ERC20Permit preset + chain_id: constants::CHAIN_ID, + revision: 1 // As in the current SNIP-12 implementation + } +} + +// +// Setup +// + +fn setup(data: TestData) -> ERC20PermitABIDispatcher { + start_cheat_chain_id_global(data.chain_id); + + utils::declare_and_deploy_at("DualCaseAccountMock", data.owner, array![data.key_pair.public_key]); + + let mut calldata = array![]; + calldata.append_serde(data.name.clone()); + calldata.append_serde(data.symbol.clone()); + calldata.append_serde(data.token_supply); + calldata.append_serde(data.owner); + utils::declare_and_deploy_at("ERC20Permit", data.contract_address, calldata); + + ERC20PermitABIDispatcher { contract_address: data.contract_address } +} + +// +// IERC20Permit +// + +#[test] +fn test_valid_permit_default_data() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + assert_valid_allowance(owner, spender, 0); + + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); + + assert_valid_allowance(owner, spender, amount); + assert_valid_nonce(owner, nonce + 1); +} + +#[test] +fn test_valid_permit_other_data() { + let mut data = TEST_DATA(); + data.spender = constants::OTHER(); + data.amount = constants::TOKEN_VALUE_2; + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + assert_valid_allowance(owner, spender, 0); + + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); + + assert_valid_allowance(owner, spender, amount); + assert_valid_nonce(owner, nonce + 1); +} + +#[test] +fn test_spend_permit() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + start_cheat_caller_address(dispatcher.contract_address, spender); + + dispatcher.permit(owner, spender, amount, deadline, signature); + dispatcher.transfer_from(owner, spender, amount); + + assert_valid_balance(spender, amount); + assert_valid_balance(owner, data.token_supply - amount); + assert_valid_allowance(owner, spender, 0); + assert_valid_nonce(owner, nonce + 1); +} + +#[test] +fn test_spend_half_permit() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + start_cheat_caller_address(dispatcher.contract_address, spender); + + dispatcher.permit(owner, spender, amount, deadline, signature); + let transfer_amount = amount / 2; + dispatcher.transfer_from(owner, spender, transfer_amount); + + assert_valid_balance(spender, transfer_amount); + assert_valid_balance(owner, data.token_supply - transfer_amount); + assert_valid_allowance(owner, spender, amount - transfer_amount); + assert_valid_nonce(owner, nonce + 1); +} + +#[test] +fn test_subsequent_permits() { + let mut data = TEST_DATA(); + let (owner, spender, amount_1, deadline) = ( + data.owner, data.spender, data.amount, data.deadline + ); + let dispatcher = setup(data); + + let mut expected_owner_balance = data.token_supply; + let mut expected_spender_balance = 0; + start_cheat_caller_address(dispatcher.contract_address, spender); + + // Permit 1 + let nonce_1 = dispatcher.nonces(owner); + let signature_1 = prepare_permit_signature(data, nonce_1); + + dispatcher.permit(owner, spender, amount_1, deadline, signature_1); + dispatcher.transfer_from(owner, spender, amount_1); + + expected_owner_balance -= amount_1; + expected_spender_balance += amount_1; + assert_valid_balance(owner, expected_owner_balance); + assert_valid_balance(spender, expected_spender_balance); + assert_valid_allowance(owner, spender, 0); + assert_valid_nonce(owner, nonce_1 + 1); + + // Permit 2 + data.amount = constants::TOKEN_VALUE_2; + let amount_2 = data.amount; + let nonce_2 = dispatcher.nonces(owner); + let signature_2 = prepare_permit_signature(data, nonce_2); + + dispatcher.permit(owner, spender, amount_2, deadline, signature_2); + dispatcher.transfer_from(owner, spender, amount_2); + + expected_owner_balance -= amount_2; + expected_spender_balance += amount_2; + assert_valid_balance(owner, expected_owner_balance); + assert_valid_balance(spender, expected_spender_balance); + assert_valid_allowance(owner, spender, 0); + assert_valid_nonce(owner, nonce_2 + 1); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_replay_attack() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let nonce = dispatcher.nonces(owner); + start_cheat_caller_address(dispatcher.contract_address, spender); + + // 1st call is fine + let signature = prepare_permit_signature(data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); + + // 2nd call must fail (nonce already used) + let signature = prepare_permit_signature(data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +fn test_domain_separator() { + let data = TEST_DATA(); + let dispatcher = setup(data); + + let sn_domain = StarknetDomain { + name: data.metadata_name, + version: data.metadata_version, + chain_id: data.chain_id, + revision: data.revision + }; + let expected_domain_separator = sn_domain.hash_struct(); + assert_eq!(dispatcher.DOMAIN_SEPARATOR(), expected_domain_separator); +} + +// +// SNIP12Metadata +// + +#[test] +fn test_snip12_metadata() { + let data = TEST_DATA(); + let dispatcher = setup(data); + + let (metadata_name, metadata_version) = dispatcher.snip12_metadata(); + assert_eq!(metadata_name, data.metadata_name, "Invalid metadata name"); + assert_eq!(metadata_version, data.metadata_version, "Invalid metadata version"); +} + +// +// Invalid signature +// + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_owner() { + let data = TEST_DATA(); + let (spender, amount, deadline) = (data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let another_account = constants::OTHER(); + utils::deploy_another_at(data.owner, another_account, array![data.key_pair.public_key]); + let nonce = dispatcher.nonces(another_account); + let signature = prepare_permit_signature(data, nonce); + dispatcher.permit(another_account, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_token_address() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); + + let mut modified_data = data; + modified_data.contract_address = constants::OTHER(); + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_spender() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let mut modified_data = data; + modified_data.spender = constants::OTHER(); + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_amount() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let mut modified_data = data; + modified_data.amount = constants::TOKEN_VALUE_2; + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_nonce() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let another_nonce = dispatcher.nonces(owner) + 1; + let signature = prepare_permit_signature(data, another_nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_sig_r() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + let (sig_r, sig_s) = (*signature.at(0), *signature.at(1)); + let modified_signature = array![sig_r + 1, sig_s]; + dispatcher.permit(owner, spender, amount, deadline, modified_signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_sig_s() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + let (sig_r, sig_s) = (*signature.at(0), *signature.at(1)); + let modified_signature = array![sig_r, sig_s + 1]; + dispatcher.permit(owner, spender, amount, deadline, modified_signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_metadata_name() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let mut modified_data = data; + modified_data.metadata_name = 'ANOTHER_NAME'; + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_metadata_version() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let mut modified_data = data; + modified_data.metadata_version = 'ANOTHER_VERSION'; + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_signing_key() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let mut modified_data = data; + modified_data.key_pair = constants::stark::KEY_PAIR_2(); + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_chain_id() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let mut modified_data = data; + modified_data.chain_id = 'ANOTHER_CHAIN_ID'; + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +#[test] +#[should_panic(expected: ('ERC20Permit: invalid signature',))] +fn test_invalid_sig_bad_revision() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let mut modified_data = data; + modified_data.revision = 'ANOTHER_REVISION'; + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(modified_data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +// +// Expired signature +// + +#[test] +#[should_panic(expected: ('ERC20Permit: expired signature',))] +fn test_invalid_sig_bad_deadline() { + let data = TEST_DATA(); + let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); + let dispatcher = setup(data); + + let timestamp_after_deadline = deadline + 1; + start_cheat_block_timestamp(dispatcher.contract_address, timestamp_after_deadline); + let nonce = dispatcher.nonces(owner); + let signature = prepare_permit_signature(data, nonce); + dispatcher.permit(owner, spender, amount, deadline, signature); +} + +// +// Helpers +// + +fn prepare_permit_signature( + data: TestData, nonce: felt252 +) -> Array { + let sn_domain = StarknetDomain { + name: data.metadata_name, + version: data.metadata_version, + chain_id: data.chain_id, + revision: data.revision + }; + let permit = Permit { + token: data.contract_address, spender: data.spender, amount: data.amount, nonce, deadline: data.deadline + }; + let msg_hash = PoseidonTrait::new() + .update_with('StarkNet Message') + .update_with(sn_domain.hash_struct()) + .update_with(data.owner) + .update_with(permit.hash_struct()) + .finalize(); + + data.key_pair.serialized_sign(msg_hash) +} + +fn assert_valid_nonce(account: ContractAddress, expected: felt252) { + let dispatcher = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; + assert_eq!(dispatcher.nonces(account), expected); +} + +fn assert_valid_allowance( + owner: ContractAddress, spender: ContractAddress, expected: u256 +) { + let dispatcher = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; + assert_eq!(dispatcher.allowance(owner, spender), expected); +} + +fn assert_valid_balance(account: ContractAddress, expected: u256) { + let dispatcher = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; + assert_eq!(dispatcher.balance_of(account), expected); +} From 67c3fc7c7a28772b0afba34e356d2ba7e97381fb Mon Sep 17 00:00:00 2001 From: Alexander Antonov Date: Tue, 10 Sep 2024 14:06:45 +0300 Subject: [PATCH 06/17] Run linter --- .../presets/src/tests/test_erc20_permit.cairo | 32 ++++++++------- .../src/tests/erc20/test_erc20_permit.cairo | 40 ++++++++++--------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/packages/presets/src/tests/test_erc20_permit.cairo b/packages/presets/src/tests/test_erc20_permit.cairo index bfdd0b2f2..9849a48e6 100644 --- a/packages/presets/src/tests/test_erc20_permit.cairo +++ b/packages/presets/src/tests/test_erc20_permit.cairo @@ -1,20 +1,22 @@ -use core::poseidon::PoseidonTrait; use core::hash::{HashStateTrait, HashStateExTrait}; +use core::poseidon::PoseidonTrait; +use openzeppelin_testing as utils; use openzeppelin_testing::constants; use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; -use openzeppelin_testing as utils; use openzeppelin_token::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; use openzeppelin_token::erc20::extensions::ERC20PermitComponent::{ ERC20PermitImpl, SNIP12MetadataExternalImpl }; -use openzeppelin_token::erc20::extensions::erc20_permit::interface::{ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait}; use openzeppelin_token::erc20::extensions::erc20_permit::erc20_permit::Permit; -use openzeppelin_utils::cryptography::snip12::{ - StarknetDomain, StructHashStarknetDomainImpl +use openzeppelin_token::erc20::extensions::erc20_permit::interface::{ + ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait }; +use openzeppelin_utils::cryptography::snip12::{StarknetDomain, StructHashStarknetDomainImpl}; use openzeppelin_utils::serde::SerializedAppend; use snforge_std::signature::stark_curve::StarkCurveSignerImpl; -use snforge_std::{start_cheat_caller_address, start_cheat_block_timestamp, start_cheat_chain_id_global}; +use snforge_std::{ + start_cheat_caller_address, start_cheat_block_timestamp, start_cheat_chain_id_global +}; use starknet::ContractAddress; // @@ -63,7 +65,9 @@ fn TEST_DATA() -> TestData { fn setup(data: TestData) -> ERC20PermitABIDispatcher { start_cheat_chain_id_global(data.chain_id); - utils::declare_and_deploy_at("DualCaseAccountMock", data.owner, array![data.key_pair.public_key]); + utils::declare_and_deploy_at( + "DualCaseAccountMock", data.owner, array![data.key_pair.public_key] + ); let mut calldata = array![]; calldata.append_serde(data.name.clone()); @@ -439,9 +443,7 @@ fn test_invalid_sig_bad_deadline() { // Helpers // -fn prepare_permit_signature( - data: TestData, nonce: felt252 -) -> Array { +fn prepare_permit_signature(data: TestData, nonce: felt252) -> Array { let sn_domain = StarknetDomain { name: data.metadata_name, version: data.metadata_version, @@ -449,7 +451,11 @@ fn prepare_permit_signature( revision: data.revision }; let permit = Permit { - token: data.contract_address, spender: data.spender, amount: data.amount, nonce, deadline: data.deadline + token: data.contract_address, + spender: data.spender, + amount: data.amount, + nonce, + deadline: data.deadline }; let msg_hash = PoseidonTrait::new() .update_with('StarkNet Message') @@ -466,9 +472,7 @@ fn assert_valid_nonce(account: ContractAddress, expected: felt252) { assert_eq!(dispatcher.nonces(account), expected); } -fn assert_valid_allowance( - owner: ContractAddress, spender: ContractAddress, expected: u256 -) { +fn assert_valid_allowance(owner: ContractAddress, spender: ContractAddress, expected: u256) { let dispatcher = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; assert_eq!(dispatcher.allowance(owner, spender), expected); } diff --git a/packages/token/src/tests/erc20/test_erc20_permit.cairo b/packages/token/src/tests/erc20/test_erc20_permit.cairo index 531410a22..06f6d8197 100644 --- a/packages/token/src/tests/erc20/test_erc20_permit.cairo +++ b/packages/token/src/tests/erc20/test_erc20_permit.cairo @@ -1,20 +1,20 @@ -use core::poseidon::PoseidonTrait; use core::hash::{HashStateTrait, HashStateExTrait}; -use openzeppelin_testing::constants; -use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; -use openzeppelin_testing as utils; +use core::poseidon::PoseidonTrait; use crate::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; -use crate::erc20::extensions::ERC20PermitComponent::{ - ERC20PermitImpl, SNIP12MetadataExternalImpl -}; -use crate::erc20::extensions::erc20_permit::interface::{ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait}; +use crate::erc20::extensions::ERC20PermitComponent::{ERC20PermitImpl, SNIP12MetadataExternalImpl}; use crate::erc20::extensions::erc20_permit::erc20_permit::{Permit, PERMIT_TYPE_HASH}; -use openzeppelin_utils::cryptography::snip12::{ - StarknetDomain, StructHashStarknetDomainImpl +use crate::erc20::extensions::erc20_permit::interface::{ + ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait }; +use openzeppelin_testing as utils; +use openzeppelin_testing::constants; +use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; +use openzeppelin_utils::cryptography::snip12::{StarknetDomain, StructHashStarknetDomainImpl}; use openzeppelin_utils::serde::SerializedAppend; use snforge_std::signature::stark_curve::StarkCurveSignerImpl; -use snforge_std::{start_cheat_caller_address, start_cheat_block_timestamp, start_cheat_chain_id_global}; +use snforge_std::{ + start_cheat_caller_address, start_cheat_block_timestamp, start_cheat_chain_id_global +}; use starknet::ContractAddress; // @@ -63,7 +63,9 @@ fn TEST_DATA() -> TestData { fn setup(data: TestData) -> ERC20PermitABIDispatcher { start_cheat_chain_id_global(data.chain_id); - utils::declare_and_deploy_at("DualCaseAccountMock", data.owner, array![data.key_pair.public_key]); + utils::declare_and_deploy_at( + "DualCaseAccountMock", data.owner, array![data.key_pair.public_key] + ); let mut calldata = array![]; calldata.append_serde(data.name.clone()); @@ -447,9 +449,7 @@ fn test_invalid_sig_bad_deadline() { // Helpers // -fn prepare_permit_signature( - data: TestData, nonce: felt252 -) -> Array { +fn prepare_permit_signature(data: TestData, nonce: felt252) -> Array { let sn_domain = StarknetDomain { name: data.metadata_name, version: data.metadata_version, @@ -457,7 +457,11 @@ fn prepare_permit_signature( revision: data.revision }; let permit = Permit { - token: data.contract_address, spender: data.spender, amount: data.amount, nonce, deadline: data.deadline + token: data.contract_address, + spender: data.spender, + amount: data.amount, + nonce, + deadline: data.deadline }; let msg_hash = PoseidonTrait::new() .update_with('StarkNet Message') @@ -474,9 +478,7 @@ fn assert_valid_nonce(account: ContractAddress, expected: felt252) { assert_eq!(mock.nonces(account), expected); } -fn assert_valid_allowance( - owner: ContractAddress, spender: ContractAddress, expected: u256 -) { +fn assert_valid_allowance(owner: ContractAddress, spender: ContractAddress, expected: u256) { let mock = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; assert_eq!(mock.allowance(owner, spender), expected); } From 4bfbe36e3c042d55094cffd9ab433a36bf449273 Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 25 Sep 2024 15:27:35 +0200 Subject: [PATCH 07/17] Remove ERC20Permit preset --- packages/presets/src/erc20_permit.cairo | 92 ---- packages/presets/src/lib.cairo | 2 - packages/presets/src/tests.cairo | 2 - .../presets/src/tests/test_erc20_permit.cairo | 483 ------------------ 4 files changed, 579 deletions(-) delete mode 100644 packages/presets/src/erc20_permit.cairo delete mode 100644 packages/presets/src/tests/test_erc20_permit.cairo diff --git a/packages/presets/src/erc20_permit.cairo b/packages/presets/src/erc20_permit.cairo deleted file mode 100644 index 9c84b8611..000000000 --- a/packages/presets/src/erc20_permit.cairo +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.16.0 (presets/erc20_permit.cairo) - -/// # ERC20Permit Preset -/// -/// The ERC20Permit preset integrates standard ERC20 token functionality with the ERC20Permit -/// extension, as defined by EIP-2612. This preset allows for token approvals via off-chain -/// signatures, thus enhancing transaction efficiency by reducing the need for on-chain approval -/// transactions. -/// -/// This implementation features a fixed-supply model and the initial owner is specified in the -/// constructor. -/// -/// The preset implements SNIP12Metadata with hardcoded values. These values are part of a -/// signature (following SNIP-12 standard) used for ERC20Permit functionality. It's crucial that the -/// SNIP12Metadata name remains unique to avoid confusion and potential security issues. -/// -/// For more complex or custom contracts, use Wizard for Cairo -/// https://wizard.openzeppelin.com/cairo -#[starknet::contract] -pub mod ERC20Permit { - use openzeppelin_token::erc20::extensions::ERC20PermitComponent; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use openzeppelin_utils::cryptography::nonces::NoncesComponent; - use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; - use starknet::ContractAddress; - - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - component!(path: ERC20PermitComponent, storage: erc20_permit, event: ERC20PermitEvent); - component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); - - // ERC20Mixin - #[abi(embed_v0)] - impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; - impl ERC20InternalImpl = ERC20Component::InternalImpl; - - // ERC20Permit - #[abi(embed_v0)] - impl ERC20PermitImpl = ERC20PermitComponent::ERC20PermitImpl; - - // SNIP12Metadata - #[abi(embed_v0)] - impl SNIP12MetadataExternalImpl = - ERC20PermitComponent::SNIP12MetadataExternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc20: ERC20Component::Storage, - #[substorage(v0)] - erc20_permit: ERC20PermitComponent::Storage, - #[substorage(v0)] - nonces: NoncesComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20Event: ERC20Component::Event, - #[flat] - ERC20PermitEvent: ERC20PermitComponent::Event, - #[flat] - NoncesEvent: NoncesComponent::Event - } - - /// Sets the token `name` and `symbol`. - /// Mints `fixed_supply` tokens to `recipient`. - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - fixed_supply: u256, - recipient: ContractAddress - ) { - self.erc20.initializer(name, symbol); - self.erc20.mint(recipient, fixed_supply); - } - - impl SNIP12MetadataImpl of SNIP12Metadata { - /// Returns token name to be used for SNIP-12 signature. - fn name() -> felt252 { - 'My unique token name' - } - - /// Returns token version to be used for SNIP-12 signature. - fn version() -> felt252 { - 'v1' - } - } -} diff --git a/packages/presets/src/lib.cairo b/packages/presets/src/lib.cairo index 1ed3be7a1..5dd344f9d 100644 --- a/packages/presets/src/lib.cairo +++ b/packages/presets/src/lib.cairo @@ -1,7 +1,6 @@ pub mod account; pub mod erc1155; pub mod erc20; -pub mod erc20_permit; pub mod erc721; pub mod eth_account; pub mod interfaces; @@ -13,7 +12,6 @@ pub mod vesting; pub use account::AccountUpgradeable; pub use erc1155::ERC1155Upgradeable; pub use erc20::ERC20Upgradeable; -pub use erc20_permit::ERC20Permit; pub use erc721::ERC721Upgradeable; pub use eth_account::EthAccountUpgradeable; pub use universal_deployer::UniversalDeployer; diff --git a/packages/presets/src/tests.cairo b/packages/presets/src/tests.cairo index fdca78482..68fc25020 100644 --- a/packages/presets/src/tests.cairo +++ b/packages/presets/src/tests.cairo @@ -7,8 +7,6 @@ mod test_erc1155; #[cfg(test)] mod test_erc20; #[cfg(test)] -mod test_erc20_permit; -#[cfg(test)] mod test_erc721; #[cfg(test)] mod test_eth_account; diff --git a/packages/presets/src/tests/test_erc20_permit.cairo b/packages/presets/src/tests/test_erc20_permit.cairo deleted file mode 100644 index 9849a48e6..000000000 --- a/packages/presets/src/tests/test_erc20_permit.cairo +++ /dev/null @@ -1,483 +0,0 @@ -use core::hash::{HashStateTrait, HashStateExTrait}; -use core::poseidon::PoseidonTrait; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants; -use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; -use openzeppelin_token::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; -use openzeppelin_token::erc20::extensions::ERC20PermitComponent::{ - ERC20PermitImpl, SNIP12MetadataExternalImpl -}; -use openzeppelin_token::erc20::extensions::erc20_permit::erc20_permit::Permit; -use openzeppelin_token::erc20::extensions::erc20_permit::interface::{ - ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait -}; -use openzeppelin_utils::cryptography::snip12::{StarknetDomain, StructHashStarknetDomainImpl}; -use openzeppelin_utils::serde::SerializedAppend; -use snforge_std::signature::stark_curve::StarkCurveSignerImpl; -use snforge_std::{ - start_cheat_caller_address, start_cheat_block_timestamp, start_cheat_chain_id_global -}; -use starknet::ContractAddress; - -// -// Test Data -// - -#[derive(Copy, Drop)] -struct TestData { - contract_address: ContractAddress, - owner: ContractAddress, - key_pair: StarkKeyPair, - spender: ContractAddress, - amount: u256, - deadline: u64, - token_supply: u256, - name: @ByteArray, - symbol: @ByteArray, - metadata_name: felt252, - metadata_version: felt252, - chain_id: felt252, - revision: felt252 -} - -fn TEST_DATA() -> TestData { - TestData { - contract_address: constants::CONTRACT_ADDRESS(), - owner: constants::OWNER(), - key_pair: constants::stark::KEY_PAIR(), - spender: constants::SPENDER(), - amount: constants::TOKEN_VALUE, - deadline: constants::TIMESTAMP, - token_supply: constants::SUPPLY, - name: @constants::NAME(), - symbol: @constants::SYMBOL(), - metadata_name: 'My unique token name', // As in ERC20Permit preset - metadata_version: 'v1', // As in ERC20Permit preset - chain_id: constants::CHAIN_ID, - revision: 1 // As in the current SNIP-12 implementation - } -} - -// -// Setup -// - -fn setup(data: TestData) -> ERC20PermitABIDispatcher { - start_cheat_chain_id_global(data.chain_id); - - utils::declare_and_deploy_at( - "DualCaseAccountMock", data.owner, array![data.key_pair.public_key] - ); - - let mut calldata = array![]; - calldata.append_serde(data.name.clone()); - calldata.append_serde(data.symbol.clone()); - calldata.append_serde(data.token_supply); - calldata.append_serde(data.owner); - utils::declare_and_deploy_at("ERC20Permit", data.contract_address, calldata); - - ERC20PermitABIDispatcher { contract_address: data.contract_address } -} - -// -// IERC20Permit -// - -#[test] -fn test_valid_permit_default_data() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - assert_valid_allowance(owner, spender, 0); - - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); - - assert_valid_allowance(owner, spender, amount); - assert_valid_nonce(owner, nonce + 1); -} - -#[test] -fn test_valid_permit_other_data() { - let mut data = TEST_DATA(); - data.spender = constants::OTHER(); - data.amount = constants::TOKEN_VALUE_2; - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - assert_valid_allowance(owner, spender, 0); - - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); - - assert_valid_allowance(owner, spender, amount); - assert_valid_nonce(owner, nonce + 1); -} - -#[test] -fn test_spend_permit() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(data, nonce); - start_cheat_caller_address(dispatcher.contract_address, spender); - - dispatcher.permit(owner, spender, amount, deadline, signature); - dispatcher.transfer_from(owner, spender, amount); - - assert_valid_balance(spender, amount); - assert_valid_balance(owner, data.token_supply - amount); - assert_valid_allowance(owner, spender, 0); - assert_valid_nonce(owner, nonce + 1); -} - -#[test] -fn test_spend_half_permit() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(data, nonce); - start_cheat_caller_address(dispatcher.contract_address, spender); - - dispatcher.permit(owner, spender, amount, deadline, signature); - let transfer_amount = amount / 2; - dispatcher.transfer_from(owner, spender, transfer_amount); - - assert_valid_balance(spender, transfer_amount); - assert_valid_balance(owner, data.token_supply - transfer_amount); - assert_valid_allowance(owner, spender, amount - transfer_amount); - assert_valid_nonce(owner, nonce + 1); -} - -#[test] -fn test_subsequent_permits() { - let mut data = TEST_DATA(); - let (owner, spender, amount_1, deadline) = ( - data.owner, data.spender, data.amount, data.deadline - ); - let dispatcher = setup(data); - - let mut expected_owner_balance = data.token_supply; - let mut expected_spender_balance = 0; - start_cheat_caller_address(dispatcher.contract_address, spender); - - // Permit 1 - let nonce_1 = dispatcher.nonces(owner); - let signature_1 = prepare_permit_signature(data, nonce_1); - - dispatcher.permit(owner, spender, amount_1, deadline, signature_1); - dispatcher.transfer_from(owner, spender, amount_1); - - expected_owner_balance -= amount_1; - expected_spender_balance += amount_1; - assert_valid_balance(owner, expected_owner_balance); - assert_valid_balance(spender, expected_spender_balance); - assert_valid_allowance(owner, spender, 0); - assert_valid_nonce(owner, nonce_1 + 1); - - // Permit 2 - data.amount = constants::TOKEN_VALUE_2; - let amount_2 = data.amount; - let nonce_2 = dispatcher.nonces(owner); - let signature_2 = prepare_permit_signature(data, nonce_2); - - dispatcher.permit(owner, spender, amount_2, deadline, signature_2); - dispatcher.transfer_from(owner, spender, amount_2); - - expected_owner_balance -= amount_2; - expected_spender_balance += amount_2; - assert_valid_balance(owner, expected_owner_balance); - assert_valid_balance(spender, expected_spender_balance); - assert_valid_allowance(owner, spender, 0); - assert_valid_nonce(owner, nonce_2 + 1); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_replay_attack() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let nonce = dispatcher.nonces(owner); - start_cheat_caller_address(dispatcher.contract_address, spender); - - // 1st call is fine - let signature = prepare_permit_signature(data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); - - // 2nd call must fail (nonce already used) - let signature = prepare_permit_signature(data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -fn test_domain_separator() { - let data = TEST_DATA(); - let dispatcher = setup(data); - - let sn_domain = StarknetDomain { - name: data.metadata_name, - version: data.metadata_version, - chain_id: data.chain_id, - revision: data.revision - }; - let expected_domain_separator = sn_domain.hash_struct(); - assert_eq!(dispatcher.DOMAIN_SEPARATOR(), expected_domain_separator); -} - -// -// SNIP12Metadata -// - -#[test] -fn test_snip12_metadata() { - let data = TEST_DATA(); - let dispatcher = setup(data); - - let (metadata_name, metadata_version) = dispatcher.snip12_metadata(); - assert_eq!(metadata_name, data.metadata_name, "Invalid metadata name"); - assert_eq!(metadata_version, data.metadata_version, "Invalid metadata version"); -} - -// -// Invalid signature -// - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_owner() { - let data = TEST_DATA(); - let (spender, amount, deadline) = (data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let another_account = constants::OTHER(); - utils::deploy_another_at(data.owner, another_account, array![data.key_pair.public_key]); - let nonce = dispatcher.nonces(another_account); - let signature = prepare_permit_signature(data, nonce); - dispatcher.permit(another_account, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_token_address() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); - - let mut modified_data = data; - modified_data.contract_address = constants::OTHER(); - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(modified_data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_spender() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let mut modified_data = data; - modified_data.spender = constants::OTHER(); - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(modified_data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_amount() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let mut modified_data = data; - modified_data.amount = constants::TOKEN_VALUE_2; - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(modified_data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_nonce() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let another_nonce = dispatcher.nonces(owner) + 1; - let signature = prepare_permit_signature(data, another_nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_sig_r() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(data, nonce); - let (sig_r, sig_s) = (*signature.at(0), *signature.at(1)); - let modified_signature = array![sig_r + 1, sig_s]; - dispatcher.permit(owner, spender, amount, deadline, modified_signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_sig_s() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(data, nonce); - let (sig_r, sig_s) = (*signature.at(0), *signature.at(1)); - let modified_signature = array![sig_r, sig_s + 1]; - dispatcher.permit(owner, spender, amount, deadline, modified_signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_metadata_name() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let mut modified_data = data; - modified_data.metadata_name = 'ANOTHER_NAME'; - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(modified_data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_metadata_version() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let mut modified_data = data; - modified_data.metadata_version = 'ANOTHER_VERSION'; - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(modified_data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_signing_key() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let mut modified_data = data; - modified_data.key_pair = constants::stark::KEY_PAIR_2(); - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(modified_data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_chain_id() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let mut modified_data = data; - modified_data.chain_id = 'ANOTHER_CHAIN_ID'; - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(modified_data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -#[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] -fn test_invalid_sig_bad_revision() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let mut modified_data = data; - modified_data.revision = 'ANOTHER_REVISION'; - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(modified_data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -// -// Expired signature -// - -#[test] -#[should_panic(expected: ('ERC20Permit: expired signature',))] -fn test_invalid_sig_bad_deadline() { - let data = TEST_DATA(); - let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); - let dispatcher = setup(data); - - let timestamp_after_deadline = deadline + 1; - start_cheat_block_timestamp(dispatcher.contract_address, timestamp_after_deadline); - let nonce = dispatcher.nonces(owner); - let signature = prepare_permit_signature(data, nonce); - dispatcher.permit(owner, spender, amount, deadline, signature); -} - -// -// Helpers -// - -fn prepare_permit_signature(data: TestData, nonce: felt252) -> Array { - let sn_domain = StarknetDomain { - name: data.metadata_name, - version: data.metadata_version, - chain_id: data.chain_id, - revision: data.revision - }; - let permit = Permit { - token: data.contract_address, - spender: data.spender, - amount: data.amount, - nonce, - deadline: data.deadline - }; - let msg_hash = PoseidonTrait::new() - .update_with('StarkNet Message') - .update_with(sn_domain.hash_struct()) - .update_with(data.owner) - .update_with(permit.hash_struct()) - .finalize(); - - data.key_pair.serialized_sign(msg_hash) -} - -fn assert_valid_nonce(account: ContractAddress, expected: felt252) { - let dispatcher = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; - assert_eq!(dispatcher.nonces(account), expected); -} - -fn assert_valid_allowance(owner: ContractAddress, spender: ContractAddress, expected: u256) { - let dispatcher = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; - assert_eq!(dispatcher.allowance(owner, spender), expected); -} - -fn assert_valid_balance(account: ContractAddress, expected: u256) { - let dispatcher = ERC20PermitABIDispatcher { contract_address: constants::CONTRACT_ADDRESS() }; - assert_eq!(dispatcher.balance_of(account), expected); -} From 09fb2d26a30de0aaaae494fea8deddf7b6596dd0 Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 25 Sep 2024 16:10:47 +0200 Subject: [PATCH 08/17] Add ERC20Permit as a trait of ERC20Component --- packages/token/src/erc20/erc20.cairo | 117 +++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 6 deletions(-) diff --git a/packages/token/src/erc20/erc20.cairo b/packages/token/src/erc20/erc20.cairo index baf76b0a5..7ea18e049 100644 --- a/packages/token/src/erc20/erc20.cairo +++ b/packages/token/src/erc20/erc20.cairo @@ -14,13 +14,19 @@ pub mod ERC20Component { use core::num::traits::Bounded; use core::num::traits::Zero; + use crate::erc20::extensions::erc20_permit::Permit; use crate::erc20::interface; - use starknet::ContractAddress; - use starknet::get_caller_address; - use starknet::storage::{ - Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, - StoragePointerWriteAccess + use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; + use openzeppelin_utils::cryptography::interface::{INonces, ISNIP12Metadata}; + use openzeppelin_utils::cryptography::snip12::{ + StructHash, OffchainMessageHash, SNIP12Metadata, StarknetDomain }; + use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; + use openzeppelin_utils::nonces::NoncesComponent; + use starknet::ContractAddress; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{get_block_timestamp, get_caller_address, get_contract_address, get_tx_info}; #[storage] pub struct Storage { @@ -68,6 +74,8 @@ pub mod ERC20Component { pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; pub const INSUFFICIENT_BALANCE: felt252 = 'ERC20: insufficient balance'; pub const INSUFFICIENT_ALLOWANCE: felt252 = 'ERC20: insufficient allowance'; + pub const EXPIRED_PERMIT_SIGNATURE: felt252 = 'ERC20: expired permit signature'; + pub const INVALID_PERMIT_SIGNATURE: felt252 = 'ERC20: invalid permit signature'; } // @@ -288,6 +296,104 @@ pub mod ERC20Component { } } + /// The ERC20Permit trait implements the EIP-2612 standard, facilitating token approvals via + /// off-chain signatures. This approach allows token holders to delegate their approval to spend + /// tokens without executing an on-chain transaction, reducing gas costs and enhancing + /// usability. + /// The message signed and the signature must follow the SNIP-12 standard for hashing and + /// signing typed structured data. + /// + /// To safeguard against replay attacks and ensure the uniqueness of each approval via `permit`, + /// the data signed includes: + /// - The address of the owner + /// - The parameters specified in the `approve` function (spender and amount) + /// - The address of the token contract itself + /// - A nonce, which must be unique for each operation, incrementing after each use to prevent + /// reuse of the signature - The chain ID, which protects against cross-chain replay attacks + /// + /// EIP-2612: https://eips.ethereum.org/EIPS/eip-2612 + /// SNIP-12: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md + #[embeddable_as(ERC20PermitImpl)] + impl ERC20Permit< + TContractState, + impl ERC20: HasComponent, + +ERC20HooksTrait, + impl Nonces: NoncesComponent::HasComponent, + impl Metadata: SNIP12Metadata, + +Drop + > of interface::IERC20Permit> { + /// Sets the allowance of the `spender` over `owner`'s tokens after validating the signature + /// generated off-chain and signed by the `owner`. + /// + /// Requirements: + /// + /// - `owner` is a deployed account contract. + /// - `spender` is not the zero address. + /// - `deadline` is a timestamp in the future. + /// - `signature` is a valid signature that can be validated with a call to `owner` account. + /// - `signature` must use the current nonce of the `owner`. + /// + /// Emits an `Approval` event. + fn permit( + ref self: ComponentState, + owner: ContractAddress, + spender: ContractAddress, + amount: u256, + deadline: u64, + signature: Array + ) { + // 1. Ensure the deadline is not missed + assert(get_block_timestamp() <= deadline, Errors::EXPIRED_PERMIT_SIGNATURE); + + // 2. Get the current nonce and increment it + let mut nonces_component = get_dep_component_mut!(ref self, Nonces); + let nonce = nonces_component.use_nonce(owner); + + // 3. Make a call to the account to validate permit signature + let permit = Permit { token: get_contract_address(), spender, amount, nonce, deadline }; + let permit_hash = permit.get_message_hash(owner); + let is_valid_sig_felt = ISRC6Dispatcher { contract_address: owner } + .is_valid_signature(permit_hash, signature); + + // 4. Check the response is either 'VALID' or True (for backwards compatibility) + let is_valid_sig = is_valid_sig_felt == starknet::VALIDATED || is_valid_sig_felt == 1; + assert(is_valid_sig, Errors::INVALID_PERMIT_SIGNATURE); + + // 5. Approve + let mut erc20_component = get_dep_component_mut!(ref self, ERC20); + erc20_component._approve(owner, spender, amount); + } + + /// Returns the current nonce of the `owner`. A nonce value must be + /// included whenever a signature for `permit` is generated. + fn nonces(self: @ComponentState, owner: ContractAddress) -> felt252 { + let nonces_component = get_dep_component!(self, Nonces); + nonces_component.nonces(owner) + } + + /// Returns the domain separator used in generating a message hash for `permit` signature. + /// The domain hashing logic follows SNIP-12 standard. + fn DOMAIN_SEPARATOR(self: @ComponentState) -> felt252 { + let domain = StarknetDomain { + name: Metadata::name(), + version: Metadata::version(), + chain_id: get_tx_info().unbox().chain_id, + revision: 1 + }; + domain.hash_struct() + } + } + + #[embeddable_as(SNIP12MetadataExternalImpl)] + impl SNIP12MetadataExternal< + TContractState, +HasComponent, impl Metadata: SNIP12Metadata + > of ISNIP12Metadata> { + /// Returns domain name and version used for generating a message hash for permit signature. + fn snip12_metadata(self: @ComponentState) -> (felt252, felt252) { + (Metadata::name(), Metadata::version()) + } + } + // // Internal // @@ -333,7 +439,6 @@ pub mod ERC20Component { self.update(account, Zero::zero(), amount); } - /// Transfers an `amount` of tokens from `from` to `to`, or alternatively mints (or burns) /// if `from` (or `to`) is the zero address. /// From 950205e7314a6b00f4c21efbe801629e274fa006 Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 25 Sep 2024 16:11:43 +0200 Subject: [PATCH 09/17] Remove ERC20Permit component, restructure files --- packages/token/src/erc20/extensions.cairo | 1 - .../src/erc20/extensions/erc20_permit.cairo | 37 +++- .../erc20_permit/erc20_permit.cairo | 169 ------------------ .../extensions/erc20_permit/interface.cairo | 58 ------ packages/token/src/erc20/interface.cairo | 54 ++++++ 5 files changed, 89 insertions(+), 230 deletions(-) delete mode 100644 packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo delete mode 100644 packages/token/src/erc20/extensions/erc20_permit/interface.cairo diff --git a/packages/token/src/erc20/extensions.cairo b/packages/token/src/erc20/extensions.cairo index 034c2004b..8c753aace 100644 --- a/packages/token/src/erc20/extensions.cairo +++ b/packages/token/src/erc20/extensions.cairo @@ -1,5 +1,4 @@ pub mod erc20_permit; pub mod erc20_votes; -pub use erc20_permit::erc20_permit::ERC20PermitComponent; pub use erc20_votes::ERC20VotesComponent; diff --git a/packages/token/src/erc20/extensions/erc20_permit.cairo b/packages/token/src/erc20/extensions/erc20_permit.cairo index f1d24346d..fd07ec8d2 100644 --- a/packages/token/src/erc20/extensions/erc20_permit.cairo +++ b/packages/token/src/erc20/extensions/erc20_permit.cairo @@ -1,2 +1,35 @@ -pub mod erc20_permit; -pub mod interface; +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc20_permit.cairo) + +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::poseidon::PoseidonTrait; +use openzeppelin_utils::cryptography::snip12::StructHash; +use starknet::ContractAddress; + +#[derive(Copy, Drop, Hash)] +pub struct Permit { + pub token: ContractAddress, + pub spender: ContractAddress, + pub amount: u256, + pub nonce: felt252, + pub deadline: u64, +} + +// Since there's no u64 type in SNIP-12, the type used for `deadline` parameter is u128 +// selector!( +// "\"Permit\"( +// \"token\":\"ContractAddress\", +// \"spender\":\"ContractAddress\", +// \"amount\":\"u256\", +// \"nonce\":\"felt\", +// \"deadline\":\"u128\" +// )" +// ); +pub const PERMIT_TYPE_HASH: felt252 = + 0x2a8eb238e7cde741a544afcc79fe945d4292b089875fd068633854927fd5a96; + +impl StructHashImpl of StructHash { + fn hash_struct(self: @Permit) -> felt252 { + PoseidonTrait::new().update_with(PERMIT_TYPE_HASH).update_with(*self).finalize() + } +} diff --git a/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo b/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo deleted file mode 100644 index 05a831e3d..000000000 --- a/packages/token/src/erc20/extensions/erc20_permit/erc20_permit.cairo +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.16.0 (token/erc20/extensions/erc20_permit/erc20_permit.cairo) - -use core::hash::{HashStateTrait, HashStateExTrait}; -use core::poseidon::PoseidonTrait; -use openzeppelin_utils::cryptography::snip12::StructHash; -use starknet::ContractAddress; - -/// # ERC20Permit Component -/// -/// The ERC20Permit component implements the EIP-2612 standard, facilitating token approvals via -/// off-chain signatures. This approach allows token holders to delegate their approval to spend -/// tokens without executing an on-chain transaction, reducing gas costs and enhancing usability. -/// The message signed and the signature must follow the SNIP-12 standard for hashing and signing -/// typed structured data. -/// -/// To safeguard against replay attacks and ensure the uniqueness of each approval via `permit`, the -/// data signed includes: -/// - The address of the owner -/// - The parameters specified in the `approve` function (spender and amount) -/// - The address of the token contract itself -/// - A nonce, which must be unique for each operation, incrementing after each use to prevent -/// reuse of the signature - The chain ID, which protects against cross-chain replay attacks -/// -/// EIP-2612: https://eips.ethereum.org/EIPS/eip-2612 -/// SNIP-12: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md -#[starknet::component] -pub mod ERC20PermitComponent { - use crate::erc20::ERC20Component::InternalTrait; - use crate::erc20::ERC20Component; - use crate::erc20::extensions::erc20_permit::interface::IERC20Permit; - use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; - use openzeppelin_utils::cryptography::interface::{INonces, ISNIP12Metadata}; - use openzeppelin_utils::cryptography::snip12::{ - StructHash, OffchainMessageHash, SNIP12Metadata, StarknetDomain - }; - use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; - use openzeppelin_utils::nonces::NoncesComponent; - use starknet::{ContractAddress, get_block_timestamp, get_contract_address, get_tx_info}; - - #[storage] - struct Storage {} - - pub mod Errors { - pub const EXPIRED_SIGNATURE: felt252 = 'ERC20Permit: expired signature'; - pub const INVALID_SIGNATURE: felt252 = 'ERC20Permit: invalid signature'; - } - - // - // External - // - - #[embeddable_as(ERC20PermitImpl)] - impl ERC20Permit< - TContractState, - +HasComponent, - impl ERC20: ERC20Component::HasComponent, - +ERC20Component::ERC20HooksTrait, - impl Nonces: NoncesComponent::HasComponent, - impl Metadata: SNIP12Metadata, - +Drop - > of IERC20Permit> { - /// Sets the allowance of the `spender` over `owner`'s tokens after validating the signature - /// generated off-chain and signed by the `owner`. - /// - /// Requirements: - /// - /// - `owner` is a deployed account contract. - /// - `spender` is not the zero address. - /// - `deadline` is a timestamp in the future. - /// - `signature` is a valid signature that can be validated with a call to `owner` account. - /// - `signature` must use the current nonce of the `owner`. - /// - /// Emits an `Approval` event. - fn permit( - ref self: ComponentState, - owner: ContractAddress, - spender: ContractAddress, - amount: u256, - deadline: u64, - signature: Array - ) { - assert(get_block_timestamp() <= deadline, Errors::EXPIRED_SIGNATURE); - - // Get current nonce and increment it - let mut nonces_component = get_dep_component_mut!(ref self, Nonces); - let nonce = nonces_component.use_nonce(owner); - - // Compute hash for permit - let permit = super::Permit { - token: get_contract_address(), spender, amount, nonce, deadline - }; - let permit_hash = permit.get_message_hash(owner); - - // Make a call to the account to validate permit signature - let is_valid_sig_felt = DualCaseAccount { contract_address: owner } - .is_valid_signature(permit_hash, signature); - - // Check the response is either 'VALID' or True (for backwards compatibility) - let is_valid_sig = is_valid_sig_felt == starknet::VALIDATED || is_valid_sig_felt == 1; - assert(is_valid_sig, Errors::INVALID_SIGNATURE); - - // Approve - let mut erc20_component = get_dep_component_mut!(ref self, ERC20); - erc20_component._approve(owner, spender, amount); - } - - /// Returns the current nonce of the `owner`. A nonce value must be - /// included whenever a signature for `permit` is generated. - fn nonces(self: @ComponentState, owner: ContractAddress) -> felt252 { - let nonces_component = get_dep_component!(self, Nonces); - nonces_component.nonces(owner) - } - - /// Returns the domain separator used in generating a message hash for `permit` signature. - /// The domain hashing logic follows SNIP-12 standard. - fn DOMAIN_SEPARATOR(self: @ComponentState) -> felt252 { - let domain = StarknetDomain { - name: Metadata::name(), - version: Metadata::version(), - chain_id: get_tx_info().unbox().chain_id, - revision: 1 - }; - domain.hash_struct() - } - } - - #[embeddable_as(SNIP12MetadataExternalImpl)] - impl SNIP12MetadataExternal< - TContractState, +HasComponent, impl Metadata: SNIP12Metadata - > of ISNIP12Metadata> { - /// Returns domain name and version used for generating a message hash for permit signature. - fn snip12_metadata(self: @ComponentState) -> (felt252, felt252) { - (Metadata::name(), Metadata::version()) - } - } -} - -// -// Hash helpers -// - -#[derive(Copy, Drop, Hash)] -pub struct Permit { - pub token: ContractAddress, - pub spender: ContractAddress, - pub amount: u256, - pub nonce: felt252, - pub deadline: u64, -} - -// Since there's no u64 type in SNIP-12, the type used for `deadline` parameter is u128 -// selector!( -// "\"Permit\"( -// \"token\":\"ContractAddress\", -// \"spender\":\"ContractAddress\", -// \"amount\":\"u256\", -// \"nonce\":\"felt\", -// \"deadline\":\"u128\" -// )" -// ); -pub const PERMIT_TYPE_HASH: felt252 = - 0x2a8eb238e7cde741a544afcc79fe945d4292b089875fd068633854927fd5a96; - -impl StructHashImpl of StructHash { - fn hash_struct(self: @Permit) -> felt252 { - PoseidonTrait::new().update_with(PERMIT_TYPE_HASH).update_with(*self).finalize() - } -} diff --git a/packages/token/src/erc20/extensions/erc20_permit/interface.cairo b/packages/token/src/erc20/extensions/erc20_permit/interface.cairo deleted file mode 100644 index 72b7e0a6a..000000000 --- a/packages/token/src/erc20/extensions/erc20_permit/interface.cairo +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.16.0 (token/erc20/extensions/erc20_permit/interface.cairo) - -use starknet::ContractAddress; - -#[starknet::interface] -pub trait IERC20Permit { - fn permit( - ref self: TState, - owner: ContractAddress, - spender: ContractAddress, - amount: u256, - deadline: u64, - signature: Array - ); - fn nonces(self: @TState, owner: ContractAddress) -> felt252; - fn DOMAIN_SEPARATOR(self: @TState) -> felt252; -} - -#[starknet::interface] -pub trait ERC20PermitABI { - // IERC20 - fn total_supply(self: @TState) -> u256; - fn balance_of(self: @TState, account: ContractAddress) -> u256; - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; - fn transfer_from( - ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; - - // IERC20CamelOnly - fn totalSupply(self: @TState) -> u256; - fn balanceOf(self: @TState, account: ContractAddress) -> u256; - fn transferFrom( - ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - - // IERC20Metadata - fn name(self: @TState) -> ByteArray; - fn symbol(self: @TState) -> ByteArray; - fn decimals(self: @TState) -> u8; - - // IERC20Permit - fn permit( - ref self: TState, - owner: ContractAddress, - spender: ContractAddress, - amount: u256, - deadline: u64, - signature: Array - ); - fn nonces(self: @TState, owner: ContractAddress) -> felt252; - fn DOMAIN_SEPARATOR(self: @TState) -> felt252; - - // ISNIP12Metadata - fn snip12_metadata(self: @TState) -> (felt252, felt252); -} diff --git a/packages/token/src/erc20/interface.cairo b/packages/token/src/erc20/interface.cairo index 811eb62fc..69b551d19 100644 --- a/packages/token/src/erc20/interface.cairo +++ b/packages/token/src/erc20/interface.cairo @@ -43,6 +43,20 @@ pub trait IERC20CamelOnly { ) -> bool; } +#[starknet::interface] +pub trait IERC20Permit { + fn permit( + ref self: TState, + owner: ContractAddress, + spender: ContractAddress, + amount: u256, + deadline: u64, + signature: Array + ); + fn nonces(self: @TState, owner: ContractAddress) -> felt252; + fn DOMAIN_SEPARATOR(self: @TState) -> felt252; +} + #[starknet::interface] pub trait ERC20ABI { // IERC20 @@ -107,3 +121,43 @@ pub trait ERC20VotesABI { ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 ) -> bool; } + +#[starknet::interface] +pub trait ERC20PermitABI { + // IERC20 + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + + // IERC20CamelOnly + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + + // IERC20Metadata + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; + + // IERC20Permit + fn permit( + ref self: TState, + owner: ContractAddress, + spender: ContractAddress, + amount: u256, + deadline: u64, + signature: Array + ); + fn nonces(self: @TState, owner: ContractAddress) -> felt252; + fn DOMAIN_SEPARATOR(self: @TState) -> felt252; + + // ISNIP12Metadata + fn snip12_metadata(self: @TState) -> (felt252, felt252); +} From 1c2fc823ae7a495513e1486233bc70b7d5072f1e Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 25 Sep 2024 16:11:54 +0200 Subject: [PATCH 10/17] Update tests --- .../src/tests/erc20/test_erc20_permit.cairo | 7 ++----- .../src/tests/mocks/erc20_permit_mocks.cairo | 17 +++++------------ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/token/src/tests/erc20/test_erc20_permit.cairo b/packages/token/src/tests/erc20/test_erc20_permit.cairo index 06f6d8197..c3b5361e9 100644 --- a/packages/token/src/tests/erc20/test_erc20_permit.cairo +++ b/packages/token/src/tests/erc20/test_erc20_permit.cairo @@ -1,11 +1,8 @@ use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; use crate::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; -use crate::erc20::extensions::ERC20PermitComponent::{ERC20PermitImpl, SNIP12MetadataExternalImpl}; -use crate::erc20::extensions::erc20_permit::erc20_permit::{Permit, PERMIT_TYPE_HASH}; -use crate::erc20::extensions::erc20_permit::interface::{ - ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait -}; +use crate::erc20::extensions::erc20_permit::{Permit, PERMIT_TYPE_HASH}; +use crate::erc20::interface::{ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait}; use openzeppelin_testing as utils; use openzeppelin_testing::constants; use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; diff --git a/packages/token/src/tests/mocks/erc20_permit_mocks.cairo b/packages/token/src/tests/mocks/erc20_permit_mocks.cairo index ae4d5d9ed..8c3b0e48b 100644 --- a/packages/token/src/tests/mocks/erc20_permit_mocks.cairo +++ b/packages/token/src/tests/mocks/erc20_permit_mocks.cairo @@ -1,13 +1,11 @@ #[starknet::contract] pub(crate) mod DualCaseERC20PermitMock { - use crate::erc20::extensions::ERC20PermitComponent; use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); - component!(path: ERC20PermitComponent, storage: erc20_permit, event: ERC20PermitEvent); component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); // ERC20Mixin @@ -15,23 +13,20 @@ pub(crate) mod DualCaseERC20PermitMock { impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; impl InternalImpl = ERC20Component::InternalImpl; - // ERC20Permit + // IERC20Permit #[abi(embed_v0)] - impl ERC20PermitComponentImpl = - ERC20PermitComponent::ERC20PermitImpl; + impl ERC20PermitImpl = ERC20Component::ERC20PermitImpl; - // SNIP12Metadata + // ISNIP12Metadata #[abi(embed_v0)] - impl SNIP12MetadataExternalImpl = - ERC20PermitComponent::SNIP12MetadataExternalImpl; + impl SNIP12MetadataExternal = + ERC20Component::SNIP12MetadataExternalImpl; #[storage] struct Storage { #[substorage(v0)] erc20: ERC20Component::Storage, #[substorage(v0)] - erc20_permit: ERC20PermitComponent::Storage, - #[substorage(v0)] nonces: NoncesComponent::Storage } @@ -41,8 +36,6 @@ pub(crate) mod DualCaseERC20PermitMock { #[flat] ERC20Event: ERC20Component::Event, #[flat] - ERC20PermitEvent: ERC20PermitComponent::Event, - #[flat] NoncesEvent: NoncesComponent::Event } From 5702d0c0b7fc34fb5bbab81bd3ec7360a0da9ede Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 25 Sep 2024 17:13:36 +0200 Subject: [PATCH 11/17] Fix test error messages --- CHANGELOG.md | 2 +- .../src/tests/erc20/test_erc20_permit.cairo | 37 ++++++++++--------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 416687226..51b719942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- ERC20Permit component and preset +- ERC20Permit component and preset (#1140) ## 0.17.0 (2024-09-23) diff --git a/packages/token/src/tests/erc20/test_erc20_permit.cairo b/packages/token/src/tests/erc20/test_erc20_permit.cairo index c3b5361e9..d4fff5d4e 100644 --- a/packages/token/src/tests/erc20/test_erc20_permit.cairo +++ b/packages/token/src/tests/erc20/test_erc20_permit.cairo @@ -3,6 +3,7 @@ use core::poseidon::PoseidonTrait; use crate::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; use crate::erc20::extensions::erc20_permit::{Permit, PERMIT_TYPE_HASH}; use crate::erc20::interface::{ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait}; +use crate::tests::mocks::erc20_permit_mocks::DualCaseERC20PermitMock; use openzeppelin_testing as utils; use openzeppelin_testing::constants; use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; @@ -46,8 +47,8 @@ fn TEST_DATA() -> TestData { token_supply: constants::SUPPLY, name: @constants::NAME(), symbol: @constants::SYMBOL(), - metadata_name: constants::DAPP_NAME, // As in DualCaseERC20PermitMock - metadata_version: constants::DAPP_VERSION, // As in DualCaseERC20PermitMock + metadata_name: DualCaseERC20PermitMock::SNIP12MetadataImpl::name(), + metadata_version: DualCaseERC20PermitMock::SNIP12MetadataImpl::version(), chain_id: constants::CHAIN_ID, revision: 1 // As in the current SNIP-12 implementation } @@ -195,7 +196,7 @@ fn test_subsequent_permits() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_replay_attack() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -246,8 +247,8 @@ fn test_snip12_metadata() { let mock = setup(data); let (metadata_name, metadata_version) = mock.snip12_metadata(); - assert_eq!(metadata_name, data.metadata_name, "Invalid metadata name"); - assert_eq!(metadata_version, data.metadata_version, "Invalid metadata version"); + assert_eq!(metadata_name, data.metadata_name); + assert_eq!(metadata_version, data.metadata_version); } // @@ -255,7 +256,7 @@ fn test_snip12_metadata() { // #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_owner() { let data = TEST_DATA(); let (spender, amount, deadline) = (data.spender, data.amount, data.deadline); @@ -269,7 +270,7 @@ fn test_invalid_sig_bad_owner() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_token_address() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -287,7 +288,7 @@ fn test_invalid_sig_bad_token_address() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_spender() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -301,7 +302,7 @@ fn test_invalid_sig_bad_spender() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_amount() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -315,7 +316,7 @@ fn test_invalid_sig_bad_amount() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_nonce() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -327,7 +328,7 @@ fn test_invalid_sig_bad_nonce() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_sig_r() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -341,7 +342,7 @@ fn test_invalid_sig_bad_sig_r() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_sig_s() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -355,7 +356,7 @@ fn test_invalid_sig_bad_sig_s() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_metadata_name() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -369,7 +370,7 @@ fn test_invalid_sig_bad_metadata_name() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_metadata_version() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -383,7 +384,7 @@ fn test_invalid_sig_bad_metadata_version() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_signing_key() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -397,7 +398,7 @@ fn test_invalid_sig_bad_signing_key() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_chain_id() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -411,7 +412,7 @@ fn test_invalid_sig_bad_chain_id() { } #[test] -#[should_panic(expected: ('ERC20Permit: invalid signature',))] +#[should_panic(expected: 'ERC20: invalid permit signature')] fn test_invalid_sig_bad_revision() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); @@ -429,7 +430,7 @@ fn test_invalid_sig_bad_revision() { // #[test] -#[should_panic(expected: ('ERC20Permit: expired signature',))] +#[should_panic(expected: 'ERC20: expired permit signature')] fn test_invalid_sig_bad_deadline() { let data = TEST_DATA(); let (owner, spender, amount, deadline) = (data.owner, data.spender, data.amount, data.deadline); From 3e48a9aaa51a16708896b3188dca8c606ba389c1 Mon Sep 17 00:00:00 2001 From: immrsd Date: Fri, 27 Sep 2024 18:07:40 +0200 Subject: [PATCH 12/17] Make slight changes to functions doc --- packages/token/src/erc20/erc20.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc20/erc20.cairo b/packages/token/src/erc20/erc20.cairo index 7ea18e049..0b0f4ade5 100644 --- a/packages/token/src/erc20/erc20.cairo +++ b/packages/token/src/erc20/erc20.cairo @@ -329,7 +329,7 @@ pub mod ERC20Component { /// /// - `owner` is a deployed account contract. /// - `spender` is not the zero address. - /// - `deadline` is a timestamp in the future. + /// - `deadline` is not a timestamp in the past. /// - `signature` is a valid signature that can be validated with a call to `owner` account. /// - `signature` must use the current nonce of the `owner`. /// @@ -365,7 +365,7 @@ pub mod ERC20Component { } /// Returns the current nonce of the `owner`. A nonce value must be - /// included whenever a signature for `permit` is generated. + /// included whenever a signature for `permit` call is generated. fn nonces(self: @ComponentState, owner: ContractAddress) -> felt252 { let nonces_component = get_dep_component!(self, Nonces); nonces_component.nonces(owner) From 8fbbe48219111d49bb89adabf6cda54f83e9887c Mon Sep 17 00:00:00 2001 From: immrsd Date: Sat, 5 Oct 2024 20:04:07 +0200 Subject: [PATCH 13/17] Address review issues --- packages/token/src/erc20/erc20.cairo | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/token/src/erc20/erc20.cairo b/packages/token/src/erc20/erc20.cairo index 0b0f4ade5..b95ae8840 100644 --- a/packages/token/src/erc20/erc20.cairo +++ b/packages/token/src/erc20/erc20.cairo @@ -12,8 +12,7 @@ /// for examples. #[starknet::component] pub mod ERC20Component { - use core::num::traits::Bounded; - use core::num::traits::Zero; + use core::num::traits::{Bounded, Zero}; use crate::erc20::extensions::erc20_permit::Permit; use crate::erc20::interface; use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; @@ -296,12 +295,15 @@ pub mod ERC20Component { } } - /// The ERC20Permit trait implements the EIP-2612 standard, facilitating token approvals via + /// The ERC20Permit impl implements the EIP-2612 standard, facilitating token approvals via /// off-chain signatures. This approach allows token holders to delegate their approval to spend /// tokens without executing an on-chain transaction, reducing gas costs and enhancing /// usability. + /// See https://eips.ethereum.org/EIPS/eip-2612. + /// /// The message signed and the signature must follow the SNIP-12 standard for hashing and /// signing typed structured data. + /// See https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md. /// /// To safeguard against replay attacks and ensure the uniqueness of each approval via `permit`, /// the data signed includes: @@ -309,21 +311,19 @@ pub mod ERC20Component { /// - The parameters specified in the `approve` function (spender and amount) /// - The address of the token contract itself /// - A nonce, which must be unique for each operation, incrementing after each use to prevent - /// reuse of the signature - The chain ID, which protects against cross-chain replay attacks - /// - /// EIP-2612: https://eips.ethereum.org/EIPS/eip-2612 - /// SNIP-12: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md + /// reuse of the signature + /// - The chain ID, which protects against cross-chain replay attacks #[embeddable_as(ERC20PermitImpl)] impl ERC20Permit< TContractState, - impl ERC20: HasComponent, + +HasComponent, +ERC20HooksTrait, impl Nonces: NoncesComponent::HasComponent, impl Metadata: SNIP12Metadata, +Drop > of interface::IERC20Permit> { - /// Sets the allowance of the `spender` over `owner`'s tokens after validating the signature - /// generated off-chain and signed by the `owner`. + /// Sets `amount` as the allowance of `spender` over `owner`'s tokens after validating the + /// signature. /// /// Requirements: /// @@ -360,11 +360,10 @@ pub mod ERC20Component { assert(is_valid_sig, Errors::INVALID_PERMIT_SIGNATURE); // 5. Approve - let mut erc20_component = get_dep_component_mut!(ref self, ERC20); - erc20_component._approve(owner, spender, amount); + self._approve(owner, spender, amount); } - /// Returns the current nonce of the `owner`. A nonce value must be + /// Returns the current nonce of `owner`. A nonce value must be /// included whenever a signature for `permit` call is generated. fn nonces(self: @ComponentState, owner: ContractAddress) -> felt252 { let nonces_component = get_dep_component!(self, Nonces); From 26ce488158a78647b11ad55175e88e0a6b0ad99a Mon Sep 17 00:00:00 2001 From: immrsd Date: Sat, 5 Oct 2024 21:06:17 +0200 Subject: [PATCH 14/17] Update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b719942..82b100801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- ERC20Permit component and preset (#1140) +- `ISNIP12Metadata` interface to discover name and version of a SNIP-12 impl (#1140) +- `ERC20Permit` impl facilitating token approvals via off-chain signatures (#1140) +- `SNIP12MetadataExternal` impl for `ERC20Component` exposing external function for reading SNIP-12 metadata (#1140) ## 0.17.0 (2024-09-23) From acad60118bcdcfa0c0cb15c9e524b32297590249 Mon Sep 17 00:00:00 2001 From: immrsd Date: Sat, 5 Oct 2024 21:06:56 +0200 Subject: [PATCH 15/17] Restructure files --- packages/token/src/erc20.cairo | 1 + packages/token/src/erc20/erc20.cairo | 2 +- packages/token/src/erc20/extensions.cairo | 1 - .../src/erc20/extensions/erc20_votes.cairo | 32 ++----------------- packages/token/src/erc20/snip12_utils.cairo | 2 ++ .../permit.cairo} | 2 +- .../token/src/erc20/snip12_utils/votes.cairo | 26 +++++++++++++++ 7 files changed, 33 insertions(+), 33 deletions(-) create mode 100644 packages/token/src/erc20/snip12_utils.cairo rename packages/token/src/erc20/{extensions/erc20_permit.cairo => snip12_utils/permit.cairo} (92%) create mode 100644 packages/token/src/erc20/snip12_utils/votes.cairo diff --git a/packages/token/src/erc20.cairo b/packages/token/src/erc20.cairo index e40fbb2bb..9893fe013 100644 --- a/packages/token/src/erc20.cairo +++ b/packages/token/src/erc20.cairo @@ -2,6 +2,7 @@ pub mod dual20; pub mod erc20; pub mod extensions; pub mod interface; +pub mod snip12_utils; pub use erc20::{ERC20Component, ERC20HooksEmptyImpl}; pub use interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; diff --git a/packages/token/src/erc20/erc20.cairo b/packages/token/src/erc20/erc20.cairo index b95ae8840..488f2653e 100644 --- a/packages/token/src/erc20/erc20.cairo +++ b/packages/token/src/erc20/erc20.cairo @@ -13,8 +13,8 @@ #[starknet::component] pub mod ERC20Component { use core::num::traits::{Bounded, Zero}; - use crate::erc20::extensions::erc20_permit::Permit; use crate::erc20::interface; + use crate::erc20::snip12_utils::permit::Permit; use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; use openzeppelin_utils::cryptography::interface::{INonces, ISNIP12Metadata}; use openzeppelin_utils::cryptography::snip12::{ diff --git a/packages/token/src/erc20/extensions.cairo b/packages/token/src/erc20/extensions.cairo index 8c753aace..e45cdcbf3 100644 --- a/packages/token/src/erc20/extensions.cairo +++ b/packages/token/src/erc20/extensions.cairo @@ -1,4 +1,3 @@ -pub mod erc20_permit; pub mod erc20_votes; pub use erc20_votes::ERC20VotesComponent; diff --git a/packages/token/src/erc20/extensions/erc20_votes.cairo b/packages/token/src/erc20/extensions/erc20_votes.cairo index a41e6b811..84f6217f1 100644 --- a/packages/token/src/erc20/extensions/erc20_votes.cairo +++ b/packages/token/src/erc20/extensions/erc20_votes.cairo @@ -1,11 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc20_votes.cairo) -use core::hash::{HashStateTrait, HashStateExTrait}; -use core::poseidon::PoseidonTrait; -use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, StructHash, SNIP12Metadata}; -use starknet::ContractAddress; - /// # ERC20Votes Component /// /// The ERC20Votes component tracks voting units from ERC20 balances, which are a measure of voting @@ -19,8 +14,10 @@ pub mod ERC20VotesComponent { use core::num::traits::Zero; use crate::erc20::ERC20Component; use crate::erc20::interface::IERC20; + use crate::erc20::snip12_utils::votes::Delegation; use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; use openzeppelin_governance::utils::interfaces::IVotes; + use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; use openzeppelin_utils::nonces::NoncesComponent; use openzeppelin_utils::structs::checkpoint::{Checkpoint, Trace, TraceTrait}; @@ -28,7 +25,6 @@ pub mod ERC20VotesComponent { use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess }; - use super::{Delegation, OffchainMessageHash, SNIP12Metadata}; #[storage] pub struct Storage { @@ -287,27 +283,3 @@ pub mod ERC20VotesComponent { } } } - -// -// Offchain message hash generation helpers. -// - -// sn_keccak("\"Delegation\"(\"delegatee\":\"ContractAddress\",\"nonce\":\"felt\",\"expiry\":\"u128\")") -// -// Since there's no u64 type in SNIP-12, we use u128 for `expiry` in the type hash generation. -pub const DELEGATION_TYPE_HASH: felt252 = - 0x241244ac7acec849adc6df9848262c651eb035a3add56e7f6c7bcda6649e837; - -#[derive(Copy, Drop, Hash)] -pub struct Delegation { - pub delegatee: ContractAddress, - pub nonce: felt252, - pub expiry: u64 -} - -impl StructHashImpl of StructHash { - fn hash_struct(self: @Delegation) -> felt252 { - let hash_state = PoseidonTrait::new(); - hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() - } -} diff --git a/packages/token/src/erc20/snip12_utils.cairo b/packages/token/src/erc20/snip12_utils.cairo new file mode 100644 index 000000000..9b562ed1d --- /dev/null +++ b/packages/token/src/erc20/snip12_utils.cairo @@ -0,0 +1,2 @@ +pub mod permit; +pub mod votes; diff --git a/packages/token/src/erc20/extensions/erc20_permit.cairo b/packages/token/src/erc20/snip12_utils/permit.cairo similarity index 92% rename from packages/token/src/erc20/extensions/erc20_permit.cairo rename to packages/token/src/erc20/snip12_utils/permit.cairo index fd07ec8d2..e39341307 100644 --- a/packages/token/src/erc20/extensions/erc20_permit.cairo +++ b/packages/token/src/erc20/snip12_utils/permit.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc20_permit.cairo) +// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/snip12_utils/permit.cairo) use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; diff --git a/packages/token/src/erc20/snip12_utils/votes.cairo b/packages/token/src/erc20/snip12_utils/votes.cairo new file mode 100644 index 000000000..f611bdaaa --- /dev/null +++ b/packages/token/src/erc20/snip12_utils/votes.cairo @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/snip12_utils/votes.cairo) + +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::poseidon::PoseidonTrait; +use openzeppelin_utils::cryptography::snip12::StructHash; +use starknet::ContractAddress; + +// sn_keccak("\"Delegation\"(\"delegatee\":\"ContractAddress\",\"nonce\":\"felt\",\"expiry\":\"u128\")") +// +// Since there's no u64 type in SNIP-12, we use u128 for `expiry` in the type hash generation. +pub const DELEGATION_TYPE_HASH: felt252 = + 0x241244ac7acec849adc6df9848262c651eb035a3add56e7f6c7bcda6649e837; + +#[derive(Copy, Drop, Hash)] +pub struct Delegation { + pub delegatee: ContractAddress, + pub nonce: felt252, + pub expiry: u64 +} + +impl StructHashImpl of StructHash { + fn hash_struct(self: @Delegation) -> felt252 { + PoseidonTrait::new().update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() + } +} From 2cef6d189e3758c454e7b8a26efca5d3b74eb490 Mon Sep 17 00:00:00 2001 From: immrsd Date: Sat, 5 Oct 2024 21:07:05 +0200 Subject: [PATCH 16/17] Support fixes in tests --- .../src/tests/erc20/test_erc20_permit.cairo | 4 ++-- .../src/tests/erc20/test_erc20_votes.cairo | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/token/src/tests/erc20/test_erc20_permit.cairo b/packages/token/src/tests/erc20/test_erc20_permit.cairo index d4fff5d4e..8cda5273f 100644 --- a/packages/token/src/tests/erc20/test_erc20_permit.cairo +++ b/packages/token/src/tests/erc20/test_erc20_permit.cairo @@ -1,8 +1,8 @@ use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; use crate::erc20::ERC20Component::{ERC20MixinImpl, InternalImpl}; -use crate::erc20::extensions::erc20_permit::{Permit, PERMIT_TYPE_HASH}; use crate::erc20::interface::{ERC20PermitABIDispatcher, ERC20PermitABIDispatcherTrait}; +use crate::erc20::snip12_utils::permit::{Permit, PERMIT_TYPE_HASH}; use crate::tests::mocks::erc20_permit_mocks::DualCaseERC20PermitMock; use openzeppelin_testing as utils; use openzeppelin_testing::constants; @@ -16,7 +16,7 @@ use snforge_std::{ use starknet::ContractAddress; // -// Test Data +// Constants // #[derive(Copy, Drop)] diff --git a/packages/token/src/tests/erc20/test_erc20_votes.cairo b/packages/token/src/tests/erc20/test_erc20_votes.cairo index dbff4693e..2203fc8a8 100644 --- a/packages/token/src/tests/erc20/test_erc20_votes.cairo +++ b/packages/token/src/tests/erc20/test_erc20_votes.cairo @@ -4,7 +4,7 @@ use crate::erc20::ERC20Component::InternalImpl as ERC20Impl; use crate::erc20::extensions::ERC20VotesComponent::{DelegateChanged, DelegateVotesChanged}; use crate::erc20::extensions::ERC20VotesComponent::{ERC20VotesImpl, InternalImpl}; use crate::erc20::extensions::ERC20VotesComponent; -use crate::erc20::extensions::erc20_votes::Delegation; +use crate::erc20::snip12_utils::votes::Delegation; use crate::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock::SNIP12MetadataImpl; use crate::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock; use openzeppelin_testing as utils; @@ -52,7 +52,7 @@ fn setup_account(public_key: felt252) -> ContractAddress { // Checkpoints unordered insertion #[test] -#[should_panic(expected: ('Unordered insertion',))] +#[should_panic(expected: 'Unordered insertion')] fn test__delegate_checkpoints_unordered_insertion() { let mut state = setup(); let mut trace = state.ERC20Votes_delegate_checkpoints.read(OWNER()); @@ -63,7 +63,7 @@ fn test__delegate_checkpoints_unordered_insertion() { } #[test] -#[should_panic(expected: ('Unordered insertion',))] +#[should_panic(expected: 'Unordered insertion')] fn test__total_checkpoints_unordered_insertion() { let mut state = setup(); let mut trace = state.ERC20Votes_total_checkpoints.read(); @@ -111,7 +111,7 @@ fn test_get_past_votes() { } #[test] -#[should_panic(expected: ('Votes: future Lookup',))] +#[should_panic(expected: 'Votes: future Lookup')] fn test_get_past_votes_future_lookup() { let state = setup(); @@ -145,7 +145,7 @@ fn test_get_past_total_supply() { } #[test] -#[should_panic(expected: ('Votes: future Lookup',))] +#[should_panic(expected: 'Votes: future Lookup')] fn test_get_past_total_supply_future_lookup() { let state = setup(); @@ -264,7 +264,7 @@ fn test_delegate_by_sig() { } #[test] -#[should_panic(expected: ('Votes: expired signature',))] +#[should_panic(expected: 'Votes: expired signature')] fn test_delegate_by_sig_past_expiry() { start_cheat_block_timestamp_global('ts5'); @@ -276,7 +276,7 @@ fn test_delegate_by_sig_past_expiry() { } #[test] -#[should_panic(expected: ('Nonces: invalid nonce',))] +#[should_panic(expected: 'Nonces: invalid nonce')] fn test_delegate_by_sig_invalid_nonce() { let mut state = setup(); let signature = array![0, 0]; @@ -285,7 +285,7 @@ fn test_delegate_by_sig_invalid_nonce() { } #[test] -#[should_panic(expected: ('Votes: invalid signature',))] +#[should_panic(expected: 'Votes: invalid signature')] fn test_delegate_by_sig_invalid_signature() { let mut state = setup(); let account = setup_account(0x123); @@ -332,7 +332,7 @@ fn test_checkpoints() { } #[test] -#[should_panic(expected: ('Array overflow',))] +#[should_panic(expected: 'Array overflow')] fn test__checkpoints_array_overflow() { let state = setup(); state.checkpoints(OWNER(), 1); From 72ab63dda29aae06115d63d798095356f7175d47 Mon Sep 17 00:00:00 2001 From: immrsd Date: Sat, 5 Oct 2024 21:23:27 +0200 Subject: [PATCH 17/17] Fix changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c7355e5..3b6faeb2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump scarb to v2.8.3 (#1166) - ## 0.17.0 (2024-09-23) ### Added