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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

ERC20 Permit Component #1140

wants to merge 13 commits into from

Conversation

immrsd
Copy link
Collaborator

@immrsd immrsd commented Sep 10, 2024

Implements ERC20Permit extension

PR Checklist

  • Tests
  • Documentation in a separate PR
  • Added entry to CHANGELOG.md
  • Tried the feature on a public network

Copy link
Member

@ericnordelo ericnordelo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Alejandro, the implementation is looking good, but I think we should refactor this into an extra embeddable implementation in the ERC20Component, instead of an extra component, since this would reduce the boilerplate in the final contract, and a component without storage members and events can be just an extra module.

CHANGELOG.md Outdated Show resolved Hide resolved
/// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we have this release together with a Wizard update?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed ERC20Permit preset and this message also as a result

packages/token/src/erc20/extensions.cairo Outdated Show resolved Hide resolved
/// 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this extension doesn't need extra storage or events, it doesn't need to be a component, but can be implemented as an extra embeddable_impl in ERC20Component. This module can still exist as a container for helpers.

All we need is this extra impl in the ERC20Component (with the corresponding imports):

    #[embeddable_as(ERC20PermitImpl)]
    impl ERC20Permit<
        TContractState,
        +HasComponent<TContractState>,
        +ERC20HooksTrait<TContractState>,
        impl Nonces: NoncesComponent::HasComponent<TContractState>,
        impl Metadata: SNIP12Metadata,
        +Drop<TContractState>
    > of IERC20Permit<ComponentState<TContractState>> {
        /// 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<TContractState>,
            owner: ContractAddress,
            spender: ContractAddress,
            amount: u256,
            deadline: u64,
            signature: Array<felt252>
        ) {
            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 = 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
            self._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<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()
        }
    }

And then the preset can avoid the extra component boilerplate, and can just embed the impl (together with the NoncesComponent) when the permit logic is required.

As a general rule, we should avoid components without storage and events (and sometimes even with events), since they can be regular modules.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule, we should avoid components without storage and events (and sometimes even with events), since they can be regular modules.

+1

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completely agree, refactored it to be a trait of the ERC20 component

Copy link
Collaborator

@andrew-fleming andrew-fleming left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, sir! I do agree with Eric with having ERC20Permit as an embeddable impl inside of ERC20Component. This should be a little easier to integrate into a contract. Aside from this, I left a few comments :)

/// 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule, we should avoid components without storage and events (and sometimes even with events), since they can be regular modules.

+1

Comment on lines 4 to 9
/// # 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want a preset for erc20permit? I don't see why we'd include this and not erc20votes (that's not to suggest we should have a preset for erc20votes)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we did include this as a preset, it should be upgradeable and its interface should be defined in presets::src::interfaces

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to go without the preset. It doesn't make sense to have it, especially once it was refactored to be a trait instead of a component

Comment on lines +50 to +51
pub(crate) impl SNIP12MetadataImpl of SNIP12Metadata {
fn name() -> felt252 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the pub(crate)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, now I import the implementation to use the same name and version in the test setup function

Comment on lines +84 to +106
#[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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the test bodies are the same, can't we get around writing them twice? Something like this:

fn valid_permit_assertions(data: TestData) {...}

#[test]
fn test_valid_permit_default_data() {
    valid_permit_assertions(TEST_DATA());
}

Just an idea, not sure if it's worth it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frankly, I don't think this would be helpful. There would be only 2 test cases covered by this helper, all other cases have slight differences

Copy link

codecov bot commented Sep 27, 2024

Codecov Report

Attention: Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.

Project coverage is 88.95%. Comparing base (bf5d02c) to head (3e48a9a).
Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
packages/token/src/erc20/erc20.cairo 93.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1140      +/-   ##
==========================================
+ Coverage   88.87%   88.95%   +0.07%     
==========================================
  Files          57       58       +1     
  Lines        1375     1394      +19     
==========================================
+ Hits         1222     1240      +18     
- Misses        153      154       +1     
Files with missing lines Coverage Δ
packages/testing/src/constants.cairo 100.00% <100.00%> (ø)
...ages/token/src/erc20/extensions/erc20_permit.cairo 100.00% <100.00%> (ø)
packages/utils/src/cryptography/snip12.cairo 83.33% <ø> (ø)
packages/token/src/erc20/erc20.cairo 95.34% <93.33%> (-0.43%) ⬇️

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bf5d02c...3e48a9a. Read the comment docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants