Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC20 Permit Component #1140

Merged
merged 29 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a055099
Add ERC20Permit component
immrsd Sep 5, 2024
cfa836a
Add ERC20Permit preset
immrsd Sep 5, 2024
9973d9a
Add ERC20Pemit mock and update visibility modifiers
immrsd Sep 10, 2024
23ba5da
Add tests for ERC20Permit component
immrsd Sep 10, 2024
87f1859
Add tests for ERC20Permit preset
immrsd Sep 10, 2024
67c3fc7
Run linter
immrsd Sep 10, 2024
a499394
Merge main
immrsd Sep 25, 2024
4bfbe36
Remove ERC20Permit preset
immrsd Sep 25, 2024
09fb2d2
Add ERC20Permit as a trait of ERC20Component
immrsd Sep 25, 2024
950205e
Remove ERC20Permit component, restructure files
immrsd Sep 25, 2024
1c2fc82
Update tests
immrsd Sep 25, 2024
5702d0c
Fix test error messages
immrsd Sep 25, 2024
3e48a9a
Make slight changes to functions doc
immrsd Sep 27, 2024
8fbbe48
Address review issues
immrsd Oct 5, 2024
26ce488
Update changelog
immrsd Oct 5, 2024
acad601
Restructure files
immrsd Oct 5, 2024
2cef6d1
Support fixes in tests
immrsd Oct 5, 2024
4d23651
Merge main
immrsd Oct 5, 2024
72ab63d
Fix changelog
immrsd Oct 5, 2024
d90a50d
Merge main and fix conflicts
immrsd Oct 14, 2024
0204425
Change signature type to Span
immrsd Oct 14, 2024
68e45a4
Support sig changes in tests
immrsd Oct 14, 2024
e023847
Update in-code doc
immrsd Oct 14, 2024
78fa616
Refactor tests
immrsd Oct 14, 2024
6776641
Add ERC20Mixin interface, remove ERC20PermitABI interface
immrsd Oct 14, 2024
c44cf42
Update doc
immrsd Oct 14, 2024
979117b
Merge main
immrsd Oct 14, 2024
34a3d72
Remove unnecessary constant
immrsd Oct 14, 2024
a144d66
Make StructHashStarknetDomainImpl non-public back
immrsd Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `IUpgradeAndCall` interface (#1148)
- `upgrade_and_call` function in UpgradeableComponent's InternalImpl (#1148)
- `ERC20Permit` impl for `ERC20Component` facilitating token approvals via off-chain signatures (#1140)
- `ISNIP12Metadata` interface for discovering name and version of a SNIP-12 impl (#1140)
- `SNIP12MetadataExternal` impl of `ISNIP12Metadata` interface for `ERC20Component` (#1140)

### Changed

Expand Down
66 changes: 66 additions & 0 deletions packages/test_common/src/mocks/erc20.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,69 @@ pub mod DualCaseERC20VotesMock {
self.erc20.mint(recipient, fixed_supply);
}
}

#[starknet::contract]
pub mod DualCaseERC20PermitMock {
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: NoncesComponent, storage: nonces, event: NoncesEvent);

// ERC20Mixin
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
impl InternalImpl = ERC20Component::InternalImpl<ContractState>;

// IERC20Permit
#[abi(embed_v0)]
impl ERC20PermitImpl = ERC20Component::ERC20PermitImpl<ContractState>;

// ISNIP12Metadata
#[abi(embed_v0)]
impl SNIP12MetadataExternal =
ERC20Component::SNIP12MetadataExternalImpl<ContractState>;

#[storage]
pub struct Storage {
#[substorage(v0)]
pub erc20: ERC20Component::Storage,
#[substorage(v0)]
pub nonces: NoncesComponent::Storage
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC20Event: ERC20Component::Event,
#[flat]
NoncesEvent: NoncesComponent::Event
}

/// Required for hash computation.
pub 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);
}
}
2 changes: 2 additions & 0 deletions packages/testing/src/constants.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/token/src/erc20.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod erc20;
pub mod extensions;
pub mod interface;
pub mod snip12_utils;

pub use erc20::{ERC20Component, ERC20HooksEmptyImpl};
pub use interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait};
122 changes: 113 additions & 9 deletions packages/token/src/erc20/erc20.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@
/// 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::interface;
use starknet::ContractAddress;
use starknet::get_caller_address;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess
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::{
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 {
Expand Down Expand Up @@ -68,6 +73,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';
}

//
Expand Down Expand Up @@ -219,7 +226,7 @@ pub mod ERC20Component {
#[embeddable_as(ERC20MixinImpl)]
impl ERC20Mixin<
TContractState, +HasComponent<TContractState>, +ERC20HooksTrait<TContractState>
> of interface::ERC20ABI<ComponentState<TContractState>> {
> of interface::IERC20Mixin<ComponentState<TContractState>> {
// IERC20
fn total_supply(self: @ComponentState<TContractState>) -> u256 {
ERC20::total_supply(self)
Expand Down Expand Up @@ -288,6 +295,104 @@ pub mod ERC20Component {
}
}

/// 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:
/// - 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.
#[embeddable_as(ERC20PermitImpl)]
impl ERC20Permit<
TContractState,
+HasComponent<TContractState>,
+ERC20HooksTrait<TContractState>,
impl Nonces: NoncesComponent::HasComponent<TContractState>,
impl Metadata: SNIP12Metadata,
+Drop<TContractState>
> of interface::IERC20Permit<ComponentState<TContractState>> {
/// Sets `amount` as the allowance of `spender` over `owner`'s tokens after validating the
/// signature.
///
/// Requirements:
///
/// - `owner` is a deployed account contract.
/// - `spender` is not the zero address.
/// - `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`.
///
/// Emits an `Approval` event.
fn permit(
ref self: ComponentState<TContractState>,
owner: ContractAddress,
spender: ContractAddress,
amount: u256,
deadline: u64,
signature: Span<felt252>
) {
// 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.into());

// 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
self._approve(owner, spender, amount);
}

/// Returns the current nonce of `owner`. A nonce value must be
/// included whenever a signature for `permit` call is generated.
fn nonces(self: @ComponentState<TContractState>, 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<TContractState>) -> 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<TContractState>, impl Metadata: SNIP12Metadata
> of ISNIP12Metadata<ComponentState<TContractState>> {
/// Returns domain name and version used for generating a message hash for permit signature.
fn snip12_metadata(self: @ComponentState<TContractState>) -> (felt252, felt252) {
(Metadata::name(), Metadata::version())
}
}

//
// Internal
//
Expand Down Expand Up @@ -333,7 +438,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.
///
Expand Down
32 changes: 2 additions & 30 deletions packages/token/src/erc20/extensions/erc20_votes.cairo
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,16 +14,17 @@ 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::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait};
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};
use starknet::ContractAddress;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess
};
use super::{Delegation, OffchainMessageHash, SNIP12Metadata};

#[storage]
pub struct Storage {
Expand Down Expand Up @@ -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<Delegation> {
fn hash_struct(self: @Delegation) -> felt252 {
let hash_state = PoseidonTrait::new();
hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize()
}
}
54 changes: 54 additions & 0 deletions packages/token/src/erc20/interface.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,45 @@ pub trait IERC20CamelOnly<TState> {
) -> bool;
}

#[starknet::interface]
pub trait IERC20Mixin<TState> {
// 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;

// IERC20Metadata
fn name(self: @TState) -> ByteArray;
fn symbol(self: @TState) -> ByteArray;
fn decimals(self: @TState) -> u8;

// 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;
}

#[starknet::interface]
pub trait IERC20Permit<TState> {
fn permit(
ref self: TState,
owner: ContractAddress,
spender: ContractAddress,
amount: u256,
deadline: u64,
signature: Span<felt252>
);
fn nonces(self: @TState, owner: ContractAddress) -> felt252;
fn DOMAIN_SEPARATOR(self: @TState) -> felt252;
}

#[starknet::interface]
pub trait ERC20ABI<TState> {
// IERC20
Expand All @@ -66,6 +105,21 @@ pub trait ERC20ABI<TState> {
fn transferFrom(
ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256
) -> bool;

// IERC20Permit
fn permit(
ref self: TState,
owner: ContractAddress,
spender: ContractAddress,
amount: u256,
deadline: u64,
signature: Span<felt252>
);
fn nonces(self: @TState, owner: ContractAddress) -> felt252;
fn DOMAIN_SEPARATOR(self: @TState) -> felt252;

// ISNIP12Metadata
fn snip12_metadata(self: @TState) -> (felt252, felt252);
}

#[starknet::interface]
Expand Down
2 changes: 2 additions & 0 deletions packages/token/src/erc20/snip12_utils.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod permit;
pub mod votes;
Loading