From 32ac1a81582bb9c289d88a3fb1dc28cd6f84b2be Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 21 Jun 2024 11:14:08 +0200 Subject: [PATCH 01/44] erc6909 cairo contracts and tests --- packages/interfaces/src/token.cairo | 1 + packages/interfaces/src/token/erc6909.cairo | 0 .../test_common/src/mocks/erc6909_mocks.cairo | 284 ++++++++ packages/token/src/erc6909.cairo | 8 + packages/token/src/erc6909/dual6909.cairo | 137 ++++ packages/token/src/erc6909/erc6909.cairo | 662 ++++++++++++++++++ packages/token/src/erc6909/interface.cairo | 238 +++++++ packages/token/src/tests/erc6909.cairo | 4 + packages/token/src/tests/erc6909/common.cairo | 90 +++ .../src/tests/erc6909/test_dual6909.cairo | 260 +++++++ .../src/tests/erc6909/test_erc6909.cairo | 541 ++++++++++++++ 11 files changed, 2225 insertions(+) create mode 100644 packages/interfaces/src/token/erc6909.cairo create mode 100644 packages/test_common/src/mocks/erc6909_mocks.cairo create mode 100644 packages/token/src/erc6909.cairo create mode 100644 packages/token/src/erc6909/dual6909.cairo create mode 100644 packages/token/src/erc6909/erc6909.cairo create mode 100644 packages/token/src/erc6909/interface.cairo create mode 100644 packages/token/src/tests/erc6909.cairo create mode 100644 packages/token/src/tests/erc6909/common.cairo create mode 100644 packages/token/src/tests/erc6909/test_dual6909.cairo create mode 100644 packages/token/src/tests/erc6909/test_erc6909.cairo diff --git a/packages/interfaces/src/token.cairo b/packages/interfaces/src/token.cairo index a9654141f..523aef8f8 100644 --- a/packages/interfaces/src/token.cairo +++ b/packages/interfaces/src/token.cairo @@ -2,4 +2,5 @@ pub mod erc1155; pub mod erc20; pub mod erc2981; pub mod erc4626; +pub mod erc6909; pub mod erc721; diff --git a/packages/interfaces/src/token/erc6909.cairo b/packages/interfaces/src/token/erc6909.cairo new file mode 100644 index 000000000..e69de29bb diff --git a/packages/test_common/src/mocks/erc6909_mocks.cairo b/packages/test_common/src/mocks/erc6909_mocks.cairo new file mode 100644 index 000000000..d600ce077 --- /dev/null +++ b/packages/test_common/src/mocks/erc6909_mocks.cairo @@ -0,0 +1,284 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC6909Mock { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + /// Component + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + /// ABI of Components + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + #[abi(embed_v0)] + impl ERC6909CamelOnlyImpl = + ERC6909Component::ERC6909CamelOnlyImpl; + #[abi(embed_v0)] + impl ERC6909TokenSupplyImpl = + ERC6909Component::ERC6909TokenSupplyImpl; + #[abi(embed_v0)] + impl ERC6909TokenSupplyCamelImpl = + ERC6909Component::ERC6909TokenSupplyCamelImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIImpl = + ERC6909Component::ERC6909ContentURIImpl; + #[abi(embed_v0)] + impl ERC6909ContentURICamelImpl = + ERC6909Component::ERC6909ContentURICamelImpl; + + /// Internal logic + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + self.erc6909._set_contract_uri("URI"); + } +} + +#[starknet::contract] +pub(crate) mod SnakeERC6909Mock { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + /// ABI of Components + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + #[abi(embed_v0)] + impl ERC6909TokenSupplyImpl = + ERC6909Component::ERC6909TokenSupplyImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIImpl = + ERC6909Component::ERC6909ContentURIImpl; + + /// Internal logic + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } +} + +#[starknet::contract] +pub(crate) mod CamelERC6909Mock { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + /// ABI of Components + #[abi(embed_v0)] + impl ERC6909CamelOnlyImpl = + ERC6909Component::ERC6909CamelOnlyImpl; + #[abi(embed_v0)] + impl ERC6909TokenSupplyCamelImpl = + ERC6909Component::ERC6909TokenSupplyCamelImpl; + #[abi(embed_v0)] + impl ERC6909ContentURICamelImpl = + ERC6909Component::ERC6909ContentURICamelImpl; + + + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } + + #[abi(per_item)] + #[generate_trait] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256 + ) -> u256 { + self.erc6909.allowance(owner, spender, id) + } + + #[external(v0)] + fn transfer( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool { + self.erc6909.transfer(receiver, id, amount) + } + + #[external(v0)] + fn approve( + ref self: ContractState, spender: ContractAddress, id: u256, amount: u256 + ) -> bool { + self.erc6909.approve(spender, id, amount) + } + } +} + +/// Although these modules are designed to panic, functions +/// still need a valid return value. We chose: +/// +/// 3 for felt252, u8, and u256 +/// zero for ContractAddress +/// false for bool +#[starknet::contract] +pub(crate) mod SnakeERC6909Panic { + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[abi(per_item)] + #[generate_trait] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn balance_of(self: @ContractState, owner: ContractAddress, id: u256) -> u256 { + panic!("Some error"); + 3 + } + + #[external(v0)] + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256 + ) -> u256 { + panic!("Some error"); + 3 + } + + #[external(v0)] + fn is_operator( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn transfer( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn approve( + ref self: ContractState, spender: ContractAddress, id: u256, amount: u256 + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn set_operator(ref self: ContractState, spender: ContractAddress, approved: bool) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + panic!("Some error"); + false + } + } +} + +#[starknet::contract] +pub(crate) mod CamelERC6909Panic { + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[abi(per_item)] + #[generate_trait] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn balanceOf(self: @ContractState, owner: ContractAddress, id: u256) -> u256 { + panic!("Some error"); + 3 + } + + #[external(v0)] + fn isOperator( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn setOperator(ref self: ContractState, spender: ContractAddress, approved: bool) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn supportsInterface(self: @ContractState, interface_id: felt252) -> bool { + panic!("Some error"); + false + } + } +} diff --git a/packages/token/src/erc6909.cairo b/packages/token/src/erc6909.cairo new file mode 100644 index 000000000..c5fe1ce7a --- /dev/null +++ b/packages/token/src/erc6909.cairo @@ -0,0 +1,8 @@ +pub mod dual6909; +pub mod erc6909; +pub mod interface; + +pub use erc6909::ERC6909Component; +pub use erc6909::ERC6909HooksEmptyImpl; +pub use interface::ERC6909ABIDispatcher; +pub use interface::ERC6909ABIDispatcherTrait; diff --git a/packages/token/src/erc6909/dual6909.cairo b/packages/token/src/erc6909/dual6909.cairo new file mode 100644 index 000000000..470051f52 --- /dev/null +++ b/packages/token/src/erc6909/dual6909.cairo @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +use openzeppelin::utils::UnwrapAndCast; +use openzeppelin::utils::selectors; +use openzeppelin::utils::serde::SerializedAppend; +use openzeppelin::utils::try_selector_with_fallback; +use starknet::ContractAddress; +use starknet::SyscallResultTrait; +use starknet::syscalls::call_contract_syscall; + +#[derive(Copy, Drop)] +pub struct DualCaseERC6909 { + pub contract_address: ContractAddress +} + +pub trait DualCaseERC6909Trait { + fn balance_of(self: @DualCaseERC6909, owner: ContractAddress, id: u256) -> u256; + fn allowance( + self: @DualCaseERC6909, owner: ContractAddress, spender: ContractAddress, id: u256 + ) -> u256; + fn is_operator( + self: @DualCaseERC6909, owner: ContractAddress, spender: ContractAddress + ) -> bool; + fn transfer(self: @DualCaseERC6909, receiver: ContractAddress, id: u256, amount: u256) -> bool; + fn transfer_from( + self: @DualCaseERC6909, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool; + fn approve(self: @DualCaseERC6909, spender: ContractAddress, id: u256, amount: u256) -> bool; + fn set_operator(self: @DualCaseERC6909, spender: ContractAddress, approved: bool) -> bool; + fn supports_interface(self: @DualCaseERC6909, interface_id: felt252) -> bool; +} + +impl DualCaseERC6909Impl of DualCaseERC6909Trait { + fn balance_of(self: @DualCaseERC6909, owner: ContractAddress, id: u256) -> u256 { + let mut args = array![]; + args.append_serde(owner); + args.append_serde(id); + + try_selector_with_fallback( + *self.contract_address, selectors::balance_of, selectors::balanceOf, args.span() + ) + .unwrap_and_cast() + } + + fn allowance( + self: @DualCaseERC6909, owner: ContractAddress, spender: ContractAddress, id: u256 + ) -> u256 { + let mut args = array![]; + args.append_serde(owner); + args.append_serde(spender); + args.append_serde(id); + + call_contract_syscall(*self.contract_address, selectors::allowance, args.span()) + .unwrap_and_cast() + } + + fn is_operator( + self: @DualCaseERC6909, owner: ContractAddress, spender: ContractAddress + ) -> bool { + let mut args = array![]; + args.append_serde(owner); + args.append_serde(spender); + + let is_operator: felt252 = selectors::is_operator; + let isOperator: felt252 = selectors::isOperator; + + try_selector_with_fallback(*self.contract_address, is_operator, isOperator, args.span()) + .unwrap_and_cast() + } + + fn transfer(self: @DualCaseERC6909, receiver: ContractAddress, id: u256, amount: u256) -> bool { + let mut args = array![]; + args.append_serde(receiver); + args.append_serde(id); + args.append_serde(amount); + + call_contract_syscall(*self.contract_address, selectors::transfer, args.span()) + .unwrap_and_cast() + } + + fn transfer_from( + self: @DualCaseERC6909, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + let mut args = array![]; + args.append_serde(sender); + args.append_serde(receiver); + args.append_serde(id); + args.append_serde(amount); + + try_selector_with_fallback( + *self.contract_address, selectors::transfer_from, selectors::transferFrom, args.span() + ) + .unwrap_and_cast() + } + + fn approve(self: @DualCaseERC6909, spender: ContractAddress, id: u256, amount: u256) -> bool { + let mut args = array![]; + args.append_serde(spender); + args.append_serde(id); + args.append_serde(amount); + + call_contract_syscall(*self.contract_address, selectors::approve, args.span()) + .unwrap_and_cast() + } + + fn set_operator(self: @DualCaseERC6909, spender: ContractAddress, approved: bool) -> bool { + let mut args = array![]; + args.append_serde(spender); + args.append_serde(approved); + + let set_operator: felt252 = selectors::set_operator; + let setOperator: felt252 = selectors::setOperator; + + try_selector_with_fallback(*self.contract_address, set_operator, setOperator, args.span()) + .unwrap_and_cast() + } + + fn supports_interface(self: @DualCaseERC6909, interface_id: felt252) -> bool { + let mut args = array![]; + args.append_serde(interface_id); + + let supports_interface: felt252 = selectors::supports_interface; + let supportsInterface: felt252 = selectors::supportsInterface; + + try_selector_with_fallback( + *self.contract_address, supports_interface, supportsInterface, args.span() + ) + .unwrap_and_cast() + } +} diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo new file mode 100644 index 000000000..acea3ae52 --- /dev/null +++ b/packages/token/src/erc6909/erc6909.cairo @@ -0,0 +1,662 @@ +// SPDX-License-Identifier: MIT +use core::starknet::{ContractAddress}; + +/// # ERC6909 Component +/// +/// The ERC6909 component provides an implementation of the Minimal Multi-Token standard authored by jtriley.eth +/// See https://eips.ethereum.org/EIPS/eip-6909. +#[starknet::component] +pub mod ERC6909Component { + use core::integer::BoundedInt; + use core::num::traits::Zero; + use core::starknet::{ContractAddress, get_caller_address}; + use openzeppelin::introspection::interface::ISRC5_ID; + use openzeppelin::token::erc6909::interface; + + #[storage] + struct Storage { + ERC6909_name: LegacyMap, + ERC6909_symbol: LegacyMap, + ERC6909_balances: LegacyMap<(ContractAddress, u256), u256>, + ERC6909_allowances: LegacyMap<(ContractAddress, ContractAddress, u256), u256>, + ERC6909_operators: LegacyMap<(ContractAddress, ContractAddress), bool>, + ERC6909_total_supply: LegacyMap, + ERC6909_contract_uri: ByteArray, + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event { + Transfer: Transfer, + Approval: Approval, + OperatorSet: OperatorSet + } + + /// @notice The event emitted when a transfer occurs. + /// @param caller The caller of the transfer. + /// @param sender The address of the sender. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Transfer { + pub caller: ContractAddress, + #[key] + pub sender: ContractAddress, + #[key] + pub receiver: ContractAddress, + #[key] + pub id: u256, + pub amount: u256, + } + + /// @notice The event emitted when an approval occurs. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param id The id of the token. + /// @param amount The amount of the token. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Approval { + #[key] + pub owner: ContractAddress, + #[key] + pub spender: ContractAddress, + #[key] + pub id: u256, + pub amount: u256 + } + + /// @notice The event emitted when an operator is set. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param approved The approval status. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct OperatorSet { + #[key] + pub owner: ContractAddress, + #[key] + pub spender: ContractAddress, + pub approved: bool, + } + + pub mod Errors { + /// @dev Thrown when owner balance for id is insufficient. + pub const INSUFFICIENT_BALANCE: felt252 = 'ERC6909: insufficient balance'; + /// @dev Thrown when spender allowance for id is insufficient. + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'ERC6909: insufficient allowance'; + /// @dev Thrown when transfering from the zero address + pub const TRANSFER_FROM_ZERO: felt252 = 'ERC6909: transfer from 0'; + /// @dev Thrown when transfering to the zero address + pub const TRANSFER_TO_ZERO: felt252 = 'ERC6909: transfer to 0'; + /// @dev Thrown when minting to the zero address + pub const MINT_TO_ZERO: felt252 = 'ERC6909: mint to 0'; + /// @dev Thrown when burning from the zero address + pub const BURN_FROM_ZERO: felt252 = 'ERC6909: burn from 0'; + /// @dev Thrown when approving from the zero address + pub const APPROVE_FROM_ZERO: felt252 = 'ERC6909: approve from 0'; + /// @dev Thrown when approving to the zero address + pub const APPROVE_TO_ZERO: felt252 = 'ERC6909: approve to 0'; + } + + /// Hooks + pub trait ERC6909HooksTrait { + fn before_update( + ref self: ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ); + + fn after_update( + ref self: ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ); + } + + #[embeddable_as(ERC6909Impl)] + impl ERC6909< + TContractState, +HasComponent, +ERC6909HooksTrait + > of interface::IERC6909> { + /// @notice Owner balance of an id. + /// @param owner The address of the owner. + /// @param id The id of the token. + /// @return The balance of the token. + fn balance_of( + self: @ComponentState, owner: ContractAddress, id: u256 + ) -> u256 { + self.ERC6909_balances.read((owner, id)) + } + + /// @notice Spender allowance of an id. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param id The id of the token. + /// @return The allowance of the token. + fn allowance( + self: @ComponentState, + owner: ContractAddress, + spender: ContractAddress, + id: u256 + ) -> u256 { + self.ERC6909_allowances.read((owner, spender, id)) + } + + /// @notice Checks if a spender is approved by an owner as an operator + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @return The approval status. + fn is_operator( + self: @ComponentState, owner: ContractAddress, spender: ContractAddress + ) -> bool { + self.ERC6909_operators.read((owner, spender)) + } + + /// @notice Transfers an amount of an id from the caller to a receiver. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn transfer( + ref self: ComponentState, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._transfer(caller, caller, receiver, id, amount); + true + } + + /// @notice Transfers an amount of an id from a sender to a receiver. + /// @param sender The address of the sender. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn transfer_from( + ref self: ComponentState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._spend_allowance(sender, caller, id, amount); + self._transfer(caller, sender, receiver, id, amount); + true + } + + /// @notice Approves an amount of an id to a spender. + /// @param spender The address of the spender. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn approve( + ref self: ComponentState, + spender: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._approve(caller, spender, id, amount); + true + } + + /// @notice Sets or unsets a spender as an operator for the caller. + /// @param spender The address of the spender. + /// @param approved The approval status. + fn set_operator( + ref self: ComponentState, spender: ContractAddress, approved: bool + ) -> bool { + let caller = get_caller_address(); + self._set_operator(caller, spender, approved); + true + } + + /// @notice Checks if a contract implements an interface. + /// @param interfaceId The interface identifier, as specified in ERC-165. + /// @return True if the contract implements `interfaceId` and `interfaceId` is not 0xffffffff, false otherwise. + fn supports_interface( + self: @ComponentState, interface_id: felt252 + ) -> bool { + interface_id == interface::IERC6909_ID || interface_id == ISRC5_ID + } + } + + #[embeddable_as(ERC6909CamelOnlyImpl)] + impl ERC6909CamelOnly< + TContractState, +HasComponent, +ERC6909HooksTrait + > of interface::IERC6909CamelOnly> { + /// @notice Owner balance of an id. + /// @param owner The address of the owner. + /// @param id The id of the token. + /// @return The balance of the token. + fn balanceOf( + self: @ComponentState, owner: ContractAddress, id: u256 + ) -> u256 { + ERC6909::balance_of(self, owner, id) + } + + /// @notice Checks if a spender is approved by an owner as an operator + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @return The approval status. + fn isOperator( + self: @ComponentState, owner: ContractAddress, spender: ContractAddress + ) -> bool { + ERC6909::is_operator(self, owner, spender) + } + + /// @notice Transfers an amount of an id from a sender to a receiver. + /// @param sender The address of the sender. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn transferFrom( + ref self: ComponentState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + ERC6909::transfer_from(ref self, sender, receiver, id, amount) + } + + /// @notice Sets or unsets a spender as an operator for the caller. + /// @param spender The address of the spender. + /// @param approved The approval status. + fn setOperator( + ref self: ComponentState, spender: ContractAddress, approved: bool + ) -> bool { + ERC6909::set_operator(ref self, spender, approved) + } + + /// @notice Checks if a contract implements an interface. + /// @param interfaceId The interface identifier, as specified in ERC-165. + /// @return True if the contract implements `interfaceId` and `interfaceId` is not 0xffffffff, false otherwise. + fn supportsInterface(self: @ComponentState, interface_id: felt252) -> bool { + ERC6909::supports_interface(self, interface_id) + } + } + + #[embeddable_as(ERC6909MetadataImpl)] + impl ERC6909Metadata< + TContractState, +HasComponent, +ERC6909HooksTrait + > of interface::IERC6909Metadata> { + /// @notice Name of a given token. + /// @param id The id of the token. + /// @return The name of the token. + fn name(self: @ComponentState, id: u256) -> ByteArray { + self.ERC6909_name.read(id) + } + + /// @notice Symbol of a given token. + /// @param id The id of the token. + /// @return The symbol of the token. + fn symbol(self: @ComponentState, id: u256) -> ByteArray { + self.ERC6909_symbol.read(id) + } + + /// @notice Decimals of a given token. + /// @param id The id of the token. + /// @return The decimals of the token. + fn decimals(self: @ComponentState, id: u256) -> u8 { + 18 + } + } + + #[embeddable_as(ERC6909TokenSupplyImpl)] + impl ERC6909TokenSupply< + TContractState, +HasComponent, +ERC6909HooksTrait + > of interface::IERC6909TokenSupply> { + /// @notice Total supply of a token + /// @param id The id of the token. + /// @return The total supply of the token. + fn total_supply(self: @ComponentState, id: u256) -> u256 { + self.ERC6909_total_supply.read(id) + } + } + + #[embeddable_as(ERC6909TokenSupplyCamelImpl)] + impl ERC6909TokenSupplyCamel< + TContractState, +HasComponent, +ERC6909HooksTrait + > of interface::IERC6909TokenSupplyCamel> { + /// @notice Total supply of a token + /// @param id The id of the token. + /// @return The total supply of the token. + fn totalSupply(self: @ComponentState, id: u256) -> u256 { + ERC6909TokenSupply::total_supply(self, id) + } + } + + + #[embeddable_as(ERC6909ContentURIImpl)] + impl ERC6909ContentURI< + TContractState, +HasComponent, +ERC6909HooksTrait + > of interface::IERC6909ContentURI> { + /// @notice The contract level URI. + /// @return The URI of the contract. + fn contract_uri(self: @ComponentState) -> ByteArray { + self.ERC6909_contract_uri.read() + } + + /// @notice Token level URI + /// @param id The id of the token. + /// @return The token level URI. + fn token_uri(self: @ComponentState, id: u256) -> ByteArray { + let contract_uri = self.contract_uri(); + if contract_uri.len() != 0 { + return ""; + } else { + return format!("{}{}", contract_uri, id); + } + } + } + + #[embeddable_as(ERC6909ContentURICamelImpl)] + impl ERC6909ContentURICamel< + TContractState, +HasComponent, +ERC6909HooksTrait + > of interface::IERC6909ContentURICamel> { + /// @notice Contract level URI + /// @return uri The contract level URI. + fn contractUri(self: @ComponentState) -> ByteArray { + ERC6909ContentURI::contract_uri(self) + } + + /// @notice Token level URI + /// @param id The id of the token. + /// @return The token level URI. + fn tokenUri(self: @ComponentState, id: u256) -> ByteArray { + ERC6909ContentURI::token_uri(self, id) + } + } + + /// internal + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, impl Hooks: ERC6909HooksTrait + > of InternalTrait { + /// Creates a `value` amount of tokens and assigns them to `account`. + /// + /// Requirements: + /// + /// - `receiver` is not the zero address. + /// + /// Emits a `Transfer` event with `from` set to the zero address. + fn mint( + ref self: ComponentState, + receiver: ContractAddress, + id: u256, + amount: u256 + ) { + assert(!receiver.is_zero(), Errors::MINT_TO_ZERO); + self.update(get_caller_address(), Zero::zero(), receiver, id, amount); + } + + /// Destroys `amount` of tokens from `account`. + /// + /// Requirements: + /// + /// - `account` is not the zero address. + /// - `account` must have at least a balance of `amount`. + /// + /// Emits a `Transfer` event with `to` set to the zero address. + fn burn( + ref self: ComponentState, + account: ContractAddress, + id: u256, + amount: u256 + ) { + assert(!account.is_zero(), Errors::BURN_FROM_ZERO); + self.update(get_caller_address(), account, Zero::zero(), id, amount); + } + + /// Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or burns) if `sender` (or `receiver`) is + /// the zero address. + /// + /// Emits a `Transfer` event. + fn update( + ref self: ComponentState, + caller: ContractAddress, + sender: ContractAddress, // from + receiver: ContractAddress, // to + id: u256, + amount: u256 + ) { + Hooks::before_update(ref self, sender, receiver, id, amount); + + let zero_address = Zero::zero(); + if (sender == zero_address) { + let total_supply = self.ERC6909_total_supply.read(id); + self.ERC6909_total_supply.write(id, total_supply + amount); + } else { + let sender_balance = self.ERC6909_balances.read((sender, id)); + assert(sender_balance >= amount, Errors::INSUFFICIENT_BALANCE); + self.ERC6909_balances.write((sender, id), sender_balance - amount); + } + + if (receiver == zero_address) { + let total_supply = self.ERC6909_total_supply.read(id); + self.ERC6909_total_supply.write(id, total_supply - amount); + } else { + let receiver_balance = self.ERC6909_balances.read((receiver, id)); + self.ERC6909_balances.write((receiver, id), receiver_balance + amount); + } + + self.emit(Transfer { caller, sender, receiver, id, amount }); + + Hooks::after_update(ref self, sender, receiver, id, amount); + } + + /// Sets the base URI. + fn _set_contract_uri(ref self: ComponentState, contract_uri: ByteArray) { + self.ERC6909_contract_uri.write(contract_uri); + } + + /// @notice Sets or unsets a spender as an operator for the caller. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param approved The approval status. + fn _set_operator( + ref self: ComponentState, + owner: ContractAddress, + spender: ContractAddress, + approved: bool + ) { + self.ERC6909_operators.write((owner, spender), approved); + self.emit(OperatorSet { owner, spender, approved }); + } + + /// Updates `sender`s allowance for `spender` and `id` based on spent `amount`. + /// Does not update the allowance value in case of infinite allowance. + fn _spend_allowance( + ref self: ComponentState, + sender: ContractAddress, + spender: ContractAddress, + id: u256, + amount: u256 + ) { + // In accordance with the transferFrom method, spenders with operator permission are not subject to + // allowance restrictions (https://eips.ethereum.org/EIPS/eip-6909). + if sender != spender && !self.ERC6909_operators.read((sender, spender)) { + let sender_allowance = self.ERC6909_allowances.read((sender, spender, id)); + assert(sender_allowance >= amount, Errors::INSUFFICIENT_ALLOWANCE); + if sender_allowance != BoundedInt::max() { + self._approve(sender, spender, id, sender_allowance - amount) + } + } + } + + /// Internal method that sets `amount` as the allowance of `spender` over the + /// `owner`s tokens. + /// + /// Requirements: + /// + /// - `owner` is not the zero address. + /// - `spender` is not the zero address. + /// + /// Emits an `Approval` event. + fn _approve( + ref self: ComponentState, + owner: ContractAddress, + spender: ContractAddress, + id: u256, + amount: u256 + ) { + assert(!owner.is_zero(), Errors::APPROVE_FROM_ZERO); + assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO); + self.ERC6909_allowances.write((owner, spender, id), amount); + self.emit(Approval { owner, spender, id, amount }); + } + + /// Internal method that moves an `amount` of tokens from `sender` to `receiver`. + /// + /// Requirements: + /// + /// - `sender` is not the zero address. + /// - `sender` must have at least a balance of `amount`. + /// - `receiver` is not the zero address. + /// + /// Emits a `Transfer` event. + fn _transfer( + ref self: ComponentState, + caller: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) { + assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); + assert(!receiver.is_zero(), Errors::TRANSFER_TO_ZERO); + self.update(caller, sender, receiver, id, amount); + } + } + + #[embeddable_as(ERC6909MixinImpl)] + impl ERC6909Mixin< + TContractState, +HasComponent, +ERC6909HooksTrait + > of interface::ERC6909ABI> { + // + // ABI + // + + fn balance_of( + self: @ComponentState, owner: ContractAddress, id: u256 + ) -> u256 { + ERC6909::balance_of(self, owner, id) + } + + fn allowance( + self: @ComponentState, + owner: ContractAddress, + spender: ContractAddress, + id: u256 + ) -> u256 { + ERC6909::allowance(self, owner, spender, id) + } + + fn is_operator( + self: @ComponentState, owner: ContractAddress, spender: ContractAddress + ) -> bool { + ERC6909::is_operator(self, owner, spender) + } + + fn transfer( + ref self: ComponentState, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + ERC6909::transfer(ref self, receiver, id, amount) + } + + fn transfer_from( + ref self: ComponentState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + ERC6909::transfer_from(ref self, sender, receiver, id, amount) + } + + fn approve( + ref self: ComponentState, + spender: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + ERC6909::approve(ref self, spender, id, amount) + } + + fn set_operator( + ref self: ComponentState, spender: ContractAddress, approved: bool + ) -> bool { + ERC6909::set_operator(ref self, spender, approved) + } + + fn supports_interface( + self: @ComponentState, interface_id: felt252 + ) -> bool { + ERC6909::supports_interface(self, interface_id) + } + + // + // CamelCase + // + + fn balanceOf( + self: @ComponentState, owner: ContractAddress, id: u256 + ) -> u256 { + ERC6909::balance_of(self, owner, id) + } + + fn isOperator( + self: @ComponentState, owner: ContractAddress, spender: ContractAddress + ) -> bool { + ERC6909::is_operator(self, owner, spender) + } + + fn transferFrom( + ref self: ComponentState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + ERC6909::transfer_from(ref self, sender, receiver, id, amount) + } + + fn setOperator( + ref self: ComponentState, spender: ContractAddress, approved: bool + ) -> bool { + ERC6909::set_operator(ref self, spender, approved) + } + + fn supportsInterface(self: @ComponentState, interfaceId: felt252) -> bool { + ERC6909::supports_interface(self, interfaceId) + } + } +} + +/// An empty implementation of the ERC6909 hooks to be used in basic ERC6909 preset contracts. +pub impl ERC6909HooksEmptyImpl< + TContractState +> of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} +} diff --git a/packages/token/src/erc6909/interface.cairo b/packages/token/src/erc6909/interface.cairo new file mode 100644 index 000000000..6fd6685dc --- /dev/null +++ b/packages/token/src/erc6909/interface.cairo @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MIT +use starknet::ContractAddress; + +// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol +pub const IERC6909_ID: felt252 = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee; + +#[starknet::interface] +pub trait IERC6909 { + /// @notice Owner balance of an id. + /// @param owner The address of the owner. + /// @param id The id of the token. + /// @return The balance of the token. + fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; + + /// @notice Spender allowance of an id. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param id The id of the token. + /// @return The allowance of the token. + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + + /// @notice Checks if a spender is approved by an owner as an operator + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @return The approval status. + fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + + /// @notice Transfers an amount of an id from the caller to a receiver. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; + + /// @notice Transfers an amount of an id from a sender to a receiver. + /// @param sender The address of the sender. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn transfer_from( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + + /// @notice Approves an amount of an id to a spender. + /// @param spender The address of the spender. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; + + /// @notice Sets or removes a spender as an operator for the caller. + /// @param spender The address of the spender. + /// @param approved The approval status. + fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + + // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC165.sol + /// @notice Checks if a contract implements an interface. + /// @param interfaceId The interface identifier, as specified in ERC-165. + /// @return True if the contract implements `interfaceId` and + /// `interfaceId` is not 0xffffffff, false otherwise. + fn supports_interface(self: @TState, interface_id: felt252) -> bool; +} + +#[starknet::interface] +pub trait IERC6909Camel { + /// @notice Owner balance of an id. + /// @param owner The address of the owner. + /// @param id The id of the token. + /// @return The balance of the token. + fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; + + /// @notice Spender allowance of an id. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param id The id of the token. + /// @return The allowance of the token. + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + + /// @notice Checks if a spender is approved by an owner as an operator + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @return The approval status. + fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + + /// @notice Transfers an amount of an id from the caller to a receiver. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; + + /// @notice Transfers an amount of an id from a sender to a receiver. + /// @param sender The address of the sender. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn transferFrom( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + + /// @notice Approves an amount of an id to a spender. + /// @param spender The address of the spender. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; + + /// @notice Sets or removes a spender as an operator for the caller. + /// @param spender The address of the spender. + /// @param approved The approval status. + fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + + // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC165.sol + /// @notice Checks if a contract implements an interface. + /// @param interfaceId The interface identifier, as specified in ERC-165. + /// @return True if the contract implements `interfaceId` and + /// `interfaceId` is not 0xffffffff, false otherwise. + fn supportsInterface(self: @TState, interface_id: felt252) -> bool; +} + + +#[starknet::interface] +pub trait IERC6909CamelOnly { + /// @notice Owner balance of an id. + /// @param owner The address of the owner. + /// @param id The id of the token. + /// @return The balance of the token. + fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; + + /// @notice Checks if a spender is approved by an owner as an operator + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @return The approval status. + fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + + /// @notice Transfers an amount of an id from a sender to a receiver. + /// @param sender The address of the sender. + /// @param receiver The address of the receiver. + /// @param id The id of the token. + /// @param amount The amount of the token. + fn transferFrom( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + + /// @notice Sets or removes a spender as an operator for the caller. + /// @param spender The address of the spender. + /// @param approved The approval status. + fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + + // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC165.sol + /// @notice Checks if a contract implements an interface. + /// @param interfaceId The interface identifier, as specified in ERC-165. + /// @return True if the contract implements `interfaceId` and + /// `interfaceId` is not 0xffffffff, false otherwise. + fn supportsInterface(self: @TState, interface_id: felt252) -> bool; +} + +// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909Metadata.sol +#[starknet::interface] +pub trait IERC6909Metadata { + /// @notice Name of a given token. + /// @param id The id of the token. + /// @return The name of the token. + fn name(self: @TState, id: u256) -> ByteArray; + + /// @notice Symbol of a given token. + /// @param id The id of the token. + /// @return The symbol of the token. + fn symbol(self: @TState, id: u256) -> ByteArray; + + /// @notice Decimals of a given token. + /// @param id The id of the token. + /// @return The decimals of the token. + fn decimals(self: @TState, id: u256) -> u8; +} + +// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909TokenSupply.sol +#[starknet::interface] +pub trait IERC6909TokenSupply { + /// @notice Total supply of a token + /// @param id The id of the token. + /// @return The total supply of the token. + fn total_supply(self: @TState, id: u256) -> u256; +} + +#[starknet::interface] +pub trait IERC6909TokenSupplyCamel { + /// @notice Total supply of a token + /// @param id The id of the token. + /// @return The total supply of the token. + fn totalSupply(self: @TState, id: u256) -> u256; +} + +//https://github.com/jtriley-eth/ERC-6909/blob/main/src/ERC6909ContentURI.sol +#[starknet::interface] +pub trait IERC6909ContentURI { + /// @notice Contract level URI + /// @return The contract level URI. + fn contract_uri(self: @TState) -> ByteArray; + + /// @notice Token level URI + /// @param id The id of the token. + /// @return The token level URI. + fn token_uri(self: @TState, id: u256) -> ByteArray; +} + +#[starknet::interface] +pub trait IERC6909ContentURICamel { + /// @notice Contract level URI + /// @return The contract level URI. + fn contractUri(self: @TState) -> ByteArray; + + /// @notice Token level URI + /// @param id The id of the token. + /// @return The token level URI. + fn tokenUri(self: @TState, id: u256) -> ByteArray; +} + +// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol +#[starknet::interface] +pub trait ERC6909ABI { + /// @notice IERC6909 standard interface + fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; + fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + fn supports_interface(self: @TState, interface_id: felt252) -> bool; + + /// @notice IERC6909Camel + fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transferFrom( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + fn supportsInterface(self: @TState, interfaceId: felt252) -> bool; +} diff --git a/packages/token/src/tests/erc6909.cairo b/packages/token/src/tests/erc6909.cairo new file mode 100644 index 000000000..1bc5001cc --- /dev/null +++ b/packages/token/src/tests/erc6909.cairo @@ -0,0 +1,4 @@ +pub(crate) mod common; + +mod test_dual6909; +mod test_erc6909; diff --git a/packages/token/src/tests/erc6909/common.cairo b/packages/token/src/tests/erc6909/common.cairo new file mode 100644 index 000000000..0c8fdba98 --- /dev/null +++ b/packages/token/src/tests/erc6909/common.cairo @@ -0,0 +1,90 @@ +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet, InternalImpl}; +use openzeppelin::token::erc6909::ERC6909Component; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; + +// Approval +pub(crate) fn assert_event_approval( + contract: ContractAddress, + owner: ContractAddress, + spender: ContractAddress, + id: u256, + amount: u256 +) { + let event = utils::pop_log::(contract).unwrap(); + let expected = ERC6909Component::Event::Approval(Approval { owner, spender, id, amount }); + assert!(event == expected); + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("Approval")); + indexed_keys.append_serde(owner); + indexed_keys.append_serde(spender); + indexed_keys.append_serde(id); + utils::assert_indexed_keys(event, indexed_keys.span()) +} + +pub(crate) fn assert_only_event_approval( + contract: ContractAddress, + owner: ContractAddress, + spender: ContractAddress, + id: u256, + amount: u256 +) { + assert_event_approval(contract, owner, spender, id, amount); + utils::assert_no_events_left(contract); +} + +// Transfer +pub(crate) fn assert_event_transfer( + contract: ContractAddress, + caller: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 +) { + let event = utils::pop_log::(contract).unwrap(); + let expected = ERC6909Component::Event::Transfer( + Transfer { caller, sender, receiver, id, amount } + ); + assert!(event == expected); + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("Transfer")); + indexed_keys.append_serde(sender); + indexed_keys.append_serde(receiver); + indexed_keys.append_serde(id); + utils::assert_indexed_keys(event, indexed_keys.span()); +} + +pub(crate) fn assert_only_event_transfer( + contract: ContractAddress, + caller: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 +) { + assert_event_transfer(contract, caller, sender, receiver, id, amount); + utils::assert_no_events_left(contract); +} + +// OperatorSet +pub(crate) fn assert_only_event_operator_set( + contract: ContractAddress, owner: ContractAddress, spender: ContractAddress, approved: bool, +) { + assert_event_operator_set(contract, owner, spender, approved); + utils::assert_no_events_left(contract); +} + +pub(crate) fn assert_event_operator_set( + contract: ContractAddress, owner: ContractAddress, spender: ContractAddress, approved: bool +) { + let event = utils::pop_log::(contract).unwrap(); + let expected = ERC6909Component::Event::OperatorSet(OperatorSet { owner, spender, approved }); + assert!(event == expected); + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("OperatorSet")); + indexed_keys.append_serde(owner); + indexed_keys.append_serde(spender); + utils::assert_indexed_keys(event, indexed_keys.span()) +} diff --git a/packages/token/src/tests/erc6909/test_dual6909.cairo b/packages/token/src/tests/erc6909/test_dual6909.cairo new file mode 100644 index 000000000..82b4d38cc --- /dev/null +++ b/packages/token/src/tests/erc6909/test_dual6909.cairo @@ -0,0 +1,260 @@ +use openzeppelin::tests::mocks::erc6909_mocks::{CamelERC6909Mock, SnakeERC6909Mock}; +use openzeppelin::tests::mocks::erc6909_mocks::{CamelERC6909Panic, SnakeERC6909Panic}; +use openzeppelin::tests::mocks::non_implementing_mock::NonImplementingMock; +use openzeppelin::tests::utils::constants::{ + OWNER, RECIPIENT, SPENDER, OPERATOR, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::dual6909::{DualCaseERC6909, DualCaseERC6909Trait}; +use openzeppelin::token::erc6909::interface::{ + IERC6909CamelDispatcher, IERC6909CamelDispatcherTrait +}; +use openzeppelin::token::erc6909::interface::{IERC6909Dispatcher, IERC6909DispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::testing::set_contract_address; + +// +// Setup +// + +pub const TOKEN_ID: u256 = 420; + +fn setup_snake() -> (DualCaseERC6909, IERC6909Dispatcher) { + let mut calldata = array![]; + calldata.append_serde(OWNER()); + calldata.append_serde(TOKEN_ID); + calldata.append_serde(SUPPLY); + let target = utils::deploy(SnakeERC6909Mock::TEST_CLASS_HASH, calldata); + (DualCaseERC6909 { contract_address: target }, IERC6909Dispatcher { contract_address: target }) +} + +fn setup_camel() -> (DualCaseERC6909, IERC6909CamelDispatcher) { + let mut calldata = array![]; + calldata.append_serde(OWNER()); + calldata.append_serde(TOKEN_ID); + calldata.append_serde(SUPPLY); + let target = utils::deploy(CamelERC6909Mock::TEST_CLASS_HASH, calldata); + ( + DualCaseERC6909 { contract_address: target }, + IERC6909CamelDispatcher { contract_address: target } + ) +} + +fn setup_non_erc6909() -> DualCaseERC6909 { + let calldata = array![]; + let target = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, calldata); + DualCaseERC6909 { contract_address: target } +} + +fn setup_erc6909_panic() -> (DualCaseERC6909, DualCaseERC6909) { + let snake_target = utils::deploy(SnakeERC6909Panic::TEST_CLASS_HASH, array![]); + let camel_target = utils::deploy(CamelERC6909Panic::TEST_CLASS_HASH, array![]); + ( + DualCaseERC6909 { contract_address: snake_target }, + DualCaseERC6909 { contract_address: camel_target } + ) +} + +// +// Case agnostic methods +// + +#[test] +fn test_dual_transfer() { + let (snake_dispatcher, snake_target) = setup_snake(); + set_contract_address(OWNER()); + assert!(snake_dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE)); + assert_eq!(snake_target.balance_of(RECIPIENT(), TOKEN_ID), VALUE); + + let (camel_dispatcher, camel_target) = setup_camel(); + set_contract_address(OWNER()); + assert!(camel_dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE)); + assert_eq!(camel_target.balanceOf(RECIPIENT(), TOKEN_ID), VALUE); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_transfer() { + let dispatcher = setup_non_erc6909(); + dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_transfer_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE); +} + + +#[test] +fn test_dual_approve() { + let (snake_dispatcher, snake_target) = setup_snake(); + set_contract_address(OWNER()); + assert!(snake_dispatcher.approve(SPENDER(), TOKEN_ID, VALUE)); + + let snake_allowance = snake_target.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(snake_allowance, VALUE); + + let (camel_dispatcher, camel_target) = setup_camel(); + set_contract_address(OWNER()); + assert!(camel_dispatcher.approve(SPENDER(), TOKEN_ID, VALUE)); + + let camel_allowance = camel_target.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(camel_allowance, VALUE); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_approve() { + let dispatcher = setup_non_erc6909(); + dispatcher.approve(SPENDER(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_approve_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.approve(SPENDER(), TOKEN_ID, VALUE); +} + +// +// snake_case target +// + +#[test] +fn test_dual_balance_of() { + let (dispatcher, _) = setup_snake(); + assert_eq!(dispatcher.balance_of(OWNER(), TOKEN_ID), SUPPLY); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_balance_of() { + let dispatcher = setup_non_erc6909(); + dispatcher.balance_of(OWNER(), TOKEN_ID); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_balance_of_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.balance_of(OWNER(), TOKEN_ID); +} + +#[test] +fn test_dual_transfer_from() { + let (dispatcher, target) = setup_snake(); + set_contract_address(OWNER()); + target.approve(OPERATOR(), TOKEN_ID, VALUE); + + set_contract_address(OPERATOR()); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + assert_eq!(target.balance_of(RECIPIENT(), TOKEN_ID), VALUE); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_transfer_from() { + let dispatcher = setup_non_erc6909(); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_transfer_from_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); +} + +// set_operator +#[test] +fn test_dual_set_operator() { + let (dispatcher, target) = setup_snake(); + set_contract_address(OWNER()); + target.set_operator(OPERATOR(), true); + + set_contract_address(OPERATOR()); + assert!(dispatcher.is_operator(OWNER(), OPERATOR())); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_set_operator() { + let dispatcher = setup_non_erc6909(); + dispatcher.set_operator(OPERATOR(), true); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_set_operator_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.set_operator(OPERATOR(), true); +} + +// is_operator +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_is_operator() { + let dispatcher = setup_non_erc6909(); + dispatcher.is_operator(OWNER(), OPERATOR()); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_is_operator_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.is_operator(OWNER(), OPERATOR()); +} + +// +// camelCase target +// + +#[test] +fn test_dual_balanceOf() { + let (dispatcher, _) = setup_camel(); + assert_eq!(dispatcher.balance_of(OWNER(), TOKEN_ID), SUPPLY); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_balanceOf_exists_and_panics() { + let (_, dispatcher) = setup_erc6909_panic(); + dispatcher.balance_of(OWNER(), TOKEN_ID); +} + +#[test] +fn test_dual_transferFrom() { + let (dispatcher, target) = setup_camel(); + set_contract_address(OWNER()); + target.approve(OPERATOR(), TOKEN_ID, VALUE); + + set_contract_address(OPERATOR()); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + assert_eq!(target.balanceOf(RECIPIENT(), TOKEN_ID), VALUE); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_transferFrom_exists_and_panics() { + let (_, dispatcher) = setup_erc6909_panic(); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +fn test_dual_setOperator() { + let (dispatcher, target) = setup_camel(); + set_contract_address(OWNER()); + target.setOperator(OPERATOR(), true); + + set_contract_address(OPERATOR()); + assert!(dispatcher.is_operator(OWNER(), OPERATOR())); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_setOperator_exists_and_panics() { + let (_, dispatcher) = setup_erc6909_panic(); + dispatcher.set_operator(OPERATOR(), true); +} diff --git a/packages/token/src/tests/erc6909/test_erc6909.cairo b/packages/token/src/tests/erc6909/test_erc6909.cairo new file mode 100644 index 000000000..0bfae9eeb --- /dev/null +++ b/packages/token/src/tests/erc6909/test_erc6909.cairo @@ -0,0 +1,541 @@ +use core::integer::BoundedInt; +use core::starknet::{ContractAddress, testing}; +use openzeppelin::introspection::interface::ISRC5_ID; +use openzeppelin::tests::mocks::erc6909_mocks::DualCaseERC6909Mock; +use openzeppelin::tests::utils::constants::{ + ZERO, OWNER, SPENDER, RECIPIENT, SUPPLY, VALUE, OPERATOR +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::{ + InternalImpl, ERC6909Impl, ERC6909CamelOnlyImpl, ERC6909TokenSupplyImpl, + ERC6909TokenSupplyCamelImpl +}; +use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet}; +use openzeppelin::token::erc6909::ERC6909Component; +use super::common::{ + assert_event_approval, assert_only_event_approval, assert_only_event_transfer, + assert_only_event_operator_set, assert_event_operator_set +}; + +// +// Setup +// + +const TOKEN_ID: u256 = 420; + +type ComponentState = ERC6909Component::ComponentState; + +fn COMPONENT_STATE() -> ComponentState { + ERC6909Component::component_state_for_testing() +} + +fn setup() -> ComponentState { + let mut state = COMPONENT_STATE(); + state.mint(OWNER(), TOKEN_ID, SUPPLY); + utils::drop_event(ZERO()); + state +} + +// +// Getters +// + +#[test] +fn test_total_supply() { + let mut state = COMPONENT_STATE(); + state.mint(OWNER(), TOKEN_ID, SUPPLY); + assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); +} + +#[test] +fn test_totalSupply() { + let mut state = COMPONENT_STATE(); + state.mint(OWNER(), TOKEN_ID, SUPPLY); + assert_eq!(state.totalSupply(TOKEN_ID), SUPPLY); +} + +#[test] +fn test_balance_of() { + let mut state = COMPONENT_STATE(); + state.mint(OWNER(), TOKEN_ID, SUPPLY); + assert_eq!(state.balance_of((OWNER()), TOKEN_ID), SUPPLY); +} + +#[test] +fn test_balanceOf() { + let mut state = COMPONENT_STATE(); + state.mint(OWNER(), TOKEN_ID, SUPPLY); + assert_eq!(state.balanceOf((OWNER()), TOKEN_ID), SUPPLY); +} + +#[test] +fn test_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, VALUE); + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(allowance, VALUE); +} + +// +// approve & _approve +// + +#[test] +fn test_approve() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + assert!(state.approve(SPENDER(), TOKEN_ID, VALUE)); + assert_only_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, VALUE); + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(allowance, VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: approve from 0',))] +fn test_approve_from_zero() { + let mut state = setup(); + state.approve(SPENDER(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: approve to 0',))] +fn test_approve_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(ZERO(), TOKEN_ID, VALUE); +} + +#[test] +fn test__approve() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state._approve(OWNER(), SPENDER(), TOKEN_ID, VALUE); + assert_only_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, VALUE); + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID,); + assert_eq!(allowance, VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: approve from 0',))] +fn test__approve_from_zero() { + let mut state = setup(); + state._approve(ZERO(), SPENDER(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: approve to 0',))] +fn test__approve_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state._approve(OWNER(), ZERO(), TOKEN_ID, VALUE); +} + +// +// transfer & _transfer +// + +#[test] +fn test_transfer() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + assert!(state.transfer(RECIPIENT(), TOKEN_ID, VALUE)); + + assert_only_event_transfer(ZERO(), OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); +} + +#[test] +#[should_panic(expected: ('ERC6909: insufficient balance',))] +fn test_transfer_not_enough_balance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + let balance_plus_one = SUPPLY + 1; + state.transfer(RECIPIENT(), TOKEN_ID, balance_plus_one); +} + +#[test] +#[should_panic(expected: ('ERC6909: transfer from 0',))] +fn test_transfer_from_zero() { + let mut state = setup(); + state.transfer(RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: transfer to 0',))] +fn test_transfer_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.transfer(ZERO(), TOKEN_ID, VALUE); +} + +#[test] +fn test__transfer() { + let mut state = setup(); + state._transfer(OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + assert_only_event_transfer(ZERO(), OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); +} + +#[test] +#[should_panic(expected: ('ERC6909: insufficient balance',))] +fn test__transfer_not_enough_balance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + let balance_plus_one = SUPPLY + 1; + state._transfer(OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, balance_plus_one); +} + +#[test] +#[should_panic(expected: ('ERC6909: transfer from 0',))] +fn test__transfer_from_zero() { + let mut state = setup(); + state._transfer(ZERO(), ZERO(), RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: transfer to 0',))] +fn test__transfer_to_zero() { + let mut state = setup(); + state._transfer(OWNER(), OWNER(), ZERO(), TOKEN_ID, VALUE); +} + +#[test] +fn test_self_transfer() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); + assert!(state.transfer(OWNER(), TOKEN_ID, 1)); + assert_only_event_transfer(ZERO(), OWNER(), OWNER(), OWNER(), TOKEN_ID, 1); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); +} + + +// +// transfer_from & transferFrom +// + +#[test] +fn test_transfer_from() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, VALUE); + utils::drop_event(ZERO()); + + testing::set_caller_address(SPENDER()); + assert!(state.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE)); + + assert_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, 0); + assert_only_event_transfer(ZERO(), SPENDER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(allowance, 0); + + assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); +} + +#[test] +fn test_transfer_from_doesnt_consume_infinite_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, BoundedInt::max()); + + testing::set_caller_address(SPENDER()); + state.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(allowance, BoundedInt::max()); +} + +#[test] +#[should_panic(expected: ('ERC6909: insufficient allowance',))] +fn test_transfer_from_greater_than_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, VALUE); + + testing::set_caller_address(SPENDER()); + let allowance_plus_one = VALUE + 1; + state.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, allowance_plus_one); +} + +#[test] +#[should_panic(expected: ('ERC6909: transfer to 0',))] +fn test_transfer_from_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, VALUE); + + testing::set_caller_address(SPENDER()); + state.transfer_from(OWNER(), ZERO(), TOKEN_ID, VALUE); +} + +// This does not check `_spend_allowance` since the owner (the zero address) +// is the sender, see `_spend_allowance` in erc6909.cairo +#[test] +#[should_panic(expected: ('ERC6909: transfer from 0',))] +fn test_transfer_from_from_zero_address() { + let mut state = setup(); + state.transfer_from(ZERO(), RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: insufficient allowance',))] +fn test_transfer_no_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, VALUE); + + testing::set_caller_address(RECIPIENT()); + state.transfer_from(OWNER(), ZERO(), TOKEN_ID, VALUE); +} + +#[test] +fn test_transferFrom() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, VALUE); + utils::drop_event(ZERO()); + + testing::set_caller_address(SPENDER()); + assert!(state.transferFrom(OWNER(), RECIPIENT(), TOKEN_ID, VALUE)); + + assert_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, 0); + assert_only_event_transfer(ZERO(), SPENDER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(allowance, 0); + + assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); + assert_eq!(allowance, 0); +} + +#[test] +fn test_transferFrom_doesnt_consume_infinite_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, BoundedInt::max()); + + testing::set_caller_address(SPENDER()); + state.transferFrom(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(allowance, BoundedInt::max()); +} + +#[test] +#[should_panic(expected: ('ERC6909: insufficient allowance',))] +fn test_transferFrom_greater_than_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, VALUE); + + testing::set_caller_address(SPENDER()); + let allowance_plus_one = VALUE + 1; + state.transferFrom(OWNER(), RECIPIENT(), TOKEN_ID, allowance_plus_one); +} + +#[test] +#[should_panic(expected: ('ERC6909: transfer to 0',))] +fn test_transferFrom_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.approve(SPENDER(), TOKEN_ID, VALUE); + + testing::set_caller_address(SPENDER()); + state.transferFrom(OWNER(), ZERO(), TOKEN_ID, VALUE); +} + +#[test] +fn test_self_transfer_from() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); + assert!(state.transfer_from(OWNER(), OWNER(), TOKEN_ID, 1)); + assert_only_event_transfer(ZERO(), OWNER(), OWNER(), OWNER(), TOKEN_ID, 1); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); +} + + +// +// _spend_allowance +// + +#[test] +fn test__spend_allowance_not_unlimited() { + let mut state = setup(); + + state._approve(OWNER(), SPENDER(), TOKEN_ID, SUPPLY); + utils::drop_event(ZERO()); + + state._spend_allowance(OWNER(), SPENDER(), TOKEN_ID, VALUE); + + assert_only_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, SUPPLY - VALUE); + + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(allowance, SUPPLY - VALUE); +} + +#[test] +fn test__spend_allowance_unlimited() { + let mut state = setup(); + state._approve(OWNER(), SPENDER(), TOKEN_ID, BoundedInt::max()); + + let max_minus_one: u256 = BoundedInt::max() - 1; + state._spend_allowance(OWNER(), SPENDER(), TOKEN_ID, max_minus_one); + + let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(allowance, BoundedInt::max()); +} + +// +// _mint +// + +#[test] +fn test__mint() { + let mut state = COMPONENT_STATE(); + state.mint(OWNER(), TOKEN_ID, VALUE); + + assert_only_event_transfer(ZERO(), ZERO(), ZERO(), OWNER(), TOKEN_ID, VALUE); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), VALUE); + assert_eq!(state.total_supply(TOKEN_ID), VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: mint to 0',))] +fn test__mint_to_zero() { + let mut state = COMPONENT_STATE(); + state.mint(ZERO(), TOKEN_ID, VALUE); +} + +// +// _burn +// + +#[test] +fn test__burn() { + let mut state = setup(); + state.burn(OWNER(), TOKEN_ID, VALUE); + + assert_only_event_transfer(ZERO(), ZERO(), OWNER(), ZERO(), TOKEN_ID, VALUE); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.total_supply(TOKEN_ID), SUPPLY - VALUE); +} + +#[test] +#[should_panic(expected: ('ERC6909: burn from 0',))] +fn test__burn_from_zero() { + let mut state = setup(); + state.burn(ZERO(), TOKEN_ID, VALUE); +} + +// +// supports_interface +// +#[test] +fn test_set_supports_interface() { + let mut state = setup(); + // IERC6909_ID as defined in `interface.cairo` = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee + assert!( + state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee) + ); + assert_eq!(state.supports_interface(0x32cb), false); + assert_eq!( + state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ef), + false + ); + assert!(state.supports_interface(ISRC5_ID)) +} + + +// +// is_operator & set_operator +// + +#[test] +fn test_transfer_from_caller_is_operator() { + let mut state = setup(); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); + assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), 0); + assert_eq!(state.is_operator(OWNER(), OPERATOR()), false); + + testing::set_caller_address(OWNER()); + state.set_operator(OPERATOR(), true); + + assert_only_event_operator_set(ZERO(), OWNER(), OPERATOR(), true); + + testing::set_caller_address(OPERATOR()); + assert!(state.transfer_from(OWNER(), OPERATOR(), TOKEN_ID, VALUE)); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.balance_of(OPERATOR(), TOKEN_ID), VALUE); + assert!(state.is_operator(OWNER(), OPERATOR())); +} + +#[test] +fn test_set_operator() { + let mut state = setup(); + assert_eq!(state.is_operator(OWNER(), OPERATOR()), false); + + testing::set_caller_address(OWNER()); + state.set_operator(OPERATOR(), true); + + assert_only_event_operator_set(ZERO(), OWNER(), OPERATOR(), true); + assert!(state.is_operator(OWNER(), OPERATOR())); +} + +#[test] +fn test_set_operator_false() { + let mut state = setup(); + assert_eq!(state.is_operator(OWNER(), OPERATOR()), false); + + testing::set_caller_address(OWNER()); + state.set_operator(OPERATOR(), true); + assert_only_event_operator_set(ZERO(), OWNER(), OPERATOR(), true); + assert!(state.is_operator(OWNER(), OPERATOR())); + + testing::set_caller_address(OWNER()); + state.set_operator(OPERATOR(), false); + assert_only_event_operator_set(ZERO(), OWNER(), OPERATOR(), false); + assert_eq!(state.is_operator(OWNER(), OPERATOR()), false); +} + +#[test] +fn test_operator_does_not_deduct_allowance() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + state.approve(OPERATOR(), TOKEN_ID, 1); + assert_eq!(state.allowance(OWNER(), OPERATOR(), TOKEN_ID), 1); + assert_event_approval(ZERO(), OWNER(), OPERATOR(), TOKEN_ID, 1); + + testing::set_caller_address(OWNER()); + state.set_operator(OPERATOR(), true); + assert!(state.is_operator(OWNER(), OPERATOR())); + assert_event_operator_set(ZERO(), OWNER(), OPERATOR(), true); + + testing::set_caller_address(OPERATOR()); + assert!(state.transfer_from(OWNER(), OPERATOR(), TOKEN_ID, 1)); + assert_only_event_transfer(ZERO(), OPERATOR(), OWNER(), OPERATOR(), TOKEN_ID, 1); + + assert_eq!(state.allowance(OWNER(), OPERATOR(), TOKEN_ID), 1); + assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - 1); + assert_eq!(state.balance_of(OPERATOR(), TOKEN_ID), 1); +} + +#[test] +fn test_self_set_operator() { + let mut state = setup(); + assert_eq!(state.is_operator(OWNER(), OWNER()), false); + testing::set_caller_address(OWNER()); + state.set_operator(OWNER(), true); + assert!(state.is_operator(OWNER(), OWNER())); +} From 0d766fb8b1371efa4aa781601bea0d120a054b90 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 21 Jun 2024 13:39:32 +0200 Subject: [PATCH 02/44] add docs and changelog --- CHANGELOG.md | 5 +- docs/modules/ROOT/nav.adoc | 2 + docs/modules/ROOT/pages/api/erc6909.adoc | 748 +++++++++++++++++++++++ docs/modules/ROOT/pages/erc6909.adoc | 232 +++++++ packages/token/src/erc6909/erc6909.cairo | 18 +- 5 files changed, 1003 insertions(+), 2 deletions(-) create mode 100644 docs/modules/ROOT/pages/api/erc6909.adoc create mode 100644 docs/modules/ROOT/pages/erc6909.adoc diff --git a/CHANGELOG.md b/CHANGELOG.md index 73281cc2c..91abcd432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- ERC-6909: interfaces, components, tests, and docs. (initial contribution by @swan-of-bodom; rebased to current main) + ## 3.0.0-alpha.3 (2025-10-9) ### Added @@ -499,4 +502,4 @@ with new external functions (#1173) - Account events indexed keys (#853) - Support higher tx versions in Account (#858) - Bump scarb to v2.4.1 (#858) -- Add security section to Upgrades docs (#861) +- Add security section to Upgrades docs (#861) \ No newline at end of file diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index ce7efc7df..d236bfbab 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -53,6 +53,8 @@ **** xref:/api/erc1155.adoc[API Reference] *** xref:erc4626.adoc[ERC4626] **** xref:/api/erc20.adoc#ERC4626Component[API Reference] +*** xref:erc6909.adoc[ERC6909] +**** xref:/api/erc6909.adoc[API Reference] *** xref:/api/token_common.adoc[Common] ** xref:udc.adoc[Universal Deployer Contract] diff --git a/docs/modules/ROOT/pages/api/erc6909.adoc b/docs/modules/ROOT/pages/api/erc6909.adoc new file mode 100644 index 000000000..20407e46d --- /dev/null +++ b/docs/modules/ROOT/pages/api/erc6909.adoc @@ -0,0 +1,748 @@ +:github-icon: pass:[] +:eip6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] +:erc6909-guide: xref:erc6909.adoc[ERC6909 guide] + += ERC6909 + +include::../utils/_common.adoc[] + +Reference of interfaces and utilities related to ERC6909 contracts. + +TIP: For an overview of ERC6909, read our {erc6909-guide}. + +== Core + +[.contract] +[[IERC6909]] +=== `++IERC6909++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/interface.cairo[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```cairo +use openzeppelin::token::erc6909::interface::IERC6909; +``` + +Interface of the IERC6909 standard as defined in {eip6909}. + +[.contract-index] +.Functions +-- +* xref:#IERC6909-balance_of[`++balance_of(owner, id)++`] +* xref:#IERC6909-allowance[`++allowance(owner, spender, id)++`] +* xref:#IERC6909-is_operator[`++is_operator(owner, spender)++`] +* xref:#IERC6909-transfer[`++transfer(receiver, id, amount)++`] +* xref:#IERC6909-transfer_from[`++transfer_from(sender, receiver, id, amount)++`] +* xref:#IERC6909-approve[`++approve(spender, id, amount)++`] +* xref:#IERC6909-set_operator[`++set_operator(spender, approved)++`] +* xref:#IERC6909-supports_interface[`++supports_interface(interface_id)++`] +-- + +[.contract-index] +.Events +-- +* xref:#IERC6909-Transfer[`++Transfer(caller, sender, receiver, id, amount)++`] +* xref:#IERC6909-Approval[`++Approval(owner, spender, id, amount)++`] +* xref:#IERC6909-OperatorSet[`++OperatorSet(owner, spender, approved)++`] +-- + +[#IERC6909-Functions] +==== Functions + +[.contract-item] +[[IERC6909-balance_of]] +==== `[.contract-item-name]#++balance_of++#++(owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# + +Returns the amount owned by `owner` of `id`. + +[.contract-item] +[[IERC6909-allowance]] +==== `[.contract-item-name]#++allowance++#++(owner: ContractAddress, spender: ContractAddress, id: u256) → u256++` [.item-kind]#external# + +Returns the remaining number of `id` tokens that `spender` is allowed to spend on behalf of `owner` through <>. This is zero by default. + +This value changes when <> or <> are called. + +[.contract-item] +[[IERC6909-is_operator]] +==== `[.contract-item-name]#++is_operator++#++(owner: ContractAddress, spender: ContractAddress) → bool++` [.item-kind]#external# + +Checks if a `spender` is approved by an `owner` as an operator. Operators are not subject to allowance restrictions. + +[.contract-item] +[[IERC6909-transfer]] +==== `[.contract-item-name]#++transfer++#++(receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# + +Moves `amount` of an `id` from the caller's token balance to `receiver`. +Returns `true` on success, reverts otherwise. + +Emits a <> event. + +[.contract-item] +[[IERC6909-transfer_from]] +==== `[.contract-item-name]#++transfer_from++#++(sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# + +Moves `amount` of an `id` from `sender` to `receiver` using the allowance mechanism. +`amount` is then deducted from the caller's allowance, unless called by an operator. +Returns `true` on success, reverts otherwise. + +Emits a <> event. + +[.contract-item] +[[IERC6909-approve]] +==== `[.contract-item-name]#++approve++#++(spender: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# + +Sets `amount` as the allowance of `spender` over the caller's `id`. +Returns `true` on success, reverts otherwise. + +Emits an <> event. + +[.contract-item] +[[IERC6909-set_operator]] +==== `[.contract-item-name]#++set_operator++#++(spender: ContractAddress, approved: bool) → bool++` [.item-kind]#external# + +Sets or unsets `spender` as an operator for the caller. + +Emits an <> event. + +[.contract-item] +[[IERC6909-set_operator]] +==== `[.contract-item-name]#++supports_interface++#++(interface_id: felt252) → bool++` [.item-kind]#external# + +Checks if a contract implements `interface_id`. + +[#IERC6909-Events] +==== Events + +[.contract-item] +[[IERC6909-Transfer]] +==== `[.contract-item-name]#++Transfer++#++(caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# + +Emitted when `amount` of `id` are moved from one address (`sender`) to another (`receiver`). + +Note that `amount` may be zero. + +[.contract-item] +[[IERC6909-Approval]] +==== `[.contract-item-name]#++Approval++#++(owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# + +Emitted when the allowance of a `spender` for an `owner` is set over a token `id`. +`amount` is the new allowance. + +[.contract-item] +[[IERC6909-OperatorSet]] +==== `[.contract-item-name]#++OperatorSet++#++(owner: ContractAddress, spender: ContractAddress, approved: bool)++` [.item-kind]#event# + +Emitted when an operator (`spender`) is set or unset for `owner`. `approved` is the new status of the operator. + +// [.contract] +// [[IERC6909Metadata]] +// === `++IERC6909Metadata++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc20/interface.cairo#L19[{github-icon},role=heading-link] +// +// [.hljs-theme-dark] +// ```cairo +// use openzeppelin::token::erc20::interface::IERC6909Metadata; +// ``` +// +// Interface for the optional metadata functions in {eip20}. +// +// [.contract-index] +// .Functions +// -- +// * xref:#IERC6909Metadata-name[`++name()++`] +// * xref:#IERC6909Metadata-symbol[`++symbol()++`] +// * xref:#IERC6909Metadata-decimals[`++decimals()++`] +// -- +// +// [#IERC6909Metadata-Functions] +// ==== Functions +// +// [.contract-item] +// [[IERC6909Metadata-name]] +// ==== `[.contract-item-name]#++name++#++() → ByteArray++` [.item-kind]#external# +// +// Returns the name of the token. +// +// [.contract-item] +// [[IERC6909Metadata-symbol]] +// ==== `[.contract-item-name]#++symbol++#++() → ByteArray++` [.item-kind]#external# +// +// Returns the ticker symbol of the token. +// +// [.contract-item] +// [[IERC6909Metadata-decimals]] +// ==== `[.contract-item-name]#++decimals++#++() → u8++` [.item-kind]#external# +// +// Returns the number of decimals the token uses - e.g. `8` means to divide the token amount by `100000000` to get its user-readable representation. +// +// For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). +// +// Tokens usually opt for a value of `18`, imitating the relationship between Ether and Wei. +// This is the default value returned by this function. +// To create a custom decimals implementation, see {custom-decimals}. +// +// NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract. + +[.contract] +[[ERC6909Component]] +=== `++ERC6909Component++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/erc6909.cairo[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```cairo +use openzeppelin::token::erc6909::ERC6909Component; +``` +ERC6909 component extending <>. + +NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how are hooks used. + +[.contract-index] +.Hooks +-- +[.sub-index#ERC6909Component-ERC6909HooksTrait] +.ERC6909HooksTrait +* xref:#ERC6909Component-before_update[`++before_update(self, from, recipient, id, amount)++`] +* xref:#ERC6909Component-after_update[`++after_update(self, from, recipient, id, amount)++`] +-- + +[.contract-index#ERC6909Component-Embeddable-Mixin-Impl] +.{mixin-impls} +-- +.ERC6909MixinImpl +* xref:#ERC6909Component-Embeddable-Impls-ERC6909Impl[`++ERC6909Impl++`] +* xref:#ERC6909Component-Embeddable-Impls-ERC6909CamelOnlyImpl[`++ERC6909CamelOnlyImpl++`] +// * xref:#ERC6909Component-Embeddable-Impls-ERC6909MetadataImpl[`++ERC6909MetadataImpl++`] +-- + +[.contract-index#ERC6909Component-Embeddable-Impls] +.Embeddable Implementations +-- +[.sub-index#ERC6909Component-Embeddable-Impls-ERC6909Impl] +.ERC6909Impl +* xref:#ERC6909Component-balance_of[`++balance_of(self, owner, id)++`] +* xref:#ERC6909Component-allowance[`++allowance(self, owner, spender, id)++`] +* xref:#ERC6909Component-is_operator[`++is_operator(self, owner, spender)++`] +* xref:#ERC6909Component-transfer[`++transfer(self, receiver, id, amount)++`] +* xref:#ERC6909Component-transfer_from[`++transfer_from(self, sender, receiver, id, amount)++`] +* xref:#ERC6909Component-approve[`++approve(self, spender, id, amount)++`] +* xref:#ERC6909Component-set_operator[`++set_operator(self, spender, approved)++`] +* xref:#ERC6909Component-supports_interface[`++supports_interface(self, interface_id)++`] + +// [.sub-index#ERC6909Component-Embeddable-Impls-ERC6909MetadataImpl] +// .ERC6909MetadataImpl +// * xref:#ERC6909Component-name[`++name(self)++`] +// * xref:#ERC6909Component-symbol[`++symbol(self)++`] +// * xref:#ERC6909Component-decimals[`++decimals(self)++`] + +[.sub-index#ERC6909Component-Embeddable-Impls-ERC6909CamelOnlyImpl] +.ERC6909CamelOnlyImpl +* xref:#ERC6909Component-balanceOf[`++balanceOf(self, owner, id)++`] +* xref:#ERC6909Component-isOperator[`++isOperator(self, owner, spender)++`] +* xref:#ERC6909Component-transferFrom[`++transferFrom(self, sender, receiver, id, amount)++`] +* xref:#ERC6909Component-setOperator[`++setOperator(self, spender, approved)++`] +* xref:#ERC6909Component-supportsInterface[`++supportsInterface(self, interface_id)++`] +-- + +[.contract-index] +.Internal implementations +-- +.InternalImpl +* xref:#ERC6909Component-mint[`++mint(self, receiver, id, amount)++`] +* xref:#ERC6909Component-burn[`++burn(self, account, id, amount)++`] +* xref:#ERC6909Component-update[`++update(self, caller, sender, receiver, id, amount)++`] +* xref:#ERC6909Component-_transfer[`++_transfer(self, sender, receiver, id, amount)++`] +* xref:#ERC6909Component-_approve[`++_approve(self, owner, spender, id, amount)++`] +* xref:#ERC6909Component-_spend_allowance[`++_spend_allowance(self, owner, spender, id, amount)++`] +* xref:#ERC6909Component-_set_contract_uri[`++_set_contract_uri(self, contract_uri)++`] +* xref:#ERC6909Component-_set_token_name[`++_set_token_name(self, id, name)++`] +* xref:#ERC6909Component-_set_token_symbol[`++_set_token_symbol(self, id, symbol)++`] +* xref:#ERC6909Component-_set_token_decimals[`++_set_token_decimals(self, id, decimals)++`] +* xref:#ERC6909Component-_set_operator[`++_set_operator(self, owner, spender, approved)++`] +-- + +[.contract-index] +.Events +-- +* xref:#ERC6909Component-Transfer[`++Transfer(caller, sender, receiver, id, amount)++`] +* xref:#ERC6909Component-Approval[`++Approval(owner, spender, id, amount)++`] +* xref:#ERC6909Component-OperatorSet[`++OperatorSet(owner, spender, approved)++`] +-- + +[#ERC6909Component-Hooks] +==== Hooks + +Hooks are functions which implementations can extend the functionality of the component source code. Every contract +using ERC6909Component is expected to provide an implementation of the ERC6909HooksTrait. For basic token contracts, an +empty implementation with no logic must be provided. + +TIP: You can use `openzeppelin::token::erc6909::ERC6909HooksEmptyImpl` which is already available as part of the library +for this purpose. + +[.contract-item] +[[ERC6909Component-before_update]] +==== `[.contract-item-name]#++before_update++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#hook# + +Function executed at the beginning of the xref:#ERC6909Component-update[update] function prior to any other logic. + +[.contract-item] +[[ERC6909Component-after_update]] +==== `[.contract-item-name]#++after_update++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#hook# + +Function executed at the end of the xref:#ERC6909Component-update[update] function. + +[#ERC6909Component-Embeddable-functions] +==== Embeddable functions + +//[.contract-item] +//[[ERC6909Component-total_supply]] +//==== `[.contract-item-name]#++total_supply++#++(@self: ContractState, id: u256) → u256++` [.item-kind]#external# +// +//See <>. + +[.contract-item] +[[ERC6909Component-balance_of]] +==== `[.contract-item-name]#++balance_of++#++(@self: ContractState, account: ContractAddress, id: u256) → u256++` [.item-kind]#external# + +See <>. + +[.contract-item] +[[ERC6909Component-allowance]] +==== `[.contract-item-name]#++allowance++#++(@self: ContractState, owner: ContractAddress, spender: ContractAddress, id: u256) → u256++` [.item-kind]#external# + +See <>. + +[.contract-item] +[[ERC6909Component-transfer]] +==== `[.contract-item-name]#++transfer++#++(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# + +See <>. + +Requirements: + +- `receiver` cannot be the zero address. +- The caller must have a balance of `id` of at least `amount`. + +[.contract-item] +[[ERC6909Component-transfer_from]] +==== `[.contract-item-name]#++transfer_from++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# + +See <>. + +Requirements: + +- `sender` cannot be the zero address. +- `sender` must have a balance of `id` of at least `amount`. +- `receiver` cannot be the zero address. +- The caller must have allowance of `id` for ``sender``'s tokens of at least `amount` or be an operator. + +[.contract-item] +[[ERC6909Component-approve]] +==== `[.contract-item-name]#++approve++#++(ref self: ContractState, spender: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# + +See <>. + +Requirements: + +- `spender` cannot be the zero address. + +//[.contract-item] +//[[ERC6909Component-name]] +//==== `[.contract-item-name]#++name++#++() → ByteArray++` [.item-kind]#external# +// +//See <>. +// +//[.contract-item] +//[[ERC6909Component-symbol]] +//==== `[.contract-item-name]#++symbol++#++() → ByteArray++` [.item-kind]#external# +// +//See <>. +// +//[.contract-item] +//[[ERC6909Component-decimals]] +//==== `[.contract-item-name]#++decimals++#++() → u8++` [.item-kind]#external# +// +//See <>. +// +//[.contract-item] +//[[ERC6909Component-totalSupply]] +//==== `[.contract-item-name]#++totalSupply++#++(self: @ContractState) → u256++` [.item-kind]#external# +// +//See <>. +// +//Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. + +[.contract-item] +[[ERC6909Component-balanceOf]] +==== `[.contract-item-name]#++balanceOf++#++(self: @ContractState, owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# + +See <>. + +Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. + +[.contract-item] +[[ERC6909Component-transferFrom]] +==== `[.contract-item-name]#++transferFrom++#++(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# + +See <>. + +Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. + +[#ERC6909Component-Internal-functions] +==== Internal functions + +[.contract-item] +[[ERC6909Component-mint]] +==== `[.contract-item-name]#++mint++#++(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +Creates an `amount` number of `id` tokens and assigns them to `receiver`. + +Emits a <> event with `sender` being the zero address. + +Requirements: + +- `receiver` cannot be the zero address. + +[.contract-item] +[[ERC6909Component-burn]] +==== `[.contract-item-name]#++burn++#++(ref self: ContractState, account: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +Destroys `amount` number of `id` tokens from `account`. + +Emits a <> event with `receiver` set to the zero address. + +Requirements: + +- `account` cannot be the zero address. + +[.contract-item] +[[ERC6909Component-update]] +==== `[.contract-item-name]#++update++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or burns) if `sender` (or `receiver`) is +the zero address. + +NOTE: This function can be extended using the xref:ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait], to add +functionality before and/or after the transfer, mint, or burn. + +Emits a <> event. + +[.contract-item] +[[ERC6909Component-_transfer]] +==== `[.contract-item-name]#++_transfer++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +Moves `amount` of `id` tokens from `sender` to `receiver`. + +This internal function does not check for access permissions but can be useful as a building block, for example to implement automatic token fees, slashing mechanisms, etc. + +Emits a <> event. + +Requirements: + +- `sender` cannot be the zero address. +- `receiver` cannot be the zero address. +- `sender` must have a balance of `id` tokens of at least `amount`. + +[.contract-item] +[[ERC6909Component-_approve]] +==== `[.contract-item-name]#++_approve++#++(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +Sets `amount` as the allowance of `spender` over ``owner``'s `id` tokens. + +This internal function does not check for access permissions but can be useful as a building block, for example to implement automatic allowances on behalf of other addresses. + +Emits an <> event. + +Requirements: + +- `owner` cannot be the zero address. +- `spender` cannot be the zero address. + +[.contract-item] +[[ERC6909Component-_spend_allowance]] +==== `[.contract-item-name]#++_spend_allowance++#++(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +Updates ``owner``'s allowance for `spender` for `id` token based on spent `amount`. + +This internal function does not update the allowance value in the case of infinite allowance or if called by an operator. + +Possibly emits an <> event. + +[#ERC6909Component-Events] +==== Events + +[.contract-item] +[[ERC6909Component-Transfer]] +==== `[.contract-item-name]#++Transfer++#++(caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# + +See <>. + +[.contract-item] +[[ERC6909Component-Approval]] +==== `[.contract-item-name]#++Approval++#++(owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# + +See <>. + +[.contract-item] +[[ERC6909Component-OperatorSet]] +==== `[.contract-item-name]#++OperatorSet++#++(owner: ContractAddress, spender: ContractAddress, approved: bool)++` [.item-kind]#event# + +See <>. + +== Extensions + +//[.contract] +//[[ERC6909VotesComponent]] +//=== `++ERC6909VotesComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc20/extensions/erc20_votes.cairo[{github-icon},role=heading-link] +// +//```cairo +//use openzeppelin::token::extensions::ERC6909VotesComponent; +//``` +// +//:DelegateChanged: xref:ERC6909VotesComponent-DelegateChanged[DelegateChanged] +//:DelegateVotesChanged: xref:ERC6909VotesComponent-DelegateVotesChanged[DelegateVotesChanged] +// +//Extension of ERC6909 to support voting and delegation. +// +//NOTE: Implementing xref:#ERC6909Component[ERC6909Component] is a requirement for this component to be implemented. +// +//WARNING: To track voting units, this extension requires that the +//xref:#ERC6909VotesComponent-transfer_voting_units[transfer_voting_units] function is called after every transfer, +//mint, or burn operation. For this, the xref:ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait] must be used. +// +// +//This extension keeps a history (checkpoints) of each account’s vote power. Vote power can be delegated either by calling +//the xref:#ERC6909VotesComponent-delegate[delegate] function directly, or by providing a signature to be used with +//xref:#ERC6909VotesComponent-delegate_by_sig[delegate_by_sig]. Voting power can be queried through the public accessors +//xref:#ERC6909VotesComponent-get_votes[get_votes] and xref:#ERC6909VotesComponent-get_past_votes[get_past_votes]. +// +//By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. +// +//[.contract-index#ERC6909VotesComponent-Embeddable-Impls] +//.Embeddable Implementations +//-- +//[.sub-index#ERC6909VotesComponent-Embeddable-Impls-ERC6909VotesImpl] +//.ERC6909VotesImpl +//* xref:#ERC6909VotesComponent-get_votes[`++get_votes(self, account)++`] +//* xref:#ERC6909VotesComponent-get_past_votes[`++get_past_votes(self, account, timepoint)++`] +//* xref:#ERC6909VotesComponent-get_past_total_supply[`++get_past_total_supply(self, timepoint)++`] +//* xref:#ERC6909VotesComponent-delegates[`++delegates(self, account)++`] +//* xref:#ERC6909VotesComponent-delegate[`++delegate(self, delegatee)++`] +//* xref:#ERC6909VotesComponent-delegate_by_sig[`++delegate_by_sig(self, delegator, delegatee, nonce, expiry, signature)++`] +//-- +// +//[.contract-index] +//.Internal implementations +//-- +//.InternalImpl +//* xref:#ERC6909VotesComponent-get_total_supply[`++get_total_supply(self)++`] +//* xref:#ERC6909VotesComponent-_delegate[`++_delegate(self, account, delegatee)++`] +//* xref:#ERC6909VotesComponent-move_delegate_votes[`++move_delegate_votes(self, from, to, amount)++`] +//* xref:#ERC6909VotesComponent-transfer_voting_units[`++transfer_voting_units(self, from, to, amount)++`] +//* xref:#ERC6909VotesComponent-checkpoints[`++checkpoints(self, account, pos)++`] +//* xref:#ERC6909VotesComponent-get_voting_units[`++get_voting_units(self, account)++`] +//-- +// +//[.contract-index] +//.Events +//-- +//* xref:#ERC6909VotesComponent-DelegateChanged[`++DelegateChanged(delegator, from_delegate, to_delegate)++`] +//* xref:#ERC6909VotesComponent-DelegateVotesChanged[`++DelegateVotesChanged(delegate, previous_votes, new_votes)++`] +//-- +// +//[#ERC6909VotesComponent-Embeddable-functions] +//==== Embeddable functions +// +//[.contract-item] +//[[ERC6909VotesComponent-get_votes]] +//==== `[.contract-item-name]#++get_votes++#++(self: @ContractState, account: ContractAddress) → u256++` [.item-kind]#external# +// +//Returns the current amount of votes that `account` has. +// +//[.contract-item] +//[[ERC6909VotesComponent-get_past_votes]] +//==== `[.contract-item-name]#++get_past_votes++#++(self: @ContractState, account: ContractAddress, timepoint: u64) → u256++` [.item-kind]#external# +// +//Returns the amount of votes that `account` had at a specific moment in the past. +// +//Requirements: +// +//- `timepoint` must be in the past. +// +//[.contract-item] +//[[ERC6909VotesComponent-get_past_total_supply]] +//==== `[.contract-item-name]#++get_past_total_supply++#++(self: @ContractState, timepoint: u64) → u256++` [.item-kind]#external# +// +//Returns the total supply of votes available at a specific moment in the past. +// +//NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. +//Votes that have not been delegated are still part of total supply, even though they would not participate in a +//vote. +// +//[.contract-item] +//[[ERC6909VotesComponent-delegates]] +//==== `[.contract-item-name]#++delegates++#++(self: @ContractState, account: ContractAddress) → ContractAddress++` [.item-kind]#external# +// +//Returns the delegate that `account` has chosen. +// +//[.contract-item] +//[[ERC6909VotesComponent-delegate]] +//==== `[.contract-item-name]#++delegate++#++(ref self: ContractState, delegatee: ContractAddress)++` [.item-kind]#external# +// +//Delegates votes from the caller to `delegatee`. +// +//Emits a {DelegateChanged} event. +// +//May emit one or two {DelegateVotesChanged} events. +// +//[.contract-item] +//[[ERC6909VotesComponent-delegate_by_sig]] +//==== `[.contract-item-name]#++delegate_by_sig++#++(ref self: ContractState, delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Array)++` [.item-kind]#external# +// +//Delegates votes from `delegator` to `delegatee` through a SNIP12 message signature validation. +// +//Requirements: +// +//- `expiry` must not be in the past. +//- `nonce` must match the account's current nonce. +//- `delegator` must implement `SRC6::is_valid_signature`. +//- `signature` should be valid for the message hash. +// +//Emits a {DelegateChanged} event. +// +//May emit one or two {DelegateVotesChanged} events. +// +//[#ERC6909VotesComponent-Internal-functions] +//==== Internal functions +// +//[.contract-item] +//[[ERC6909VotesComponent-get_total_supply]] +//==== `[.contract-item-name]#++get_total_supply++#++(self: @ContractState) → u256++` [.item-kind]#internal# +// +//Returns the current total supply of votes. +// +//[.contract-item] +//[[ERC6909VotesComponent-_delegate]] +//==== `[.contract-item-name]#++_delegate++#++(ref self: ContractState, account: ContractAddress, delegatee: ContractAddress)++` [.item-kind]#internal# +// +//Delegates all of ``account``'s voting units to `delegatee`. +// +//Emits a {DelegateChanged} event. +// +//May emit one or two {DelegateVotesChanged} events. +// +//[.contract-item] +//[[ERC6909VotesComponent-move_delegate_votes]] +//==== `[.contract-item-name]#++move_delegate_votes++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# +// +//Moves `amount` of delegated votes from `from` to `to`. +// +//May emit one or two {DelegateVotesChanged} events. +// +//[.contract-item] +//[[ERC6909VotesComponent-transfer_voting_units]] +//==== `[.contract-item-name]#++transfer_voting_units++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# +// +//Transfers, mints, or burns voting units. +// +//To register a mint, `from` should be zero. To register a burn, `to` +//should be zero. Total supply of voting units will be adjusted with mints and burns. +// +//May emit one or two {DelegateVotesChanged} events. +// +//[.contract-item] +//[[ERC6909VotesComponent-num_checkpoints]] +//==== `[.contract-item-name]#++num_checkpoints++#++(self: @ContractState, account: ContractAddress) → u32++` [.item-kind]#internal# +// +//Returns the number of checkpoints for `account`. +// +//[.contract-item] +//[[ERC6909VotesComponent-checkpoints]] +//==== `[.contract-item-name]#++checkpoints++#++(self: @ContractState, account: ContractAddress, pos: u32) → Checkpoint++` [.item-kind]#internal# +// +//Returns the `pos`-th checkpoint for `account`. +// +//[.contract-item] +//[[ERC6909VotesComponent-get_voting_units]] +//==== `[.contract-item-name]#++get_voting_units++#++(self: @ContractState, account: ContractAddress) → u256++` [.item-kind]#internal# +// +//Returns the voting units of an `account`. +// +//[#ERC6909VotesComponent-Events] +//==== Events +// +//[.contract-item] +//[[ERC6909VotesComponent-DelegateChanged]] +//==== `[.contract-item-name]#++DelegateChanged++#++(delegator: ContractAddress, from_delegate: ContractAddress, to_delegate: ContractAddress)++` [.item-kind]#event# +// +//Emitted when `delegator` delegates their votes from `from_delegate` to `to_delegate`. +// +//[.contract-item] +//[[ERC6909VotesComponent-DelegateVotesChanged]] +//==== `[.contract-item-name]#++DelegateVotesChanged++#++(delegate: ContractAddress, previous_votes: u256, new_votes: u256)++` [.item-kind]#event# +// +//Emitted when `delegate` votes are updated from `previous_votes` to `new_votes`. +// +//== Presets +// +//[.contract] +//[[ERC6909Upgradeable]] +//=== `++ERC6909Upgradeable++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/presets/erc20.cairo[{github-icon},role=heading-link] +// +//```cairo +//use openzeppelin::presets::ERC6909Upgradeable; +//``` +// +//Upgradeable ERC6909 contract leveraging xref:#ERC6909Component[ERC6909Component] with a fixed-supply mechanism for token distribution. +// +//include::../utils/_class_hashes.adoc[] +// +//[.contract-index] +//.{presets-page} +//-- +//{ERC6909Upgradeable-class-hash} +//-- +// +//[.contract-index] +//.Constructor +//-- +//* xref:#ERC6909Upgradeable-constructor[`++constructor(self, name, symbol, fixed_supply, recipient, owner)++`] +//-- +// +//[.contract-index] +//.Embedded Implementations +//-- +//.ERC6909MixinImpl +// +//* xref:#ERC6909Component-Embeddable-Mixin-Impl[`++ERC6909MixinImpl++`] +// +//.OwnableMixinImpl +// +//* xref:/api/access.adoc#OwnableComponent-Mixin-Impl[`++OwnableMixinImpl++`] +//-- +// +//[.contract-index] +//.External Functions +//-- +//* xref:#ERC6909Upgradeable-upgrade[`++upgrade(self, new_class_hash)++`] +//-- +// +//[#ERC6909Upgradeable-constructor-section] +//==== Constructor +// +//[.contract-item] +//[[ERC6909Upgradeable-constructor]] +//==== `[.contract-item-name]#++constructor++#++(ref self: ContractState, name: ByteArray, symbol: ByteArray, fixed_supply: u256, recipient: ContractAddress, owner: ContractAddress)++` [.item-kind]#constructor# +// +//Sets the `name` and `symbol` and mints `fixed_supply` tokens to `recipient`. +//Assigns `owner` as the contract owner with permissions to upgrade. +// +//[#ERC6909Upgradeable-external-functions] +//==== External functions +// +//[.contract-item] +//[[ERC6909Upgradeable-upgrade]] +//==== `[.contract-item-name]#++upgrade++#++(ref self: ContractState, new_class_hash: ClassHash)++` [.item-kind]#external# +// +//Upgrades the contract to a new implementation given by `new_class_hash`. +// +//Requirements: +// +//- The caller is the contract owner. +//- `new_class_hash` cannot be zero. diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc new file mode 100644 index 000000000..aca94e72d --- /dev/null +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -0,0 +1,232 @@ +:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] +:fungibility-agnostic: https://docs.openzeppelin.com/contracts/5.x/tokens#different-kinds-of-tokens[fungibility-agnostic] +:solidity-implementation: https://github.com/jtriley-eth/ERC-6909/tree/main/src + += ERC6909 + +The ERC6909 minimal multi token standard is a specification for {fungibility-agnostic} token contracts. +`token::erc6909::ERC6909Component` provides an approximation of {eip-6909} in Cairo for StarkNet. + +== Minimal Multi Token Standard + +Similar to ERC1155, it uses a single smart contract to represent multiple tokens at once via IDs. The main difference is +that callbacks and batching have been removed from the interface and the permission system is a hybrid operator-approval +scheme for granular and scalable permissions. Functionally, the interface has been reduced to the bare minimum +required to manage multiple tokens under the same contract. + +== Usage + +Using Contracts for Cairo, constructing an ERC6909 contract requires integrating the `ERC6909Component`. + +Since some functions commonly found on token standards (such as `total_supply` or metadata) are not part of the EIP, +the logic is implemented within the component but under separate modules for ease of use. Developers can choose which modules to +include in their contracts as they see fit. We followed the original {solidity-implementation} to replicate these in Cairo. + +Aside from the core `ERC6909`, the 3 optional modules that can be imported are: + +* `ERC6909ContentURI` +* `ERC6909TokenSupply` +* `ERC6909Metadata` + +Each module also has their camel counterparts: + +* `ERC6909ContentURICamel` +* `ERC6909TokenSupplyCamel` +* `ERC6909MetadataCamel` + +To create the contract URI, it can be set up in constructor by calling `_set_contract_uri`. + +[,cairo] +---- +#[starknet::contract] +mod MyToken { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + // ERC6909 Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + + // Optional to keep track of token supplies and URIs. + // In this case we only use the snake_case implementations. + #[abi(embed_v0)] + impl ERC6909TokenSupplyImpl = ERC6909Component::ERC6909TokenSupplyImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIImpl = ERC6909Component::ERC6909ContentURIImpl; + + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + recipient: ContractAddress, + token_id: u256, + initial_supply: u256, + contract_uri: ByteArray + ) { + self.erc6909._set_contract_uri(contract_uri); + self.erc6909.mint(recipient, token_id, initial_supply); + } +} +---- + +`MyToken` integrates the `ERC6909Impl`, `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` with the embed directives which marks the implementations as external in the contract. +While the `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` are optional, it's generally recommended to include them to keep track of individual token supplies and URI's. Metadata +is not part of the EIP, as such it is up to the developers to create this. +The above example also includes the `ERC6909InternalImpl` instance, allowing the contract's constructor to set the `contract_uri` and mint an initial supply of tokens. + +== Interface + +:dual-interfaces: xref:/interfaces.adoc#dual_interfaces[Dual interfaces] +:erc6909-component: xref:/api/erc6909.adoc#ERC6909Component[ERC6909Component] +:ierc6909-interface: xref:/api/erc6909.adoc#IERC6909[IERC6909] + +:ierc6909-supply: xref:/guides/ierc6909-supply.adoc[Creating ERC6909 Supply] +:ierc6909-content: xref:/guides/ierc6909-content.adoc[Creating ERC6909 Content URI] +:ierc6909-metadata: xref:/guides/erc6909-metadata.adoc[Creating ERC6909 Metadata] + +The following interface represents the full ABI of the Contracts for Cairo {erc6909-component}. + +[,cairo] +---- +#[starknet::interface] +pub trait ERC6909ABI { + /// @notice IERC6909 standard interface + fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; + fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + fn supports_interface(self: @TState, interface_id: felt252) -> bool; + + /// @notice IERC6909Camel + fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transferFrom( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + fn supportsInterface(self: @TState, interfaceId: felt252) -> bool; +} +---- + +== ERC6909 compatibility + +:cairo-selectors: https://github.com/starkware-libs/cairo/blob/7dd34f6c57b7baf5cd5a30c15e00af39cb26f7e1/crates/cairo-lang-starknet/src/contract.rs#L39-L48[Cairo] +:solidity-selectors: https://solidity-by-example.org/function-selector/[Solidity] +:dual-interface: xref:/interfaces.adoc#dual_interfaces[dual interface] + +Although Starknet is not EVM compatible, this component aims to be as close as possible to the ERC6909 token standard. +Some notable differences, however, can still be found, such as: + +* The `felt252` type is used to represent the `interfaceId`. +* The `ByteArray` type is used to represent strings such as in the `contract_uri`. +* The component offers a {dual-interface} which supports both snake_case and camelCase methods, as opposed to just camelCase in Solidity. +* `transfer`, `transfer_from` and `approve` will never return anything different from `true` because they will revert on any error. +* Function selectors are calculated differently between {cairo-selectors} and {solidity-selectors}. + +== Customizing Token Metadata + +Metadata is not required as per the EIP so it is included as a separate optional module. + +Since ERC6909 is a multi-token standard, instead of having a single `name`, `decimals`, and `symbol` functions for the entire token contract, +the optional module defines these metadata properties for each token ID individually. + +There are 3 internal methods which can be used to set individual id metadata: `_set_token_name(id, name)`, `_set_token_symbol(id, symbol)` and `_set_token_decimals(id, decimals)`. + +Developers can also just set a single `name`, `decimals` and `symbol` for the whole contract (just like in the ERC20 standard). + +[,cairo] +---- +#[starknet::contract] +mod MyToken { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + // ERC6909 Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + + // Optional to keep track of token supplies and URIs. + // In this case we only use the snake_case implementations. + #[abi(embed_v0)] + impl ERC6909TokenSupplyImpl = ERC6909Component::ERC6909TokenSupplyImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIImpl = ERC6909Component::ERC6909ContentURIImpl; + + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + recipient: ContractAddress, + token_id: u256, + initial_supply: u256, + contract_uri: ByteArray + ) { + self.erc6909._set_contract_uri(contract_uri); + self.erc6909.mint(recipient, token_id, initial_supply); + } + + #[abi(per_item)] + #[generate_trait] + impl MetadataImpl of MetadataTrait { + #[external(v0)] + fn name(self: @ContractState) -> ByteArray { + "MyToken" + } + + #[external(v0)] + fn symbol(self: @ContractState) -> ByteArray { + "MTK" + } + + #[external(v0)] + fn decimals(self: @ContractState) -> u8 { + 18 + } + } +} +---- + +== Storing ERC6909 URIs + +Token URI and Contract URI are also not part of the EIP. To implement these, the implementation `ERC6909ContentURIImpl` must be imported in the token contract. The contract URI +ideally would be initialized in the constructor via `_set_contract_uri` as shown above. + +The base URI is stored as a ByteArray and the full token URI is returned as the ByteArray concatenation of the base URI and the token ID through the token_uri method. +This design mirrors OpenZeppelin’s default Solidity implementation for ERC721. diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index acea3ae52..d2b3d3f71 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -17,6 +17,7 @@ pub mod ERC6909Component { struct Storage { ERC6909_name: LegacyMap, ERC6909_symbol: LegacyMap, + ERC6909_decimals: LegacyMap, ERC6909_balances: LegacyMap<(ContractAddress, u256), u256>, ERC6909_allowances: LegacyMap<(ContractAddress, ContractAddress, u256), u256>, ERC6909_operators: LegacyMap<(ContractAddress, ContractAddress), bool>, @@ -302,7 +303,7 @@ pub mod ERC6909Component { /// @param id The id of the token. /// @return The decimals of the token. fn decimals(self: @ComponentState, id: u256) -> u8 { - 18 + self.ERC6909_decimals.read(id) } } @@ -454,6 +455,21 @@ pub mod ERC6909Component { self.ERC6909_contract_uri.write(contract_uri); } + /// Sets the token name. + fn _set_token_name(ref self: ComponentState, id: u256, name: ByteArray) { + self.ERC6909_name.write(id, name); + } + + /// Sets the token symbol. + fn _set_token_symbol(ref self: ComponentState, id: u256, symbol: ByteArray) { + self.ERC6909_symbol.write(id, symbol); + } + + /// Sets the token decimals. + fn _set_token_decimals(ref self: ComponentState, id: u256, decimals: u8) { + self.ERC6909_decimals.write(id, decimals); + } + /// @notice Sets or unsets a spender as an operator for the caller. /// @param owner The address of the owner. /// @param spender The address of the spender. From ffd4a3801c39d7de04e2ce00866cc311075018f5 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Thu, 27 Jun 2024 13:20:05 +0200 Subject: [PATCH 03/44] edit `/docs/` --- docs/modules/ROOT/pages/erc6909.adoc | 25 +++++++++++++----------- packages/token/src/erc6909/erc6909.cairo | 16 ++++++++------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index aca94e72d..0d448525d 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -19,22 +19,24 @@ required to manage multiple tokens under the same contract. Using Contracts for Cairo, constructing an ERC6909 contract requires integrating the `ERC6909Component`. Since some functions commonly found on token standards (such as `total_supply` or metadata) are not part of the EIP, -the logic is implemented within the component but under separate modules for ease of use. Developers can choose which modules to -include in their contracts as they see fit. We followed the original {solidity-implementation} to replicate these in Cairo. +the logic for these are implemented under separate modules for ease of use. Developers can choose which modules to +include within their contracts as they see fit. We followed the original {solidity-implementation} to replicate these in Cairo. Aside from the core `ERC6909`, the 3 optional modules that can be imported are: +* `ERC6909Metadata` * `ERC6909ContentURI` * `ERC6909TokenSupply` -* `ERC6909Metadata` Each module also has their camel counterparts: +* `ERC6909MetadataCamel` * `ERC6909ContentURICamel` * `ERC6909TokenSupplyCamel` -* `ERC6909MetadataCamel` -To create the contract URI, it can be set up in constructor by calling `_set_contract_uri`. +To create the contract URI, it can be set up (ideally) in the constructor via `_set_contract_uri`. + +Here’s an example of a basic contract which includes the Content URI and Token Supply modules: [,cairo] ---- @@ -86,8 +88,7 @@ mod MyToken { ---- `MyToken` integrates the `ERC6909Impl`, `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` with the embed directives which marks the implementations as external in the contract. -While the `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` are optional, it's generally recommended to include them to keep track of individual token supplies and URI's. Metadata -is not part of the EIP, as such it is up to the developers to create this. +While the `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` are optional, it's generally recommended to include them to keep track of individual token supplies and URIs. The above example also includes the `ERC6909InternalImpl` instance, allowing the contract's constructor to set the `contract_uri` and mint an initial supply of tokens. == Interface @@ -102,6 +103,8 @@ The above example also includes the `ERC6909InternalImpl` instance, allowing the The following interface represents the full ABI of the Contracts for Cairo {erc6909-component}. +To support older token deployments, as mentioned in {dual-interfaces}, the component also includes an implementation of the interface written in camelCase. + [,cairo] ---- #[starknet::interface] @@ -134,15 +137,15 @@ pub trait ERC6909ABI { :cairo-selectors: https://github.com/starkware-libs/cairo/blob/7dd34f6c57b7baf5cd5a30c15e00af39cb26f7e1/crates/cairo-lang-starknet/src/contract.rs#L39-L48[Cairo] :solidity-selectors: https://solidity-by-example.org/function-selector/[Solidity] :dual-interface: xref:/interfaces.adoc#dual_interfaces[dual interface] +:interface-id: https://community.starknet.io/t/starknet-standard-interface-detection/92664/23[interface ID] Although Starknet is not EVM compatible, this component aims to be as close as possible to the ERC6909 token standard. Some notable differences, however, can still be found, such as: -* The `felt252` type is used to represent the `interfaceId`. -* The `ByteArray` type is used to represent strings such as in the `contract_uri`. +* The `ByteArray` type is used to represent strings in Cairo. +* The `felt252` type is used to represent the `byte4` interface ID. The {interface-id} is also calculated different in Cairo. * The component offers a {dual-interface} which supports both snake_case and camelCase methods, as opposed to just camelCase in Solidity. * `transfer`, `transfer_from` and `approve` will never return anything different from `true` because they will revert on any error. -* Function selectors are calculated differently between {cairo-selectors} and {solidity-selectors}. == Customizing Token Metadata @@ -153,7 +156,7 @@ the optional module defines these metadata properties for each token ID individu There are 3 internal methods which can be used to set individual id metadata: `_set_token_name(id, name)`, `_set_token_symbol(id, symbol)` and `_set_token_decimals(id, decimals)`. -Developers can also just set a single `name`, `decimals` and `symbol` for the whole contract (just like in the ERC20 standard). +Developers can also just set a single `name`, `decimals` and `symbol` for the whole contract which might prove to be simpler (just like in the ERC20 standard). [,cairo] ---- diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index d2b3d3f71..cbf565c7f 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -456,18 +456,20 @@ pub mod ERC6909Component { } /// Sets the token name. - fn _set_token_name(ref self: ComponentState, id: u256, name: ByteArray) { - self.ERC6909_name.write(id, name); + fn _set_token_name(ref self: ComponentState, id: u256, name: ByteArray) { + self.ERC6909_name.write(id, name); } /// Sets the token symbol. - fn _set_token_symbol(ref self: ComponentState, id: u256, symbol: ByteArray) { - self.ERC6909_symbol.write(id, symbol); + fn _set_token_symbol( + ref self: ComponentState, id: u256, symbol: ByteArray + ) { + self.ERC6909_symbol.write(id, symbol); } /// Sets the token decimals. - fn _set_token_decimals(ref self: ComponentState, id: u256, decimals: u8) { - self.ERC6909_decimals.write(id, decimals); + fn _set_token_decimals(ref self: ComponentState, id: u256, decimals: u8) { + self.ERC6909_decimals.write(id, decimals); } /// @notice Sets or unsets a spender as an operator for the caller. @@ -485,7 +487,7 @@ pub mod ERC6909Component { } /// Updates `sender`s allowance for `spender` and `id` based on spent `amount`. - /// Does not update the allowance value in case of infinite allowance. + /// Does not update the allowance value in case of infinite allowance or if spender is operator. fn _spend_allowance( ref self: ComponentState, sender: ContractAddress, From 5366a817f62a4359353ec76704bc9aaa806da38f Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 28 Jun 2024 23:33:03 +0200 Subject: [PATCH 04/44] docs --- docs/modules/ROOT/pages/erc6909.adoc | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index 0d448525d..db0008344 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -1,21 +1,22 @@ -:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] -:fungibility-agnostic: https://docs.openzeppelin.com/contracts/5.x/tokens#different-kinds-of-tokens[fungibility-agnostic] -:solidity-implementation: https://github.com/jtriley-eth/ERC-6909/tree/main/src - = ERC6909 +:fungibility-agnostic: https://docs.openzeppelin.com/contracts/5.x/tokens#different-kinds-of-tokens[fungibility-agnostic] +:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] + The ERC6909 minimal multi token standard is a specification for {fungibility-agnostic} token contracts. `token::erc6909::ERC6909Component` provides an approximation of {eip-6909} in Cairo for StarkNet. == Minimal Multi Token Standard Similar to ERC1155, it uses a single smart contract to represent multiple tokens at once via IDs. The main difference is -that callbacks and batching have been removed from the interface and the permission system is a hybrid operator-approval +that in ERC6909 the callbacks and batching have been removed from the interface and the permission system is a hybrid operator-approval scheme for granular and scalable permissions. Functionally, the interface has been reduced to the bare minimum required to manage multiple tokens under the same contract. == Usage +:solidity-implementation: https://github.com/jtriley-eth/ERC-6909/tree/main/src + Using Contracts for Cairo, constructing an ERC6909 contract requires integrating the `ERC6909Component`. Since some functions commonly found on token standards (such as `total_supply` or metadata) are not part of the EIP, @@ -24,17 +25,17 @@ include within their contracts as they see fit. We followed the original {solidi Aside from the core `ERC6909`, the 3 optional modules that can be imported are: -* `ERC6909Metadata` -* `ERC6909ContentURI` -* `ERC6909TokenSupply` +* `ERC6909Metadata` - Name, symbol, decimals of each token ID +* `ERC6909ContentURI` - The URI of the contract & each token ID +* `ERC6909TokenSupply` - The total supply of each token ID -Each module also has their camel counterparts: +Each module also has their camelCase counterparts: * `ERC6909MetadataCamel` * `ERC6909ContentURICamel` * `ERC6909TokenSupplyCamel` -To create the contract URI, it can be set up (ideally) in the constructor via `_set_contract_uri`. +The contract URI can be set up (ideally) in the constructor via `_set_contract_uri`. Here’s an example of a basic contract which includes the Content URI and Token Supply modules: @@ -87,21 +88,22 @@ mod MyToken { } ---- -`MyToken` integrates the `ERC6909Impl`, `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` with the embed directives which marks the implementations as external in the contract. +`MyToken` integrates the `ERC6909MixinImpl` along with the optional `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl`. The embed directives mark the implementations as external in the contract. While the `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` are optional, it's generally recommended to include them to keep track of individual token supplies and URIs. The above example also includes the `ERC6909InternalImpl` instance, allowing the contract's constructor to set the `contract_uri` and mint an initial supply of tokens. == Interface -:dual-interfaces: xref:/interfaces.adoc#dual_interfaces[Dual interfaces] :erc6909-component: xref:/api/erc6909.adoc#ERC6909Component[ERC6909Component] +:dual-interfaces: xref:/interfaces.adoc#dual_interfaces[Dual interfaces] :ierc6909-interface: xref:/api/erc6909.adoc#IERC6909[IERC6909] -:ierc6909-supply: xref:/guides/ierc6909-supply.adoc[Creating ERC6909 Supply] -:ierc6909-content: xref:/guides/ierc6909-content.adoc[Creating ERC6909 Content URI] -:ierc6909-metadata: xref:/guides/erc6909-metadata.adoc[Creating ERC6909 Metadata] +:ierc6909-supply: xref:/guides/ierc6909-supply.adoc[IERC6909TokenSupply] +:ierc6909-content: xref:/guides/ierc6909-content.adoc[IERC6909ContentURI] +:ierc6909-metadata: xref:/guides/erc6909-metadata.adoc[IERC6909Metadata] The following interface represents the full ABI of the Contracts for Cairo {erc6909-component}. +The interface includes the {ierc6909-interface} standard interface and the optional {ierc6909-metadata}, {ierc6909-supply} and {ierc6909-content}. To support older token deployments, as mentioned in {dual-interfaces}, the component also includes an implementation of the interface written in camelCase. @@ -149,10 +151,8 @@ Some notable differences, however, can still be found, such as: == Customizing Token Metadata -Metadata is not required as per the EIP so it is included as a separate optional module. - Since ERC6909 is a multi-token standard, instead of having a single `name`, `decimals`, and `symbol` functions for the entire token contract, -the optional module defines these metadata properties for each token ID individually. +the optional `IERC6909Metadata` module defines these metadata properties for each token ID individually. There are 3 internal methods which can be used to set individual id metadata: `_set_token_name(id, name)`, `_set_token_symbol(id, symbol)` and `_set_token_decimals(id, decimals)`. From 7f36ba2153ff5ce4d17c52bf184a7cbfc385cd93 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 2 Jul 2024 00:40:36 +0200 Subject: [PATCH 05/44] refactor `erc6909` to use metadata, supply and uri extensions instead Removed much of the logic for these extensions from the `ERC6909Component`. Now these are different components. Reason for this was to keep the original code as small as possible as metadata, supply and uri are not part of the EIP, but are just optional extensions --- .../test_common/src/mocks/erc6909_mocks.cairo | 30 +--- packages/token/src/erc6909.cairo | 1 + packages/token/src/erc6909/erc6909.cairo | 150 ++---------------- packages/token/src/erc6909/extensions.cairo | 7 + .../extensions/erc6909_content_uri.cairo | 60 +++++++ .../erc6909/extensions/erc6909_metadata.cairo | 76 +++++++++ .../extensions/erc6909_token_supply.cairo | 71 +++++++++ packages/token/src/erc6909/interface.cairo | 76 ++++----- 8 files changed, 267 insertions(+), 204 deletions(-) create mode 100644 packages/token/src/erc6909/extensions.cairo create mode 100644 packages/token/src/erc6909/extensions/erc6909_content_uri.cairo create mode 100644 packages/token/src/erc6909/extensions/erc6909_metadata.cairo create mode 100644 packages/token/src/erc6909/extensions/erc6909_token_supply.cairo diff --git a/packages/test_common/src/mocks/erc6909_mocks.cairo b/packages/test_common/src/mocks/erc6909_mocks.cairo index d600ce077..3091f38a9 100644 --- a/packages/test_common/src/mocks/erc6909_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_mocks.cairo @@ -10,20 +10,11 @@ pub(crate) mod DualCaseERC6909Mock { #[abi(embed_v0)] impl ERC6909Impl = ERC6909Component::ERC6909Impl; #[abi(embed_v0)] - impl ERC6909CamelOnlyImpl = - ERC6909Component::ERC6909CamelOnlyImpl; + impl ERC6909CamelOnlyImpl = ERC6909Component::ERC6909CamelOnlyImpl; #[abi(embed_v0)] - impl ERC6909TokenSupplyImpl = - ERC6909Component::ERC6909TokenSupplyImpl; + impl ERC6909TokenSupplyImpl = ERC6909Component::ERC6909TokenSupplyImpl; #[abi(embed_v0)] - impl ERC6909TokenSupplyCamelImpl = - ERC6909Component::ERC6909TokenSupplyCamelImpl; - #[abi(embed_v0)] - impl ERC6909ContentURIImpl = - ERC6909Component::ERC6909ContentURIImpl; - #[abi(embed_v0)] - impl ERC6909ContentURICamelImpl = - ERC6909Component::ERC6909ContentURICamelImpl; + impl ERC6909ContentURIImpl = ERC6909Component::ERC6909ContentURIImpl; /// Internal logic impl InternalImpl = ERC6909Component::InternalImpl; @@ -59,11 +50,9 @@ pub(crate) mod SnakeERC6909Mock { #[abi(embed_v0)] impl ERC6909Impl = ERC6909Component::ERC6909Impl; #[abi(embed_v0)] - impl ERC6909TokenSupplyImpl = - ERC6909Component::ERC6909TokenSupplyImpl; + impl ERC6909TokenSupplyImpl = ERC6909Component::ERC6909TokenSupplyImpl; #[abi(embed_v0)] - impl ERC6909ContentURIImpl = - ERC6909Component::ERC6909ContentURIImpl; + impl ERC6909ContentURIImpl = ERC6909Component::ERC6909ContentURIImpl; /// Internal logic impl InternalImpl = ERC6909Component::InternalImpl; @@ -96,14 +85,11 @@ pub(crate) mod CamelERC6909Mock { /// ABI of Components #[abi(embed_v0)] - impl ERC6909CamelOnlyImpl = - ERC6909Component::ERC6909CamelOnlyImpl; + impl ERC6909CamelOnlyImpl = ERC6909Component::ERC6909CamelOnlyImpl; #[abi(embed_v0)] - impl ERC6909TokenSupplyCamelImpl = - ERC6909Component::ERC6909TokenSupplyCamelImpl; + impl ERC6909TokenSupplyCamelImpl = ERC6909Component::ERC6909TokenSupplyCamelImpl; #[abi(embed_v0)] - impl ERC6909ContentURICamelImpl = - ERC6909Component::ERC6909ContentURICamelImpl; + impl ERC6909ContentURICamelImpl = ERC6909Component::ERC6909ContentURICamelImpl; impl ERC6909Impl = ERC6909Component::ERC6909Impl; diff --git a/packages/token/src/erc6909.cairo b/packages/token/src/erc6909.cairo index c5fe1ce7a..00d355fe5 100644 --- a/packages/token/src/erc6909.cairo +++ b/packages/token/src/erc6909.cairo @@ -1,5 +1,6 @@ pub mod dual6909; pub mod erc6909; +pub mod extensions; pub mod interface; pub use erc6909::ERC6909Component; diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index cbf565c7f..563209c5f 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -1,4 +1,6 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/erc6909.cairo) + use core::starknet::{ContractAddress}; /// # ERC6909 Component @@ -15,14 +17,9 @@ pub mod ERC6909Component { #[storage] struct Storage { - ERC6909_name: LegacyMap, - ERC6909_symbol: LegacyMap, - ERC6909_decimals: LegacyMap, ERC6909_balances: LegacyMap<(ContractAddress, u256), u256>, ERC6909_allowances: LegacyMap<(ContractAddress, ContractAddress, u256), u256>, ERC6909_operators: LegacyMap<(ContractAddress, ContractAddress), bool>, - ERC6909_total_supply: LegacyMap, - ERC6909_contract_uri: ByteArray, } #[event] @@ -281,98 +278,6 @@ pub mod ERC6909Component { } } - #[embeddable_as(ERC6909MetadataImpl)] - impl ERC6909Metadata< - TContractState, +HasComponent, +ERC6909HooksTrait - > of interface::IERC6909Metadata> { - /// @notice Name of a given token. - /// @param id The id of the token. - /// @return The name of the token. - fn name(self: @ComponentState, id: u256) -> ByteArray { - self.ERC6909_name.read(id) - } - - /// @notice Symbol of a given token. - /// @param id The id of the token. - /// @return The symbol of the token. - fn symbol(self: @ComponentState, id: u256) -> ByteArray { - self.ERC6909_symbol.read(id) - } - - /// @notice Decimals of a given token. - /// @param id The id of the token. - /// @return The decimals of the token. - fn decimals(self: @ComponentState, id: u256) -> u8 { - self.ERC6909_decimals.read(id) - } - } - - #[embeddable_as(ERC6909TokenSupplyImpl)] - impl ERC6909TokenSupply< - TContractState, +HasComponent, +ERC6909HooksTrait - > of interface::IERC6909TokenSupply> { - /// @notice Total supply of a token - /// @param id The id of the token. - /// @return The total supply of the token. - fn total_supply(self: @ComponentState, id: u256) -> u256 { - self.ERC6909_total_supply.read(id) - } - } - - #[embeddable_as(ERC6909TokenSupplyCamelImpl)] - impl ERC6909TokenSupplyCamel< - TContractState, +HasComponent, +ERC6909HooksTrait - > of interface::IERC6909TokenSupplyCamel> { - /// @notice Total supply of a token - /// @param id The id of the token. - /// @return The total supply of the token. - fn totalSupply(self: @ComponentState, id: u256) -> u256 { - ERC6909TokenSupply::total_supply(self, id) - } - } - - - #[embeddable_as(ERC6909ContentURIImpl)] - impl ERC6909ContentURI< - TContractState, +HasComponent, +ERC6909HooksTrait - > of interface::IERC6909ContentURI> { - /// @notice The contract level URI. - /// @return The URI of the contract. - fn contract_uri(self: @ComponentState) -> ByteArray { - self.ERC6909_contract_uri.read() - } - - /// @notice Token level URI - /// @param id The id of the token. - /// @return The token level URI. - fn token_uri(self: @ComponentState, id: u256) -> ByteArray { - let contract_uri = self.contract_uri(); - if contract_uri.len() != 0 { - return ""; - } else { - return format!("{}{}", contract_uri, id); - } - } - } - - #[embeddable_as(ERC6909ContentURICamelImpl)] - impl ERC6909ContentURICamel< - TContractState, +HasComponent, +ERC6909HooksTrait - > of interface::IERC6909ContentURICamel> { - /// @notice Contract level URI - /// @return uri The contract level URI. - fn contractUri(self: @ComponentState) -> ByteArray { - ERC6909ContentURI::contract_uri(self) - } - - /// @notice Token level URI - /// @param id The id of the token. - /// @return The token level URI. - fn tokenUri(self: @ComponentState, id: u256) -> ByteArray { - ERC6909ContentURI::token_uri(self, id) - } - } - /// internal #[generate_trait] pub impl InternalImpl< @@ -413,8 +318,12 @@ pub mod ERC6909Component { self.update(get_caller_address(), account, Zero::zero(), id, amount); } - /// Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or burns) if `sender` (or `receiver`) is - /// the zero address. + /// Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or burns) + /// if `sender` (or `receiver`) is the zero address. + /// + /// This function can be extended using the `before_update` and `after_update` hooks. + /// The implementation does not keep track of individual token supplies and this logic is left + /// to the extensions instead. /// /// Emits a `Transfer` event. fn update( @@ -427,51 +336,18 @@ pub mod ERC6909Component { ) { Hooks::before_update(ref self, sender, receiver, id, amount); - let zero_address = Zero::zero(); - if (sender == zero_address) { - let total_supply = self.ERC6909_total_supply.read(id); - self.ERC6909_total_supply.write(id, total_supply + amount); - } else { - let sender_balance = self.ERC6909_balances.read((sender, id)); - assert(sender_balance >= amount, Errors::INSUFFICIENT_BALANCE); - self.ERC6909_balances.write((sender, id), sender_balance - amount); - } + let sender_balance = self.ERC6909_balances.read((sender, id)); + assert(sender_balance >= amount, Errors::INSUFFICIENT_BALANCE); + self.ERC6909_balances.write((sender, id), sender_balance - amount); - if (receiver == zero_address) { - let total_supply = self.ERC6909_total_supply.read(id); - self.ERC6909_total_supply.write(id, total_supply - amount); - } else { - let receiver_balance = self.ERC6909_balances.read((receiver, id)); - self.ERC6909_balances.write((receiver, id), receiver_balance + amount); - } + let receiver_balance = self.ERC6909_balances.read((receiver, id)); + self.ERC6909_balances.write((receiver, id), receiver_balance + amount); self.emit(Transfer { caller, sender, receiver, id, amount }); Hooks::after_update(ref self, sender, receiver, id, amount); } - /// Sets the base URI. - fn _set_contract_uri(ref self: ComponentState, contract_uri: ByteArray) { - self.ERC6909_contract_uri.write(contract_uri); - } - - /// Sets the token name. - fn _set_token_name(ref self: ComponentState, id: u256, name: ByteArray) { - self.ERC6909_name.write(id, name); - } - - /// Sets the token symbol. - fn _set_token_symbol( - ref self: ComponentState, id: u256, symbol: ByteArray - ) { - self.ERC6909_symbol.write(id, symbol); - } - - /// Sets the token decimals. - fn _set_token_decimals(ref self: ComponentState, id: u256, decimals: u8) { - self.ERC6909_decimals.write(id, decimals); - } - /// @notice Sets or unsets a spender as an operator for the caller. /// @param owner The address of the owner. /// @param spender The address of the spender. diff --git a/packages/token/src/erc6909/extensions.cairo b/packages/token/src/erc6909/extensions.cairo new file mode 100644 index 000000000..edbe7bead --- /dev/null +++ b/packages/token/src/erc6909/extensions.cairo @@ -0,0 +1,7 @@ +pub mod erc6909_content_uri; +pub mod erc6909_metadata; +pub mod erc6909_token_supply; + +pub use erc6909_content_uri::ERC6909ContentURIComponent; +pub use erc6909_metadata::ERC6909MetadataComponent; +pub use erc6909_token_supply::ERC6909TokenSupplyComponent; diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo new file mode 100644 index 000000000..09e724e59 --- /dev/null +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/extensions/erc6909_votes.cairo) + +use starknet::ContractAddress; + +/// # ERC6909ContentURI Component +/// +/// The ERC6909Content component allows to set the contract and token ID URIs. +#[starknet::component] +pub mod ERC6909ContentURIComponent { + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::interface; + + #[storage] + struct Storage { + ERC6909ContentURI_contract_uri: ByteArray, + } + + #[embeddable_as(ERC6909ContentURIImpl)] + impl ERC6909ContentURI< + TContractState, + +HasComponent, + +ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of interface::IERC6909ContentURI> { + /// @notice The contract level URI. + /// @return The URI of the contract. + fn contract_uri(self: @ComponentState) -> ByteArray { + self.ERC6909ContentURI_contract_uri.read() + } + + /// @notice Token level URI + /// @param id The id of the token. + /// @return The token level URI. + fn token_uri(self: @ComponentState, id: u256) -> ByteArray { + let contract_uri = self.contract_uri(); + if contract_uri.len() != 0 { + return ""; + } else { + return format!("{}{}", contract_uri, id); + } + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl ERC6909: ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of InternalTrait { + /// Sets the base URI. + fn _set_contract_uri(ref self: ComponentState, contract_uri: ByteArray) { + self.ERC6909ContentURI_contract_uri.write(contract_uri); + } + } +} + diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo new file mode 100644 index 000000000..68d9345d7 --- /dev/null +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/extensions/erc6909_votes.cairo) + +use starknet::ContractAddress; + +/// # ERC6909Metadata Component +/// +/// The ERC6909Metadata component allows to set metadata to the individual token IDs. +#[starknet::component] +pub mod ERC6909MetadataComponent { + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::interface; + + #[storage] + struct Storage { + ERC6909Metadata_name: LegacyMap, + ERC6909Metadata_symbol: LegacyMap, + ERC6909Metadata_decimals: LegacyMap, + } + + #[embeddable_as(ERC6909MetadataImpl)] + impl ERC6909Metadata< + TContractState, + +HasComponent, + +ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of interface::IERC6909Metadata> { + /// @notice Name of a given token. + /// @param id The id of the token. + /// @return The name of the token. + fn name(self: @ComponentState, id: u256) -> ByteArray { + self.ERC6909Metadata_name.read(id) + } + + /// @notice Symbol of a given token. + /// @param id The id of the token. + /// @return The symbol of the token. + fn symbol(self: @ComponentState, id: u256) -> ByteArray { + self.ERC6909Metadata_symbol.read(id) + } + + /// @notice Decimals of a given token. + /// @param id The id of the token. + /// @return The decimals of the token. + fn decimals(self: @ComponentState, id: u256) -> u8 { + self.ERC6909Metadata_decimals.read(id) + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl ERC6909: ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of InternalTrait { + /// Sets the token name. + fn _set_token_name(ref self: ComponentState, id: u256, name: ByteArray) { + self.ERC6909Metadata_name.write(id, name); + } + + /// Sets the token symbol. + fn _set_token_symbol( + ref self: ComponentState, id: u256, symbol: ByteArray + ) { + self.ERC6909Metadata_symbol.write(id, symbol); + } + + /// Sets the token decimals. + fn _set_token_decimals(ref self: ComponentState, id: u256, decimals: u8) { + self.ERC6909Metadata_decimals.write(id, decimals); + } + } +} diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo new file mode 100644 index 000000000..352265ef6 --- /dev/null +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/extensions/erc6909_votes.cairo) + +use starknet::ContractAddress; + +/// # ERC6909TokenSupply Component +/// +/// The ERC6909TokenSupply component allows to keep track of individual token ID supplies. +/// The internal function `_update_token_supply` should be used inside the ERC6909 Hooks. +#[starknet::component] +pub mod ERC6909TokenSupplyComponent { + use core::num::traits::Zero; + use core::starknet::{ContractAddress}; + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::interface; + + #[storage] + struct Storage { + ERC6909TokenSupply_total_supply: LegacyMap, + } + + #[embeddable_as(ERC6909TokenSupplyImpl)] + impl ERC6909TokenSupply< + TContractState, + +HasComponent, + +ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of interface::IERC6909TokenSupply> { + /// @notice Total supply of a token + /// @param id The id of the token. + /// @return The total supply of the token. + fn total_supply(self: @ComponentState, id: u256) -> u256 { + self.ERC6909TokenSupply_total_supply.read(id) + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl ERC6909: ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of InternalTrait { + /// @notice Updates the total supply of a token ID. To keep track of token ID supplies, + /// @dev ideally this function should be called in a `before_update` or `after_update` hook. + fn _update_token_supply( + ref self: ComponentState, + caller: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) { + let zero_address = Zero::zero(); + + // In case of mints we increase the total supply of this token ID + if (sender == zero_address) { + let total_supply = self.ERC6909TokenSupply_total_supply.read(id); + self.ERC6909TokenSupply_total_supply.write(id, total_supply + amount); + } + + // In case of burns we decrease the total supply of this token ID + if (receiver == zero_address) { + let total_supply = self.ERC6909TokenSupply_total_supply.read(id); + self.ERC6909TokenSupply_total_supply.write(id, total_supply - amount); + } + } + } +} diff --git a/packages/token/src/erc6909/interface.cairo b/packages/token/src/erc6909/interface.cairo index 6fd6685dc..0b0c44485 100644 --- a/packages/token/src/erc6909/interface.cairo +++ b/packages/token/src/erc6909/interface.cairo @@ -2,6 +2,7 @@ use starknet::ContractAddress; // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol +// To generate Starknet IDs: https://community.starknet.io/t/starknet-standard-interface-detection pub const IERC6909_ID: felt252 = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee; #[starknet::interface] @@ -151,6 +152,36 @@ pub trait IERC6909CamelOnly { fn supportsInterface(self: @TState, interface_id: felt252) -> bool; } + +// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol +#[starknet::interface] +pub trait ERC6909ABI { + /// @notice IERC6909 standard interface + fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; + fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + fn supports_interface(self: @TState, interface_id: felt252) -> bool; + + /// @notice IERC6909Camel + fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transferFrom( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + fn supportsInterface(self: @TState, interfaceId: felt252) -> bool; +} + +// +// Extensions +// + // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909Metadata.sol #[starknet::interface] pub trait IERC6909Metadata { @@ -179,14 +210,6 @@ pub trait IERC6909TokenSupply { fn total_supply(self: @TState, id: u256) -> u256; } -#[starknet::interface] -pub trait IERC6909TokenSupplyCamel { - /// @notice Total supply of a token - /// @param id The id of the token. - /// @return The total supply of the token. - fn totalSupply(self: @TState, id: u256) -> u256; -} - //https://github.com/jtriley-eth/ERC-6909/blob/main/src/ERC6909ContentURI.sol #[starknet::interface] pub trait IERC6909ContentURI { @@ -199,40 +222,3 @@ pub trait IERC6909ContentURI { /// @return The token level URI. fn token_uri(self: @TState, id: u256) -> ByteArray; } - -#[starknet::interface] -pub trait IERC6909ContentURICamel { - /// @notice Contract level URI - /// @return The contract level URI. - fn contractUri(self: @TState) -> ByteArray; - - /// @notice Token level URI - /// @param id The id of the token. - /// @return The token level URI. - fn tokenUri(self: @TState, id: u256) -> ByteArray; -} - -// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol -#[starknet::interface] -pub trait ERC6909ABI { - /// @notice IERC6909 standard interface - fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; - fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; - fn transfer_from( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool; - fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; - fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - fn supports_interface(self: @TState, interface_id: felt252) -> bool; - - /// @notice IERC6909Camel - fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; - fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - fn transferFrom( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool; - fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - fn supportsInterface(self: @TState, interfaceId: felt252) -> bool; -} From d22e208e4ba7bb4ce5960dc73f5b070c3dd7754b Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 2 Jul 2024 00:40:53 +0200 Subject: [PATCH 06/44] update docs --- docs/modules/ROOT/pages/erc6909.adoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index db0008344..f5443dc48 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -8,20 +8,20 @@ The ERC6909 minimal multi token standard is a specification for {fungibility-agn == Minimal Multi Token Standard -Similar to ERC1155, it uses a single smart contract to represent multiple tokens at once via IDs. The main difference is -that in ERC6909 the callbacks and batching have been removed from the interface and the permission system is a hybrid operator-approval +Similar to ERC1155, it uses a single smart contract to represent multiple tokens via unique IDs. The main difference is +that in ERC6909 "the callbacks and batching have been removed from the interface and the permission system is a hybrid operator-approval scheme for granular and scalable permissions. Functionally, the interface has been reduced to the bare minimum -required to manage multiple tokens under the same contract. +required to manage multiple tokens under the same contract." {eip-6909} == Usage -:solidity-implementation: https://github.com/jtriley-eth/ERC-6909/tree/main/src +:solidity-implementation: https://github.com/jtriley-eth/ERC-6909/tree/main/src[sample Solidity implementations] Using Contracts for Cairo, constructing an ERC6909 contract requires integrating the `ERC6909Component`. Since some functions commonly found on token standards (such as `total_supply` or metadata) are not part of the EIP, the logic for these are implemented under separate modules for ease of use. Developers can choose which modules to -include within their contracts as they see fit. We followed the original {solidity-implementation} to replicate these in Cairo. +include within their contracts as they see fit. We replicated the {solidity-implementation} in Cairo. Aside from the core `ERC6909`, the 3 optional modules that can be imported are: From 66d074a256f6395dc25e5d4e8bb1541af7a0a840 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 2 Jul 2024 16:18:17 +0200 Subject: [PATCH 07/44] update token --- packages/token/src/erc6909/erc6909.cairo | 18 ++++++++++++------ .../extensions/erc6909_token_supply.cairo | 1 - 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 563209c5f..5e4e8c745 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -328,7 +328,7 @@ pub mod ERC6909Component { /// Emits a `Transfer` event. fn update( ref self: ComponentState, - caller: ContractAddress, + caller: ContractAddress, // For the `Transfer` event sender: ContractAddress, // from receiver: ContractAddress, // to id: u256, @@ -336,12 +336,18 @@ pub mod ERC6909Component { ) { Hooks::before_update(ref self, sender, receiver, id, amount); - let sender_balance = self.ERC6909_balances.read((sender, id)); - assert(sender_balance >= amount, Errors::INSUFFICIENT_BALANCE); - self.ERC6909_balances.write((sender, id), sender_balance - amount); + let zero_address = Zero::zero(); + + if (sender != zero_address) { + let sender_balance = self.ERC6909_balances.read((sender, id)); + assert(sender_balance >= amount, Errors::INSUFFICIENT_BALANCE); + self.ERC6909_balances.write((sender, id), sender_balance - amount); + } - let receiver_balance = self.ERC6909_balances.read((receiver, id)); - self.ERC6909_balances.write((receiver, id), receiver_balance + amount); + if (receiver != zero_address) { + let receiver_balance = self.ERC6909_balances.read((receiver, id)); + self.ERC6909_balances.write((receiver, id), receiver_balance + amount); + } self.emit(Transfer { caller, sender, receiver, id, amount }); diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index 352265ef6..c2713997c 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -47,7 +47,6 @@ pub mod ERC6909TokenSupplyComponent { /// @dev ideally this function should be called in a `before_update` or `after_update` hook. fn _update_token_supply( ref self: ComponentState, - caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, From ae2c56f127319ce4230833a4bce6c4f8f538cc45 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 2 Jul 2024 16:18:35 +0200 Subject: [PATCH 08/44] update token tests --- packages/token/src/tests/erc6909.cairo | 2 +- .../src/tests/erc6909/test_erc6909.cairo | 77 +++++++++---------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/packages/token/src/tests/erc6909.cairo b/packages/token/src/tests/erc6909.cairo index 1bc5001cc..8a82f2b2a 100644 --- a/packages/token/src/tests/erc6909.cairo +++ b/packages/token/src/tests/erc6909.cairo @@ -1,4 +1,4 @@ pub(crate) mod common; -mod test_dual6909; +// mod test_dual6909; mod test_erc6909; diff --git a/packages/token/src/tests/erc6909/test_erc6909.cairo b/packages/token/src/tests/erc6909/test_erc6909.cairo index 0bfae9eeb..86084145d 100644 --- a/packages/token/src/tests/erc6909/test_erc6909.cairo +++ b/packages/token/src/tests/erc6909/test_erc6909.cairo @@ -7,8 +7,7 @@ use openzeppelin::tests::utils::constants::{ }; use openzeppelin::tests::utils; use openzeppelin::token::erc6909::ERC6909Component::{ - InternalImpl, ERC6909Impl, ERC6909CamelOnlyImpl, ERC6909TokenSupplyImpl, - ERC6909TokenSupplyCamelImpl + InternalImpl, ERC6909Impl, ERC6909CamelOnlyImpl }; use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet}; use openzeppelin::token::erc6909::ERC6909Component; @@ -40,20 +39,6 @@ fn setup() -> ComponentState { // Getters // -#[test] -fn test_total_supply() { - let mut state = COMPONENT_STATE(); - state.mint(OWNER(), TOKEN_ID, SUPPLY); - assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); -} - -#[test] -fn test_totalSupply() { - let mut state = COMPONENT_STATE(); - state.mint(OWNER(), TOKEN_ID, SUPPLY); - assert_eq!(state.totalSupply(TOKEN_ID), SUPPLY); -} - #[test] fn test_balance_of() { let mut state = COMPONENT_STATE(); @@ -77,6 +62,41 @@ fn test_allowance() { assert_eq!(allowance, VALUE); } +#[test] +fn test_set_supports_interface() { + let mut state = setup(); + // IERC6909_ID as defined in `interface.cairo` = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee + assert!( + state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee) + ); + assert_eq!(state.supports_interface(0x32cb), false); + assert_eq!( + state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ef), + false + ); + + // id == ISRC5_ID || id == IERC6909_ID + assert!(state.supports_interface(ISRC5_ID)) +} + +#[test] +fn test_set_supportsInterface() { + let mut state = setup(); + // IERC6909_ID as defined in `interface.cairo` = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee + assert!( + state.supportsInterface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee) + ); + assert_eq!(state.supportsInterface(0x32cb), false); + assert_eq!( + state.supportsInterface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ef), + false + ); + + // id == ISRC5_ID || id == IERC6909_ID + assert!(state.supportsInterface(ISRC5_ID)) +} + + // // approve & _approve // @@ -144,7 +164,6 @@ fn test_transfer() { assert_only_event_transfer(ZERO(), OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); - assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); } #[test] @@ -178,7 +197,6 @@ fn test__transfer() { assert_only_event_transfer(ZERO(), OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); - assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); } #[test] @@ -237,7 +255,6 @@ fn test_transfer_from() { assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); - assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); } #[test] @@ -314,7 +331,6 @@ fn test_transferFrom() { assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); - assert_eq!(state.total_supply(TOKEN_ID), SUPPLY); assert_eq!(allowance, 0); } @@ -407,7 +423,6 @@ fn test__mint() { assert_only_event_transfer(ZERO(), ZERO(), ZERO(), OWNER(), TOKEN_ID, VALUE); assert_eq!(state.balance_of(OWNER(), TOKEN_ID), VALUE); - assert_eq!(state.total_supply(TOKEN_ID), VALUE); } #[test] @@ -428,7 +443,6 @@ fn test__burn() { assert_only_event_transfer(ZERO(), ZERO(), OWNER(), ZERO(), TOKEN_ID, VALUE); assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); - assert_eq!(state.total_supply(TOKEN_ID), SUPPLY - VALUE); } #[test] @@ -438,25 +452,6 @@ fn test__burn_from_zero() { state.burn(ZERO(), TOKEN_ID, VALUE); } -// -// supports_interface -// -#[test] -fn test_set_supports_interface() { - let mut state = setup(); - // IERC6909_ID as defined in `interface.cairo` = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee - assert!( - state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee) - ); - assert_eq!(state.supports_interface(0x32cb), false); - assert_eq!( - state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ef), - false - ); - assert!(state.supports_interface(ISRC5_ID)) -} - - // // is_operator & set_operator // From d116d005dff9d20befbe2dd7b381a8623993431a Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 2 Jul 2024 16:19:00 +0200 Subject: [PATCH 09/44] add `token_supply` mocoks --- .../test_common/src/mocks/erc6909_mocks.cairo | 94 +++++-------------- .../mocks/erc6909_token_supply_mocks.cairo | 83 ++++++++++++++++ 2 files changed, 107 insertions(+), 70 deletions(-) create mode 100644 packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo diff --git a/packages/test_common/src/mocks/erc6909_mocks.cairo b/packages/test_common/src/mocks/erc6909_mocks.cairo index 3091f38a9..0e62ac6f7 100644 --- a/packages/test_common/src/mocks/erc6909_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_mocks.cairo @@ -10,49 +10,8 @@ pub(crate) mod DualCaseERC6909Mock { #[abi(embed_v0)] impl ERC6909Impl = ERC6909Component::ERC6909Impl; #[abi(embed_v0)] - impl ERC6909CamelOnlyImpl = ERC6909Component::ERC6909CamelOnlyImpl; - #[abi(embed_v0)] - impl ERC6909TokenSupplyImpl = ERC6909Component::ERC6909TokenSupplyImpl; - #[abi(embed_v0)] - impl ERC6909ContentURIImpl = ERC6909Component::ERC6909ContentURIImpl; - - /// Internal logic - impl InternalImpl = ERC6909Component::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { - self.erc6909.mint(receiver, id, amount); - self.erc6909._set_contract_uri("URI"); - } -} - -#[starknet::contract] -pub(crate) mod SnakeERC6909Mock { - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - - /// ABI of Components - #[abi(embed_v0)] - impl ERC6909Impl = ERC6909Component::ERC6909Impl; - #[abi(embed_v0)] - impl ERC6909TokenSupplyImpl = ERC6909Component::ERC6909TokenSupplyImpl; - #[abi(embed_v0)] - impl ERC6909ContentURIImpl = ERC6909Component::ERC6909ContentURIImpl; + impl ERC6909CamelOnlyImpl = + ERC6909Component::ERC6909CamelOnlyImpl; /// Internal logic impl InternalImpl = ERC6909Component::InternalImpl; @@ -81,17 +40,15 @@ pub(crate) mod CamelERC6909Mock { use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; use starknet::ContractAddress; + /// Component component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - /// ABI of Components - #[abi(embed_v0)] - impl ERC6909CamelOnlyImpl = ERC6909Component::ERC6909CamelOnlyImpl; - #[abi(embed_v0)] - impl ERC6909TokenSupplyCamelImpl = ERC6909Component::ERC6909TokenSupplyCamelImpl; #[abi(embed_v0)] - impl ERC6909ContentURICamelImpl = ERC6909Component::ERC6909ContentURICamelImpl; - + impl ERC6909CamelOnlyImpl = + ERC6909Component::ERC6909CamelOnlyImpl; + // `ERC6909Impl` is not embedded because it would defeat the purpose of the + // mock. The `ERC6909Impl` case-agnostic methods are manually exposed. impl ERC6909Impl = ERC6909Component::ERC6909Impl; impl InternalImpl = ERC6909Component::InternalImpl; @@ -118,7 +75,7 @@ pub(crate) mod CamelERC6909Mock { impl ExternalImpl of ExternalTrait { #[external(v0)] fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256 + self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, ) -> u256 { self.erc6909.allowance(owner, spender, id) } @@ -156,14 +113,14 @@ pub(crate) mod SnakeERC6909Panic { #[generate_trait] impl ExternalImpl of ExternalTrait { #[external(v0)] - fn balance_of(self: @ContractState, owner: ContractAddress, id: u256) -> u256 { + fn balance_of(self: @ContractState, account: ContractAddress, id: u256) -> u256 { panic!("Some error"); 3 } #[external(v0)] fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256 + self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, ) -> u256 { panic!("Some error"); 3 @@ -171,16 +128,14 @@ pub(crate) mod SnakeERC6909Panic { #[external(v0)] fn is_operator( - self: @ContractState, owner: ContractAddress, spender: ContractAddress + self: @ContractState, owner: ContractAddress, spender: ContractAddress, ) -> bool { panic!("Some error"); false } #[external(v0)] - fn transfer( - ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool { + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { panic!("Some error"); false } @@ -198,9 +153,7 @@ pub(crate) mod SnakeERC6909Panic { } #[external(v0)] - fn approve( - ref self: ContractState, spender: ContractAddress, id: u256, amount: u256 - ) -> bool { + fn approve(ref self: ContractState, to: ContractAddress, id: u256) -> bool { panic!("Some error"); false } @@ -230,24 +183,16 @@ pub(crate) mod CamelERC6909Panic { #[generate_trait] impl ExternalImpl of ExternalTrait { #[external(v0)] - fn balanceOf(self: @ContractState, owner: ContractAddress, id: u256) -> u256 { + fn balanceOf(self: @ContractState, account: ContractAddress, id: u256) -> u256 { panic!("Some error"); 3 } - #[external(v0)] - fn isOperator( - self: @ContractState, owner: ContractAddress, spender: ContractAddress - ) -> bool { - panic!("Some error"); - false - } - #[external(v0)] fn transferFrom( ref self: ContractState, sender: ContractAddress, - receiver: ContractAddress, + recipient: ContractAddress, id: u256, amount: u256 ) -> bool { @@ -266,5 +211,14 @@ pub(crate) mod CamelERC6909Panic { panic!("Some error"); false } + + #[external(v0)] + fn isOperator( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> bool { + panic!("Some error"); + false + } } } + diff --git a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo new file mode 100644 index 000000000..cfa52733e --- /dev/null +++ b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo @@ -0,0 +1,83 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC6909TokenSupplyMock { + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent::InternalTrait as ERC6909TokenSupplyInternalTrait; + use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; + use starknet::ContractAddress; + + component!( + path: ERC6909TokenSupplyComponent, + storage: erc6909_token_supply, + event: ERC6909TokenSupplyEvent + ); + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); + + // ERC6909TokenSupply + #[abi(embed_v0)] + impl ERC6909TokenSupplyComponentImpl = + ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; + + // ERC6909Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909_token_supply: ERC6909TokenSupplyComponent::Storage, + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909TokenSupplyEvent: ERC6909TokenSupplyComponent::Event, + #[flat] + ERC6909Event: ERC6909Component::Event, + } + + impl ERC6909TokenSupplyHooksImpl< + TContractState, + impl ERC6909TokenSupply: ERC6909TokenSupplyComponent::HasComponent, + impl HasComponent: ERC6909Component::HasComponent, + +Drop + > of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + /// Update after any transfer + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) { + let mut erc6909_token_supply_component = get_dep_component_mut!( + ref self, ERC6909TokenSupply + ); + erc6909_token_supply_component._update_token_supply(from, recipient, id, amount); + } + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + fixed_supply: u256, + recipient: ContractAddress + ) { + self.erc6909.initializer(name, symbol); + self.erc6909.mint(recipient, fixed_supply); + } +} From 18929f8b04fba2efe92fd024999def8c09fcb0f5 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 5 Jul 2024 13:31:28 +0200 Subject: [PATCH 10/44] refactor dual mocks and tests --- .../test_common/src/mocks/erc6909_mocks.cairo | 42 +++++++++++++++++-- packages/token/src/tests/erc6909.cairo | 2 +- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/test_common/src/mocks/erc6909_mocks.cairo b/packages/test_common/src/mocks/erc6909_mocks.cairo index 0e62ac6f7..fbc7283bc 100644 --- a/packages/test_common/src/mocks/erc6909_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_mocks.cairo @@ -10,12 +10,46 @@ pub(crate) mod DualCaseERC6909Mock { #[abi(embed_v0)] impl ERC6909Impl = ERC6909Component::ERC6909Impl; #[abi(embed_v0)] - impl ERC6909CamelOnlyImpl = - ERC6909Component::ERC6909CamelOnlyImpl; + impl ERC6909CamelOnlyImpl = ERC6909Component::ERC6909CamelOnlyImpl; + + /// Internal logic + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } +} + +#[starknet::contract] +pub(crate) mod SnakeERC6909Mock { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + /// Component + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + /// ABI of Components + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; /// Internal logic impl InternalImpl = ERC6909Component::InternalImpl; + #[storage] struct Storage { #[substorage(v0)] @@ -135,7 +169,7 @@ pub(crate) mod SnakeERC6909Panic { } #[external(v0)] - fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + fn transfer(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) -> bool { panic!("Some error"); false } @@ -153,7 +187,7 @@ pub(crate) mod SnakeERC6909Panic { } #[external(v0)] - fn approve(ref self: ContractState, to: ContractAddress, id: u256) -> bool { + fn approve(ref self: ContractState, spender: ContractAddress, id: u256, amount: u256) -> bool { panic!("Some error"); false } diff --git a/packages/token/src/tests/erc6909.cairo b/packages/token/src/tests/erc6909.cairo index 8a82f2b2a..1bc5001cc 100644 --- a/packages/token/src/tests/erc6909.cairo +++ b/packages/token/src/tests/erc6909.cairo @@ -1,4 +1,4 @@ pub(crate) mod common; -// mod test_dual6909; +mod test_dual6909; mod test_erc6909; From e86fece96b95f8c06505840f63f6099dcbde8363 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 5 Jul 2024 17:15:45 +0200 Subject: [PATCH 11/44] add token supply tests --- .../test_common/src/mocks/erc6909_mocks.cairo | 11 +- .../mocks/erc6909_token_supply_mocks.cairo | 29 ++-- .../extensions/erc6909_token_supply.cairo | 9 +- packages/token/src/tests/erc6909.cairo | 1 + .../erc6909/test_erc6909_token_supply.cairo | 164 ++++++++++++++++++ 5 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo diff --git a/packages/test_common/src/mocks/erc6909_mocks.cairo b/packages/test_common/src/mocks/erc6909_mocks.cairo index fbc7283bc..460a11502 100644 --- a/packages/test_common/src/mocks/erc6909_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_mocks.cairo @@ -10,7 +10,8 @@ pub(crate) mod DualCaseERC6909Mock { #[abi(embed_v0)] impl ERC6909Impl = ERC6909Component::ERC6909Impl; #[abi(embed_v0)] - impl ERC6909CamelOnlyImpl = ERC6909Component::ERC6909CamelOnlyImpl; + impl ERC6909CamelOnlyImpl = + ERC6909Component::ERC6909CamelOnlyImpl; /// Internal logic impl InternalImpl = ERC6909Component::InternalImpl; @@ -169,7 +170,9 @@ pub(crate) mod SnakeERC6909Panic { } #[external(v0)] - fn transfer(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) -> bool { + fn transfer( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool { panic!("Some error"); false } @@ -187,7 +190,9 @@ pub(crate) mod SnakeERC6909Panic { } #[external(v0)] - fn approve(ref self: ContractState, spender: ContractAddress, id: u256, amount: u256) -> bool { + fn approve( + ref self: ContractState, spender: ContractAddress, id: u256, amount: u256 + ) -> bool { panic!("Some error"); false } diff --git a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo index cfa52733e..18bc7cd76 100644 --- a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo @@ -11,7 +11,6 @@ pub(crate) mod DualCaseERC6909TokenSupplyMock { event: ERC6909TokenSupplyEvent ); component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); // ERC6909TokenSupply #[abi(embed_v0)] @@ -40,6 +39,11 @@ pub(crate) mod DualCaseERC6909TokenSupplyMock { ERC6909Event: ERC6909Component::Event, } + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } + impl ERC6909TokenSupplyHooksImpl< TContractState, impl ERC6909TokenSupply: ERC6909TokenSupplyComponent::HasComponent, @@ -69,15 +73,18 @@ pub(crate) mod DualCaseERC6909TokenSupplyMock { } } - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - fixed_supply: u256, - recipient: ContractAddress - ) { - self.erc6909.initializer(name, symbol); - self.erc6909.mint(recipient, fixed_supply); + // These functions are for testing purposes only + #[abi(per_item)] + #[generate_trait] + pub impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn public_mint(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } + + #[external(v0)] + fn public_burn(ref self: ContractState, owner: ContractAddress, id: u256, amount: u256) { + self.erc6909.burn(owner, id, amount); + } } } diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index c2713997c..b8eb0e17c 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -12,7 +12,8 @@ pub mod ERC6909TokenSupplyComponent { use core::num::traits::Zero; use core::starknet::{ContractAddress}; use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::interface; + use openzeppelin::token::erc6909::interface::IERC6909; + use openzeppelin::token::erc6909::interface::IERC6909TokenSupply; #[storage] struct Storage { @@ -26,7 +27,7 @@ pub mod ERC6909TokenSupplyComponent { +ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, +Drop - > of interface::IERC6909TokenSupply> { + > of IERC6909TokenSupply> { /// @notice Total supply of a token /// @param id The id of the token. /// @return The total supply of the token. @@ -35,6 +36,10 @@ pub mod ERC6909TokenSupplyComponent { } } + // + // Internal + // + #[generate_trait] pub impl InternalImpl< TContractState, diff --git a/packages/token/src/tests/erc6909.cairo b/packages/token/src/tests/erc6909.cairo index 1bc5001cc..9a12c6deb 100644 --- a/packages/token/src/tests/erc6909.cairo +++ b/packages/token/src/tests/erc6909.cairo @@ -2,3 +2,4 @@ pub(crate) mod common; mod test_dual6909; mod test_erc6909; +mod test_erc6909_token_supply; diff --git a/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo b/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo new file mode 100644 index 000000000..2910faa38 --- /dev/null +++ b/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo @@ -0,0 +1,164 @@ +use core::integer::BoundedInt; +use core::num::traits::Zero; +use openzeppelin::tests::mocks::erc6909_token_supply_mocks::DualCaseERC6909TokenSupplyMock; +use openzeppelin::tests::mocks::erc6909_token_supply_mocks::DualCaseERC6909TokenSupplyMock::ExternalTrait; +use openzeppelin::tests::utils::constants::{OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::{InternalImpl as InternalERC6909Impl, ERC6909Impl}; +use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet}; +use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent::{ + ERC6909TokenSupplyImpl, InternalImpl, +}; +use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::storage::{StorageMapMemberAccessTrait, StorageMemberAccessTrait}; +use starknet::testing; + +use super::common::{ + assert_event_approval, assert_only_event_approval, assert_only_event_transfer, + assert_only_event_operator_set, assert_event_operator_set +}; + +// +// Setup +// + +const TOKEN_ID: u256 = 420; + +type ComponentState = + ERC6909TokenSupplyComponent::ComponentState; + +fn CONTRACT_STATE() -> DualCaseERC6909TokenSupplyMock::ContractState { + DualCaseERC6909TokenSupplyMock::contract_state_for_testing() +} + +fn COMPONENT_STATE() -> ComponentState { + ERC6909TokenSupplyComponent::component_state_for_testing() +} + +fn setup() -> (ComponentState, DualCaseERC6909TokenSupplyMock::ContractState) { + let mut state = COMPONENT_STATE(); + let mut mock_state = CONTRACT_STATE(); + mock_state.erc6909.mint(OWNER(), TOKEN_ID, SUPPLY); + utils::drop_event(ZERO()); + (state, mock_state) +} + +// +// Getters +// + +#[test] +fn test__state_total_supply() { + let (mut state, _) = setup(); + let mut id_supply = state.ERC6909TokenSupply_total_supply.read(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); +} + +#[test] +fn test__state_no_total_supply() { + let (mut state, _) = setup(); + let mut id_supply = state.ERC6909TokenSupply_total_supply.read(TOKEN_ID + 69); + assert_eq!(id_supply, 0); +} + + +#[test] +fn test_total_supply() { + let (mut state, _) = setup(); + let mut id_supply = state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); +} + +#[test] +fn test_no_total_supply() { + let (mut state, _) = setup(); + let mut id_supply = state.total_supply(TOKEN_ID + 69); + assert_eq!(id_supply, 0); +} + +#[test] +fn test_total_supply_contract() { + let (_, mut mock_state) = setup(); + let mut id_supply = mock_state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); +} +// +// mint & burn +// + +#[test] +fn test_mint_increase_supply() { + let (_, mut mock_state) = setup(); + let mut id_supply = mock_state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); + + let new_token_id = TOKEN_ID + 69; + + testing::set_caller_address(OWNER()); + mock_state.public_mint(OWNER(), new_token_id, SUPPLY * 2); + + let mut old_token_id_supply = mock_state.total_supply(TOKEN_ID); + let mut new_token_id_supply = mock_state.total_supply(new_token_id); + assert_eq!(old_token_id_supply, SUPPLY); + assert_eq!(new_token_id_supply, SUPPLY * 2); +} + +#[test] +fn test_burn_decrease_supply() { + let (_, mut mock_state) = setup(); + let mut id_supply = mock_state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); + + let new_token_id = TOKEN_ID + 69; + + testing::set_caller_address(OWNER()); + mock_state.public_mint(OWNER(), new_token_id, SUPPLY * 2); + + let mut new_token_id_supply = mock_state.total_supply(new_token_id); + assert_eq!(new_token_id_supply, SUPPLY * 2); + + testing::set_caller_address(OWNER()); + mock_state.public_burn(OWNER(), new_token_id, SUPPLY * 2); + + let mut new_token_id_supply = mock_state.total_supply(new_token_id); + assert_eq!(new_token_id_supply, 0); +} + +// transfer & transferFrom +#[test] +fn test_transfers_dont_change_supply() { + let (_, mut mock_state) = setup(); + let mut id_supply = mock_state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); + + testing::set_caller_address(OWNER()); + mock_state.transfer(RECIPIENT(), TOKEN_ID, SUPPLY); + + let mut id_supply = mock_state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); + + testing::set_caller_address(RECIPIENT()); + mock_state.transfer(OWNER(), TOKEN_ID, SUPPLY / 2); + + let mut id_supply = mock_state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); +} + +// transfer & transferFrom +#[test] +fn test_transfer_from_doesnt_change_supply() { + let (_, mut mock_state) = setup(); + let mut id_supply = mock_state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); + + testing::set_caller_address(OWNER()); + mock_state.approve(SPENDER(), TOKEN_ID, SUPPLY); + testing::set_caller_address(SPENDER()); + mock_state.transfer_from(OWNER(), SPENDER(), TOKEN_ID, SUPPLY); + + let mut id_supply = mock_state.total_supply(TOKEN_ID); + assert_eq!(id_supply, SUPPLY); +} From 5bbe1ff5177d93a1ee2ab266f984dcc3bd61c487 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 5 Jul 2024 18:40:35 +0200 Subject: [PATCH 12/44] add token supply and content uri tests --- .../src/mocks/erc6909_content_uri_mocks.cairo | 49 ++++++++ .../src/mocks/erc6909_metadata_mocks.cairo | 43 +++++++ .../mocks/erc6909_token_supply_mocks.cairo | 15 --- .../extensions/erc6909_content_uri.cairo | 4 +- packages/token/src/tests/erc6909.cairo | 2 + .../erc6909/test_erc6909_content_uri.cairo | 105 +++++++++++++++++ .../tests/erc6909/test_erc6909_metadata.cairo | 110 ++++++++++++++++++ .../erc6909/test_erc6909_token_supply.cairo | 12 +- 8 files changed, 317 insertions(+), 23 deletions(-) create mode 100644 packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo create mode 100644 packages/test_common/src/mocks/erc6909_metadata_mocks.cairo create mode 100644 packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo create mode 100644 packages/token/src/tests/erc6909/test_erc6909_metadata.cairo diff --git a/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo b/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo new file mode 100644 index 000000000..2b0760bbd --- /dev/null +++ b/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo @@ -0,0 +1,49 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC6909ContentURIMock { + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent::InternalTrait as ERC6909ContentURIInternalTrait; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + component!( + path: ERC6909ContentURIComponent, + storage: erc6909_content_uri, + event: ERC6909ContentURIEvent + ); + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + // ERC6909ContentURI + #[abi(embed_v0)] + impl ERC6909ContentURIComponentImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; + + // ERC6909Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + #[flat] + ERC6909Event: ERC6909Component::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray + ) { + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri); + } +} diff --git a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo new file mode 100644 index 000000000..e48503224 --- /dev/null +++ b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo @@ -0,0 +1,43 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC6909MetadataMock { + use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent::InternalTrait as ERC6909MetadataInternalTrait; + use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + component!( + path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent + ); + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + // ERC6909Metadata + #[abi(embed_v0)] + impl ERC6909MetadataComponentImpl = + ERC6909MetadataComponent::ERC6909MetadataImpl; + + // ERC6909Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909_metadata: ERC6909MetadataComponent::Storage, + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909MetadataEvent: ERC6909MetadataComponent::Event, + #[flat] + ERC6909Event: ERC6909Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } +} diff --git a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo index 18bc7cd76..e15b5eb54 100644 --- a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo @@ -72,19 +72,4 @@ pub(crate) mod DualCaseERC6909TokenSupplyMock { erc6909_token_supply_component._update_token_supply(from, recipient, id, amount); } } - - // These functions are for testing purposes only - #[abi(per_item)] - #[generate_trait] - pub impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn public_mint(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { - self.erc6909.mint(receiver, id, amount); - } - - #[external(v0)] - fn public_burn(ref self: ContractState, owner: ContractAddress, id: u256, amount: u256) { - self.erc6909.burn(owner, id, amount); - } - } } diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index 09e724e59..32ab38bd4 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -35,7 +35,7 @@ pub mod ERC6909ContentURIComponent { /// @return The token level URI. fn token_uri(self: @ComponentState, id: u256) -> ByteArray { let contract_uri = self.contract_uri(); - if contract_uri.len() != 0 { + if contract_uri.len() == 0 { return ""; } else { return format!("{}{}", contract_uri, id); @@ -52,7 +52,7 @@ pub mod ERC6909ContentURIComponent { +Drop > of InternalTrait { /// Sets the base URI. - fn _set_contract_uri(ref self: ComponentState, contract_uri: ByteArray) { + fn initializer(ref self: ComponentState, contract_uri: ByteArray) { self.ERC6909ContentURI_contract_uri.write(contract_uri); } } diff --git a/packages/token/src/tests/erc6909.cairo b/packages/token/src/tests/erc6909.cairo index 9a12c6deb..8d9685af3 100644 --- a/packages/token/src/tests/erc6909.cairo +++ b/packages/token/src/tests/erc6909.cairo @@ -2,4 +2,6 @@ pub(crate) mod common; mod test_dual6909; mod test_erc6909; +mod test_erc6909_content_uri; +mod test_erc6909_metadata; mod test_erc6909_token_supply; diff --git a/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo b/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo new file mode 100644 index 000000000..770e84cdd --- /dev/null +++ b/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo @@ -0,0 +1,105 @@ +use core::integer::BoundedInt; +use core::num::traits::Zero; +use openzeppelin::tests::mocks::erc6909_content_uri_mocks::DualCaseERC6909ContentURIMock; +use openzeppelin::tests::utils::constants::{ + OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO, BASE_URI, BASE_URI_2 +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::InternalImpl as InternalERC6909Impl; +use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent::{ + ERC6909ContentURIImpl, InternalImpl, +}; +use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::storage::{StorageMapMemberAccessTrait, StorageMemberAccessTrait}; +use starknet::testing; + +use super::common::{ + assert_event_approval, assert_only_event_approval, assert_only_event_transfer, + assert_only_event_operator_set, assert_event_operator_set +}; + +// +// Setup +// + +const TOKEN_ID: u256 = 420; + +type ComponentState = + ERC6909ContentURIComponent::ComponentState; + +fn CONTRACT_STATE() -> DualCaseERC6909ContentURIMock::ContractState { + DualCaseERC6909ContentURIMock::contract_state_for_testing() +} + +fn COMPONENT_STATE() -> ComponentState { + ERC6909ContentURIComponent::component_state_for_testing() +} + +fn setup() -> (ComponentState, DualCaseERC6909ContentURIMock::ContractState) { + let mut state = COMPONENT_STATE(); + let mut mock_state = CONTRACT_STATE(); + mock_state.erc6909.mint(OWNER(), TOKEN_ID, SUPPLY); + utils::drop_event(ZERO()); + (state, mock_state) +} + +// +// Getters +// + +#[test] +fn test_unset_content_uri() { + let (mut state, _) = setup(); + let mut uri = state.contract_uri(); + assert_eq!(uri, ""); +} + +#[test] +fn test_unset_token_uri() { + let (mut state, _) = setup(); + let uri = state.token_uri(TOKEN_ID); + assert_eq!(uri, ""); +} + +// +// internal setters +// + +#[test] +fn test_set_contract_uri() { + let (mut state, _) = setup(); + testing::set_caller_address(OWNER()); + state.initializer(BASE_URI()); + let uri = state.contract_uri(); + assert_eq!(uri, BASE_URI()); +} + +#[test] +fn test_set_token_uri() { + let (mut state, _) = setup(); + testing::set_caller_address(OWNER()); + state.initializer(BASE_URI()); + let uri = state.token_uri(TOKEN_ID); + let expected = format!("{}{}", BASE_URI(), TOKEN_ID); + assert_eq!(uri, expected); +} + +// Updates the URI once set +#[test] +fn test_update_token_uri() { + let (mut state, _) = setup(); + testing::set_caller_address(OWNER()); + state.initializer(BASE_URI()); + let mut uri = state.token_uri(TOKEN_ID); + let mut expected = format!("{}{}", BASE_URI(), TOKEN_ID); + assert_eq!(uri, expected); + + testing::set_caller_address(OWNER()); + state.initializer(BASE_URI_2()); + let mut uri = state.token_uri(TOKEN_ID); + let expected = format!("{}{}", BASE_URI_2(), TOKEN_ID); + assert_eq!(uri, expected); +} diff --git a/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo b/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo new file mode 100644 index 000000000..a7390310d --- /dev/null +++ b/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo @@ -0,0 +1,110 @@ +use core::integer::BoundedInt; +use core::num::traits::Zero; +use openzeppelin::tests::mocks::erc6909_metadata_mocks::DualCaseERC6909MetadataMock; +use openzeppelin::tests::utils::constants::{OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::InternalImpl as InternalERC6909Impl; +use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent::{ + ERC6909MetadataImpl, InternalImpl, +}; +use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::storage::{StorageMapMemberAccessTrait, StorageMemberAccessTrait}; +use starknet::testing; + +use super::common::{ + assert_event_approval, assert_only_event_approval, assert_only_event_transfer, + assert_only_event_operator_set, assert_event_operator_set +}; + +// +// Setup +// + +const TOKEN_ID: u256 = 420; + +type ComponentState = + ERC6909MetadataComponent::ComponentState; + +fn CONTRACT_STATE() -> DualCaseERC6909MetadataMock::ContractState { + DualCaseERC6909MetadataMock::contract_state_for_testing() +} + +fn COMPONENT_STATE() -> ComponentState { + ERC6909MetadataComponent::component_state_for_testing() +} + +fn setup() -> (ComponentState, DualCaseERC6909MetadataMock::ContractState) { + let mut state = COMPONENT_STATE(); + let mut mock_state = CONTRACT_STATE(); + mock_state.erc6909.mint(OWNER(), TOKEN_ID, SUPPLY); + utils::drop_event(ZERO()); + (state, mock_state) +} + +// +// Getters +// + +#[test] +fn test_name() { + let (mut state, _) = setup(); + let mut name = state.ERC6909Metadata_name.read(TOKEN_ID); + assert_eq!(name, ""); +} + +#[test] +fn test_symbol() { + let (mut state, _) = setup(); + let mut symbol = state.ERC6909Metadata_symbol.read(TOKEN_ID); + assert_eq!(symbol, ""); +} + +#[test] +fn test_decimals() { + let (mut state, _) = setup(); + let mut decimals = state.ERC6909Metadata_decimals.read(TOKEN_ID); + assert_eq!(decimals, 0); +} + +// +// internal setters +// + +#[test] +fn test_set_name() { + let (_, mut mock_state) = setup(); + testing::set_caller_address(OWNER()); + mock_state.erc6909_metadata._set_token_name(TOKEN_ID, "some token"); + let mut name = mock_state.name(TOKEN_ID); + assert_eq!(name, "some token"); + + let mut name = mock_state.name(TOKEN_ID + 69); + assert_eq!(name, ""); +} + +#[test] +fn test_set_symbol() { + let (_, mut mock_state) = setup(); + testing::set_caller_address(OWNER()); + mock_state.erc6909_metadata._set_token_symbol(TOKEN_ID, "some symbol"); + let mut symbol = mock_state.symbol(TOKEN_ID); + assert_eq!(symbol, "some symbol"); + + let mut symbol = mock_state.symbol(TOKEN_ID + 69); + assert_eq!(symbol, ""); +} + +#[test] +fn test_set_decimals() { + let (_, mut mock_state) = setup(); + testing::set_caller_address(OWNER()); + mock_state.erc6909_metadata._set_token_decimals(TOKEN_ID, 18); + let mut decimals = mock_state.decimals(TOKEN_ID); + assert_eq!(decimals, 18); + + let mut decimals = mock_state.decimals(TOKEN_ID + 69); + assert_eq!(decimals, 0); +} diff --git a/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo b/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo index 2910faa38..4bdf62dcf 100644 --- a/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo +++ b/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo @@ -1,11 +1,11 @@ use core::integer::BoundedInt; use core::num::traits::Zero; use openzeppelin::tests::mocks::erc6909_token_supply_mocks::DualCaseERC6909TokenSupplyMock; -use openzeppelin::tests::mocks::erc6909_token_supply_mocks::DualCaseERC6909TokenSupplyMock::ExternalTrait; use openzeppelin::tests::utils::constants::{OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO}; use openzeppelin::tests::utils; -use openzeppelin::token::erc6909::ERC6909Component::{InternalImpl as InternalERC6909Impl, ERC6909Impl}; -use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet}; +use openzeppelin::token::erc6909::ERC6909Component::{ + InternalImpl as InternalERC6909Impl, ERC6909Impl +}; use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent::{ ERC6909TokenSupplyImpl, InternalImpl, }; @@ -98,7 +98,7 @@ fn test_mint_increase_supply() { let new_token_id = TOKEN_ID + 69; testing::set_caller_address(OWNER()); - mock_state.public_mint(OWNER(), new_token_id, SUPPLY * 2); + mock_state.erc6909.mint(OWNER(), new_token_id, SUPPLY * 2); let mut old_token_id_supply = mock_state.total_supply(TOKEN_ID); let mut new_token_id_supply = mock_state.total_supply(new_token_id); @@ -115,13 +115,13 @@ fn test_burn_decrease_supply() { let new_token_id = TOKEN_ID + 69; testing::set_caller_address(OWNER()); - mock_state.public_mint(OWNER(), new_token_id, SUPPLY * 2); + mock_state.erc6909.mint(OWNER(), new_token_id, SUPPLY * 2); let mut new_token_id_supply = mock_state.total_supply(new_token_id); assert_eq!(new_token_id_supply, SUPPLY * 2); testing::set_caller_address(OWNER()); - mock_state.public_burn(OWNER(), new_token_id, SUPPLY * 2); + mock_state.erc6909.burn(OWNER(), new_token_id, SUPPLY * 2); let mut new_token_id_supply = mock_state.total_supply(new_token_id); assert_eq!(new_token_id_supply, 0); From e6aad9301cecac3197880be1717ad8f02f028480 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 5 Jul 2024 19:57:52 +0200 Subject: [PATCH 13/44] simplify mock --- docs/modules/ROOT/pages/erc6909.adoc | 92 ++++++++++++++----- .../mocks/erc6909_token_supply_mocks.cairo | 2 +- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index f5443dc48..cf3ad7656 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -18,62 +18,105 @@ required to manage multiple tokens under the same contract." {eip-6909} :solidity-implementation: https://github.com/jtriley-eth/ERC-6909/tree/main/src[sample Solidity implementations] Using Contracts for Cairo, constructing an ERC6909 contract requires integrating the `ERC6909Component`. +Here's what that looks like: -Since some functions commonly found on token standards (such as `total_supply` or metadata) are not part of the EIP, -the logic for these are implemented under separate modules for ease of use. Developers can choose which modules to -include within their contracts as they see fit. We replicated the {solidity-implementation} in Cairo. +[,cairo] +---- +#[starknet::contract] +mod MyERC6909Token { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + // ERC6909 Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } -Aside from the core `ERC6909`, the 3 optional modules that can be imported are: + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } -* `ERC6909Metadata` - Name, symbol, decimals of each token ID -* `ERC6909ContentURI` - The URI of the contract & each token ID -* `ERC6909TokenSupply` - The total supply of each token ID + #[constructor] + fn constructor( + ref self: ContractState, + recipient: ContractAddress, + token_id: u256, + initial_supply: u256, + contract_uri: ByteArray + ) { + self.erc6909.mint(recipient, token_id, initial_supply); + } +} +---- -Each module also has their camelCase counterparts: +`MyERC6909Token` integrates the `ERC6909Impl` with the embed directive which marks the implementation as external in the contract. -* `ERC6909MetadataCamel` -* `ERC6909ContentURICamel` -* `ERC6909TokenSupplyCamel` +There are 3 optional extensions which can also be imported into `MyERC6909Token`. These are: -The contract URI can be set up (ideally) in the constructor via `_set_contract_uri`. +* `ERC6909ContentURI` - Allows to set the base contract URI and thus show individual token URIs. +* `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. +* `ERC6909TokenSupply` - Allows to keep track of individual token supplies upon mints and burns. -Here’s an example of a basic contract which includes the Content URI and Token Supply modules: +Here is an example of how to include the extensions in your ERC6909 contract: [,cairo] ---- #[starknet::contract] -mod MyToken { +mod MyTokenWithSupplyAndURI { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; use starknet::ContractAddress; + // 1. Declare the 2 additional components component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); + component!(path: ERC6909TokenSupplyComponent, storage: erc6909_token_supply, event: ERC6909TokenSupplyEvent); - // ERC6909 Mixin + // 2. Mark their implementation as external to expose their functions #[abi(embed_v0)] impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - - // Optional to keep track of token supplies and URIs. - // In this case we only use the snake_case implementations. #[abi(embed_v0)] - impl ERC6909TokenSupplyImpl = ERC6909Component::ERC6909TokenSupplyImpl; + impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; #[abi(embed_v0)] - impl ERC6909ContentURIImpl = ERC6909Component::ERC6909ContentURIImpl; + impl ERC6909TokenSupplyComponentImpl = ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + // 3. Include their storage #[storage] struct Storage { #[substorage(v0)] - erc6909: ERC6909Component::Storage + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + erc6909_token_supply: ERC6909TokenSupplyComponent::Storage, + #[substorage(v0)] + erc6909_content_uri: ERC6909TokenURIComponent::Storage, } + // 4. Include their events #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] ERC6909Event: ERC6909Component::Event + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event + #[flat] + ERC6909TokenSupplyEvent: ERC6909TokenSupplyComponent::Event } + // Set the contract uri in the constructor (ideally) #[constructor] fn constructor( ref self: ContractState, @@ -82,12 +125,17 @@ mod MyToken { initial_supply: u256, contract_uri: ByteArray ) { - self.erc6909._set_contract_uri(contract_uri); self.erc6909.mint(recipient, token_id, initial_supply); + self.erc6909_content_uri.initializer(contract_uri); } } ---- + +While the `ERC20MetadataImpl` is optional, it's generally recommended to include it because the vast majority of ERC20 tokens provide the metadata methods. +The above example also includes the `ERC20InternalImpl` instance. +This allows the contract's constructor to initialize the contract and create an initial supply of tokens. + `MyToken` integrates the `ERC6909MixinImpl` along with the optional `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl`. The embed directives mark the implementations as external in the contract. While the `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` are optional, it's generally recommended to include them to keep track of individual token supplies and URIs. The above example also includes the `ERC6909InternalImpl` instance, allowing the contract's constructor to set the `contract_uri` and mint an initial supply of tokens. diff --git a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo index e15b5eb54..23ef59db6 100644 --- a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo @@ -1,7 +1,7 @@ #[starknet::contract] pub(crate) mod DualCaseERC6909TokenSupplyMock { use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent::InternalTrait as ERC6909TokenSupplyInternalTrait; + use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent::InternalTrait; use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; use starknet::ContractAddress; From e19c24f1974d075b45f24efa235f3e10975a2071 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Sat, 6 Jul 2024 20:48:03 +0200 Subject: [PATCH 14/44] update docs --- docs/modules/ROOT/pages/erc6909.adoc | 84 ++++++++----------- .../extensions/erc6909_content_uri.cairo | 2 +- 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index cf3ad7656..c266bf1f2 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -62,97 +62,89 @@ mod MyERC6909Token { `MyERC6909Token` integrates the `ERC6909Impl` with the embed directive which marks the implementation as external in the contract. -There are 3 optional extensions which can also be imported into `MyERC6909Token`. These are: +There are 3 optional extensions which can also be imported into `MyERC6909Token`: * `ERC6909ContentURI` - Allows to set the base contract URI and thus show individual token URIs. * `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. * `ERC6909TokenSupply` - Allows to keep track of individual token supplies upon mints and burns. -Here is an example of how to include the extensions in your ERC6909 contract: +Here is an example of how to include the content URI component in your ERC6909 contract: [,cairo] ---- #[starknet::contract] -mod MyTokenWithSupplyAndURI { - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use starknet::ContractAddress; +pub(crate) mod MyERC6909TokenWithURI { + // 1. Import `ERC6909Component` and `ERC6909ContentURIComponent` with trait to use internal functions + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::ERC6909HooksEmptyImpl; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent::InternalTrait; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; - // 1. Declare the 2 additional components + use starknet::{ContractAddress, get_caller_address}; + + // 2. Declare both components component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); - component!(path: ERC6909TokenSupplyComponent, storage: erc6909_token_supply, event: ERC6909TokenSupplyEvent); - // 2. Mark their implementation as external to expose their functions + // 3. Embed both component implementations to expose their external functions #[abi(embed_v0)] impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; #[abi(embed_v0)] impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; - #[abi(embed_v0)] - impl ERC6909TokenSupplyComponentImpl = ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; - impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl InternalImpl = ERC6909Component::InternalImpl; - // 3. Include their storage + // 4. Set substorage, allowing the contract to have indirect access to each component’s storage #[storage] struct Storage { #[substorage(v0)] erc6909: ERC6909Component::Storage, #[substorage(v0)] - erc6909_token_supply: ERC6909TokenSupplyComponent::Storage, - #[substorage(v0)] - erc6909_content_uri: ERC6909TokenURIComponent::Storage, + erc6909_content_uri: ERC6909ContentURIComponent::Storage, } - // 4. Include their events + // 5. Flatten the component events to remove the first key in the event logs, which is the component ID #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] - ERC6909Event: ERC6909Component::Event + ERC6909Event: ERC6909Component::Event, #[flat] - ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event - #[flat] - ERC6909TokenSupplyEvent: ERC6909TokenSupplyComponent::Event + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, } - // Set the contract uri in the constructor (ideally) #[constructor] fn constructor( - ref self: ContractState, - recipient: ContractAddress, - token_id: u256, - initial_supply: u256, - contract_uri: ByteArray + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray ) { - self.erc6909.mint(recipient, token_id, initial_supply); - self.erc6909_content_uri.initializer(contract_uri); + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri) } } ---- +`MyERC6909TokenWithURI` integrates both the `ERC6909Impl` and `ERC6909ContentURIImpl` with the embed directive which marks the implementations as external in the contract. +The above example also includes 2 internal implementations. +The `ERC6909InternalImpl` allows contract's constructor to create an initial supply of tokens via ERC6909's `mint` method. +The `ERC6909ContentURIComponent::InternalTrait` similarly allows the contract's constructor to set the URI via ERC6909ContentURI's `initializer` method -While the `ERC20MetadataImpl` is optional, it's generally recommended to include it because the vast majority of ERC20 tokens provide the metadata methods. -The above example also includes the `ERC20InternalImpl` instance. -This allows the contract's constructor to initialize the contract and create an initial supply of tokens. - -`MyToken` integrates the `ERC6909MixinImpl` along with the optional `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl`. The embed directives mark the implementations as external in the contract. -While the `ERC6909TokenSupplyImpl` and `ERC6909ContentURIImpl` are optional, it's generally recommended to include them to keep track of individual token supplies and URIs. -The above example also includes the `ERC6909InternalImpl` instance, allowing the contract's constructor to set the `contract_uri` and mint an initial supply of tokens. +// TODO +TIP: For a more complete guide on ERC6909 token mechanisms, see {erc6909-supply}. == Interface -:erc6909-component: xref:/api/erc6909.adoc#ERC6909Component[ERC6909Component] :dual-interfaces: xref:/interfaces.adoc#dual_interfaces[Dual interfaces] :ierc6909-interface: xref:/api/erc6909.adoc#IERC6909[IERC6909] +:ierc6909_metadata-interface: xref:/api/erc6909.adoc#IERC6909Metadata[IERC6909Metadata] +:ierc6909_tokensupply-interface: xref:/api/erc6909.adoc#IERC6909TokenSupply[IERC6909TokenSupply] +:ierc6909_contenturi-interface: xref:/api/erc6909.adoc#IERC6909ContentURI[IERC6909ContentURI] +:erc6909-component: xref:/api/erc6909.adoc#ERC6909Component[ERC6909Component] -:ierc6909-supply: xref:/guides/ierc6909-supply.adoc[IERC6909TokenSupply] -:ierc6909-content: xref:/guides/ierc6909-content.adoc[IERC6909ContentURI] -:ierc6909-metadata: xref:/guides/erc6909-metadata.adoc[IERC6909Metadata] +// TODO? +//:erc6909-supply: xref:/guides/erc20-supply.adoc[Creating ERC20 Supply] The following interface represents the full ABI of the Contracts for Cairo {erc6909-component}. -The interface includes the {ierc6909-interface} standard interface and the optional {ierc6909-metadata}, {ierc6909-supply} and {ierc6909-content}. - +The interface includes the {ierc6909-interface} standard interface. To support older token deployments, as mentioned in {dual-interfaces}, the component also includes an implementation of the interface written in camelCase. [,cairo] @@ -192,24 +184,22 @@ pub trait ERC6909ABI { Although Starknet is not EVM compatible, this component aims to be as close as possible to the ERC6909 token standard. Some notable differences, however, can still be found, such as: -* The `ByteArray` type is used to represent strings in Cairo. +* The `ByteArray` type is used to represent strings in Cairo in the Metadata extension. * The `felt252` type is used to represent the `byte4` interface ID. The {interface-id} is also calculated different in Cairo. * The component offers a {dual-interface} which supports both snake_case and camelCase methods, as opposed to just camelCase in Solidity. * `transfer`, `transfer_from` and `approve` will never return anything different from `true` because they will revert on any error. == Customizing Token Metadata -Since ERC6909 is a multi-token standard, instead of having a single `name`, `decimals`, and `symbol` functions for the entire token contract, +Since ERC6909 is a multi-token standard, instead of having a single `name`, `decimals`, and `symbol`, the optional `IERC6909Metadata` module defines these metadata properties for each token ID individually. There are 3 internal methods which can be used to set individual id metadata: `_set_token_name(id, name)`, `_set_token_symbol(id, symbol)` and `_set_token_decimals(id, decimals)`. -Developers can also just set a single `name`, `decimals` and `symbol` for the whole contract which might prove to be simpler (just like in the ERC20 standard). - [,cairo] ---- #[starknet::contract] -mod MyToken { +mod MyERC6909TokenWithMetadata { use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; use starknet::ContractAddress; diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index 32ab38bd4..2a0948c52 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -5,7 +5,7 @@ use starknet::ContractAddress; /// # ERC6909ContentURI Component /// -/// The ERC6909Content component allows to set the contract and token ID URIs. +/// The ERC6909ContentURI component allows to set the contract and token ID URIs. #[starknet::component] pub mod ERC6909ContentURIComponent { use openzeppelin::token::erc6909::ERC6909Component; From 09e16af4f0842a7158400621c5e29c5523c9dc25 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Sun, 7 Jul 2024 16:55:56 +0200 Subject: [PATCH 15/44] add metadata hook --- .../src/mocks/erc6909_metadata_mocks.cairo | 35 ++++++++++++- .../erc6909/extensions/erc6909_metadata.cairo | 51 +++++++++++++++++++ .../tests/erc6909/test_erc6909_metadata.cairo | 12 ++--- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo index e48503224..b0eb84306 100644 --- a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo @@ -1,9 +1,10 @@ #[starknet::contract] pub(crate) mod DualCaseERC6909MetadataMock { - use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent::InternalTrait as ERC6909MetadataInternalTrait; + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent::InternalTrait; use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; use starknet::ContractAddress; + component!( path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent ); @@ -40,4 +41,34 @@ pub(crate) mod DualCaseERC6909MetadataMock { fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { self.erc6909.mint(receiver, id, amount); } + + impl ERC6909MetadataHooksImpl< + TContractState, + impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, + impl HasComponent: ERC6909Component::HasComponent, + +Drop + > of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + /// Update after any transfer + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) { + let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); + let name = "MyERC6909Token"; + let symbol = "MET"; + let decimals = 18; + erc6909_metadata_component._update_token_metadata(from, id, name, symbol, decimals); + } + } } diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index 68d9345d7..3acd98bab 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -8,8 +8,10 @@ use starknet::ContractAddress; /// The ERC6909Metadata component allows to set metadata to the individual token IDs. #[starknet::component] pub mod ERC6909MetadataComponent { + use core::num::traits::Zero; use openzeppelin::token::erc6909::ERC6909Component; use openzeppelin::token::erc6909::interface; + use starknet::ContractAddress; #[storage] struct Storage { @@ -56,6 +58,55 @@ pub mod ERC6909MetadataComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of InternalTrait { + /// @notice Updates the total supply of a token ID. To keep track of token ID supplies, + /// @dev ideally this function should be called in a `before_update` or `after_update` hook. + /// @param sender The address of the sender + /// @param id The ID of the token + /// @param name The name of the token + /// @param symbol The symbol of the token + /// @param decimals The decimals of the token + fn _update_token_metadata( + ref self: ComponentState, + sender: ContractAddress, + id: u256, + name: ByteArray, + symbol: ByteArray, + decimals: u8 + ) { + let zero_address = Zero::zero(); + + // In case of new ID mints update the token metadata + if (sender == zero_address) { + let token_exists = self._token_exists(id); + if (!token_exists) { + self._set_token_metadata(id, name, symbol, decimals) + } + } + } + + /// @notice Checks if a token has metadata at the time of minting + /// @param id The ID of the token + /// @return Whether or not the token has metadata + fn _token_exists(self: @ComponentState, id: u256) -> bool { + return self.ERC6909Metadata_name.read(id).len() > 0; + } + + /// @notice Updates the token metadata for `id` + /// @param id The ID of the token + /// @param name The name of the token + /// @param decimals The decimals of the token + fn _set_token_metadata( + ref self: ComponentState, + id: u256, + name: ByteArray, + symbol: ByteArray, + decimals: u8 + ) { + self._set_token_name(id, name); + self._set_token_symbol(id, symbol); + self._set_token_decimals(id, decimals); + } + /// Sets the token name. fn _set_token_name(ref self: ComponentState, id: u256, name: ByteArray) { self.ERC6909Metadata_name.write(id, name); diff --git a/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo b/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo index a7390310d..5bbcc101e 100644 --- a/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo +++ b/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo @@ -44,34 +44,32 @@ fn setup() -> (ComponentState, DualCaseERC6909MetadataMock::ContractState) { (state, mock_state) } -// // Getters -// +// The mocks use this metadata +// Check that minting a token updates the metadata using the ERC6909Hooks #[test] fn test_name() { let (mut state, _) = setup(); let mut name = state.ERC6909Metadata_name.read(TOKEN_ID); - assert_eq!(name, ""); + assert_eq!(name, "MyERC6909Token"); } #[test] fn test_symbol() { let (mut state, _) = setup(); let mut symbol = state.ERC6909Metadata_symbol.read(TOKEN_ID); - assert_eq!(symbol, ""); + assert_eq!(symbol, "MET"); } #[test] fn test_decimals() { let (mut state, _) = setup(); let mut decimals = state.ERC6909Metadata_decimals.read(TOKEN_ID); - assert_eq!(decimals, 0); + assert_eq!(decimals, 18); } -// // internal setters -// #[test] fn test_set_name() { From 3896dc7f6acf09c3cf7d8aac6bf2840d33f4b399 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Sun, 7 Jul 2024 19:59:08 +0200 Subject: [PATCH 16/44] refactor `metadata` and add comments and docs --- docs/modules/ROOT/pages/erc6909.adoc | 162 ++---------------- .../ROOT/pages/guides/erc6909-extensions.adoc | 0 .../extensions/erc6909_content_uri.cairo | 4 +- .../erc6909/extensions/erc6909_metadata.cairo | 51 +++--- .../extensions/erc6909_token_supply.cairo | 10 +- 5 files changed, 49 insertions(+), 178 deletions(-) create mode 100644 docs/modules/ROOT/pages/guides/erc6909-extensions.adoc diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index c266bf1f2..83bcfaa77 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -15,10 +15,13 @@ required to manage multiple tokens under the same contract." {eip-6909} == Usage -:solidity-implementation: https://github.com/jtriley-eth/ERC-6909/tree/main/src[sample Solidity implementations] +:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] +:erc20-extensions: xref:/guides/erc6909-extensions.adoc[ERC6909 Extensions] + +The ERC6909 minimal multi token standard is a specification for {fungibility-agnostic} token contracts. Using Contracts for Cairo, constructing an ERC6909 contract requires integrating the `ERC6909Component`. -Here's what that looks like: +Here's an example of a basic ERC6909 contract: [,cairo] ---- @@ -60,7 +63,10 @@ mod MyERC6909Token { } ---- -`MyERC6909Token` integrates the `ERC6909Impl` with the embed directive which marks the implementation as external in the contract. +`MyERC6909Token` integrates the `ERC6909Impl` with the embed directive which marks the implementation as external in the contract +by importing the `ERC6909Mixin` which has both camel and snake-case functions. + +The above example also includes the `ERC6909InternalImpl` instance to access internal functions (such as `mint`) There are 3 optional extensions which can also be imported into `MyERC6909Token`: @@ -68,68 +74,7 @@ There are 3 optional extensions which can also be imported into `MyERC6909Token` * `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. * `ERC6909TokenSupply` - Allows to keep track of individual token supplies upon mints and burns. -Here is an example of how to include the content URI component in your ERC6909 contract: - -[,cairo] ----- -#[starknet::contract] -pub(crate) mod MyERC6909TokenWithURI { - // 1. Import `ERC6909Component` and `ERC6909ContentURIComponent` with trait to use internal functions - use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::ERC6909HooksEmptyImpl; - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent::InternalTrait; - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; - - use starknet::{ContractAddress, get_caller_address}; - - // 2. Declare both components - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); - - // 3. Embed both component implementations to expose their external functions - #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - #[abi(embed_v0)] - impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; - - impl InternalImpl = ERC6909Component::InternalImpl; - - // 4. Set substorage, allowing the contract to have indirect access to each component’s storage - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage, - #[substorage(v0)] - erc6909_content_uri: ERC6909ContentURIComponent::Storage, - } - - // 5. Flatten the component events to remove the first key in the event logs, which is the component ID - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event, - #[flat] - ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, - } - - #[constructor] - fn constructor( - ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray - ) { - self.erc6909.mint(receiver, id, amount); - self.erc6909_content_uri.initializer(uri) - } -} ----- - -`MyERC6909TokenWithURI` integrates both the `ERC6909Impl` and `ERC6909ContentURIImpl` with the embed directive which marks the implementations as external in the contract. -The above example also includes 2 internal implementations. -The `ERC6909InternalImpl` allows contract's constructor to create an initial supply of tokens via ERC6909's `mint` method. -The `ERC6909ContentURIComponent::InternalTrait` similarly allows the contract's constructor to set the URI via ERC6909ContentURI's `initializer` method - -// TODO -TIP: For a more complete guide on ERC6909 token mechanisms, see {erc6909-supply}. +TIP: For a more complete guide on using these extensions, see {erc6909-extensions}. == Interface @@ -140,11 +85,9 @@ TIP: For a more complete guide on ERC6909 token mechanisms, see {erc6909-supply} :ierc6909_contenturi-interface: xref:/api/erc6909.adoc#IERC6909ContentURI[IERC6909ContentURI] :erc6909-component: xref:/api/erc6909.adoc#ERC6909Component[ERC6909Component] -// TODO? -//:erc6909-supply: xref:/guides/erc20-supply.adoc[Creating ERC20 Supply] - The following interface represents the full ABI of the Contracts for Cairo {erc6909-component}. The interface includes the {ierc6909-interface} standard interface. + To support older token deployments, as mentioned in {dual-interfaces}, the component also includes an implementation of the interface written in camelCase. [,cairo] @@ -188,86 +131,3 @@ Some notable differences, however, can still be found, such as: * The `felt252` type is used to represent the `byte4` interface ID. The {interface-id} is also calculated different in Cairo. * The component offers a {dual-interface} which supports both snake_case and camelCase methods, as opposed to just camelCase in Solidity. * `transfer`, `transfer_from` and `approve` will never return anything different from `true` because they will revert on any error. - -== Customizing Token Metadata - -Since ERC6909 is a multi-token standard, instead of having a single `name`, `decimals`, and `symbol`, -the optional `IERC6909Metadata` module defines these metadata properties for each token ID individually. - -There are 3 internal methods which can be used to set individual id metadata: `_set_token_name(id, name)`, `_set_token_symbol(id, symbol)` and `_set_token_decimals(id, decimals)`. - -[,cairo] ----- -#[starknet::contract] -mod MyERC6909TokenWithMetadata { - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - - // ERC6909 Mixin - #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - - // Optional to keep track of token supplies and URIs. - // In this case we only use the snake_case implementations. - #[abi(embed_v0)] - impl ERC6909TokenSupplyImpl = ERC6909Component::ERC6909TokenSupplyImpl; - #[abi(embed_v0)] - impl ERC6909ContentURIImpl = ERC6909Component::ERC6909ContentURIImpl; - - impl ERC6909InternalImpl = ERC6909Component::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - recipient: ContractAddress, - token_id: u256, - initial_supply: u256, - contract_uri: ByteArray - ) { - self.erc6909._set_contract_uri(contract_uri); - self.erc6909.mint(recipient, token_id, initial_supply); - } - - #[abi(per_item)] - #[generate_trait] - impl MetadataImpl of MetadataTrait { - #[external(v0)] - fn name(self: @ContractState) -> ByteArray { - "MyToken" - } - - #[external(v0)] - fn symbol(self: @ContractState) -> ByteArray { - "MTK" - } - - #[external(v0)] - fn decimals(self: @ContractState) -> u8 { - 18 - } - } -} ----- - -== Storing ERC6909 URIs - -Token URI and Contract URI are also not part of the EIP. To implement these, the implementation `ERC6909ContentURIImpl` must be imported in the token contract. The contract URI -ideally would be initialized in the constructor via `_set_contract_uri` as shown above. - -The base URI is stored as a ByteArray and the full token URI is returned as the ByteArray concatenation of the base URI and the token ID through the token_uri method. -This design mirrors OpenZeppelin’s default Solidity implementation for ERC721. diff --git a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc new file mode 100644 index 000000000..e69de29bb diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index 2a0948c52..f8261b14c 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -6,6 +6,7 @@ use starknet::ContractAddress; /// # ERC6909ContentURI Component /// /// The ERC6909ContentURI component allows to set the contract and token ID URIs. +/// The internal function `initializer` should be used ideally in the constructor. #[starknet::component] pub mod ERC6909ContentURIComponent { use openzeppelin::token::erc6909::ERC6909Component; @@ -51,7 +52,8 @@ pub mod ERC6909ContentURIComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of InternalTrait { - /// Sets the base URI. + /// @notice Sets the base URI. + /// @param contract_uri The base contract URI fn initializer(ref self: ComponentState, contract_uri: ByteArray) { self.ERC6909ContentURI_contract_uri.write(contract_uri); } diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index 3acd98bab..0ec1ed461 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -6,6 +6,7 @@ use starknet::ContractAddress; /// # ERC6909Metadata Component /// /// The ERC6909Metadata component allows to set metadata to the individual token IDs. +/// The internal function `_update_token_metadata` should be used inside the ERC6909 Hooks. #[starknet::component] pub mod ERC6909MetadataComponent { use core::num::traits::Zero; @@ -58,13 +59,13 @@ pub mod ERC6909MetadataComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of InternalTrait { - /// @notice Updates the total supply of a token ID. To keep track of token ID supplies, - /// @dev ideally this function should be called in a `before_update` or `after_update` hook. - /// @param sender The address of the sender - /// @param id The ID of the token - /// @param name The name of the token - /// @param symbol The symbol of the token - /// @param decimals The decimals of the token + /// @notice Updates the metadata of a token ID. + /// @notice Ideally this function should be called in a `before_update` or `after_update` hook during mints. + /// @param sender The address of the sender. + /// @param id The ID of the token. + /// @param name The name of the token. + /// @param symbol The symbol of the token. + /// @param decimals The decimals of the token. fn _update_token_metadata( ref self: ComponentState, sender: ContractAddress, @@ -77,24 +78,24 @@ pub mod ERC6909MetadataComponent { // In case of new ID mints update the token metadata if (sender == zero_address) { - let token_exists = self._token_exists(id); - if (!token_exists) { + let token_metadata_exists = self._token_metadata_exists(id); + if (!token_metadata_exists) { self._set_token_metadata(id, name, symbol, decimals) } } } - /// @notice Checks if a token has metadata at the time of minting - /// @param id The ID of the token - /// @return Whether or not the token has metadata - fn _token_exists(self: @ComponentState, id: u256) -> bool { + /// @notice Checks if a token has metadata at the time of minting. + /// @param id The ID of the token. + /// @return Whether or not the token has metadata. + fn _token_metadata_exists(self: @ComponentState, id: u256) -> bool { return self.ERC6909Metadata_name.read(id).len() > 0; } - /// @notice Updates the token metadata for `id` - /// @param id The ID of the token - /// @param name The name of the token - /// @param decimals The decimals of the token + /// @notice Updates the token metadata for `id`. + /// @param id The ID of the token. + /// @param name The name of the token. + /// @param decimals The decimals of the token. fn _set_token_metadata( ref self: ComponentState, id: u256, @@ -107,19 +108,23 @@ pub mod ERC6909MetadataComponent { self._set_token_decimals(id, decimals); } - /// Sets the token name. + /// @notice Sets the token name. + /// @param id The id of the token. + /// @param name The name of the token. fn _set_token_name(ref self: ComponentState, id: u256, name: ByteArray) { self.ERC6909Metadata_name.write(id, name); } - /// Sets the token symbol. - fn _set_token_symbol( - ref self: ComponentState, id: u256, symbol: ByteArray - ) { + /// @notice Sets the token symbol. + /// @param id The id of the token. + /// @param symbol The symbol of the token. + fn _set_token_symbol(ref self: ComponentState, id: u256, symbol: ByteArray) { self.ERC6909Metadata_symbol.write(id, symbol); } - /// Sets the token decimals. + /// @notice Sets the token decimals. + /// @param id The id of the token. + /// @param decimals The decimals of the token. fn _set_token_decimals(ref self: ComponentState, id: u256, decimals: u8) { self.ERC6909Metadata_decimals.write(id, decimals); } diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index b8eb0e17c..48ed8986c 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -28,7 +28,7 @@ pub mod ERC6909TokenSupplyComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of IERC6909TokenSupply> { - /// @notice Total supply of a token + /// @notice Total supply of a token. /// @param id The id of the token. /// @return The total supply of the token. fn total_supply(self: @ComponentState, id: u256) -> u256 { @@ -48,8 +48,12 @@ pub mod ERC6909TokenSupplyComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of InternalTrait { - /// @notice Updates the total supply of a token ID. To keep track of token ID supplies, - /// @dev ideally this function should be called in a `before_update` or `after_update` hook. + /// @notice Updates the total supply of a token ID. + /// @notice Ideally this function should be called in a `before_update` or `after_update` hook during mints and burns. + /// @param sender The address of the sender. + /// @param receiver The address of the receiver. + /// @param id The ID of the token. + /// @param amount The amount being minted or burnt. fn _update_token_supply( ref self: ComponentState, sender: ContractAddress, From fdef4df4206af54c72f952db1ea5021ab6ead782 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Sun, 7 Jul 2024 21:00:52 +0200 Subject: [PATCH 17/44] update documentation --- docs/modules/ROOT/pages/erc6909.adoc | 2 +- .../ROOT/pages/guides/erc6909-extensions.adoc | 322 ++++++++++++++++++ 2 files changed, 323 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index 83bcfaa77..2fcb36625 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -16,7 +16,7 @@ required to manage multiple tokens under the same contract." {eip-6909} == Usage :eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] -:erc20-extensions: xref:/guides/erc6909-extensions.adoc[ERC6909 Extensions] +:erc6909-extensions: xref:/guides/erc6909-extensions.adoc[ERC6909 Extensions] The ERC6909 minimal multi token standard is a specification for {fungibility-agnostic} token contracts. diff --git a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc index e69de29bb..30d5f92ba 100644 --- a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc +++ b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc @@ -0,0 +1,322 @@ += ERC6909 Extensions + +:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] + +{eip-6909} is a multi-token standard with functionality similar to ERC20s, but does not define +certain characteristics typically found across fungible tokens: Such as metadata and +token supplies. + +There are 3 optional extensions which can also be imported into `MyERC6909Token` out of the box: + +* `ERC6909ContentURI` - Allows to set the base contract URI and thus show individual token URIs. +* `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. +* `ERC6909TokenSupply` - Allows to keep track of individual token supplies upon mints and burns. + +The `ERC6909Component` always requires for hooks to be implemented. In the case of the first extension +(Content URI) simply importing the `HooksEmptyImpl` is enough. The other extensions make use of hooks +so we must implement these. + +This guide will go over these extensions and how to integrate them into your `ERC6909` contracts, with an example +for each component integration. + + +== ERC6909 Content URI + +Let's say we want to create a ERC6909 token named `MyERC6909TokenWithURI` with a contract URI. As explained the +contract URI is not part of the {eip-6909} but rather an optional extension. Therefore to achieve +this we can make use of the `ERC6909ContentURI` extension. + +[,cairo] +---- +#[starknet::contract] +pub mod MyERC6909ContentURI { + // 1. Import the Content URI Component + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; + use starknet::ContractAddress; + + // 2. Declare the component to access its storage and events + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); + + // 3. Embed ABI to access external functions + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; + + // 4. Implement internal implementations to access internal functions + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; + + // 5. Include component storage and events + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event, + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + } + + // 6. Initialize contract URI in the constructor via the component's internal `initializer` function + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray) { + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri); + } +} +---- + +There's a few things happening in our contract so let's go from the beginning. + +To include the URI extension we must import the `ERC6909ContentURI` component (along with the `ERC6909` base component). + +The `ERC6909Component` always requires us to implement the hooks, we are simply importing the `ERC6909HooksEmptyImpl` as we do not +require any hooks for our token, so importing empty hooks suffices in this case. + +Once imported, we declare both components with `component!(path, storage, events)`. +This tells the compiler to generate an implementation for `HasComponent`, constructing the component state from the associated storage and event types (step 5). + +We then embed the ABI for both components so each function in the implementation is now accessible externally, and the impl/interface are reflected in the ABI. +Notice that we are also implementing the `ERC6909InternalImpl` and `ERC6909COntentURIInternalImpl` to access the internal functions of each component (such as `mint` or `initializer`). + +Finally, in the constructor we mint an initial token supply to `receiver` and set the contract URI via `ERC6909ContentURIComponent` initializer. Notice that the `initializer` +function is called in the constructor in this case, but since it is an internal function it can be called anytime, however it is usually recomended to set it once in the +constructor to not be accessible again. + +== ERC6909 Metadata + +Now let's say we want to add Metadata to our token. To do this we can import the `ERC6909MetadataComponent`. Since ERC6909 is a multi-token standard, +each token ID can have different metadata associated with it! + +To set the individual token IDs metadata we have two options: + +* Set the metadata during mints via hooks +* Set the metadata for each token manually + +The easiest way to set the metadata is via hooks. To do so, we import the `ERC6909MetadataComponent` and follow the same steps as above, with one small +exception: We do not import the `ERC6909EmptyHooksImpl` and instead we define the logic ourselves. Here's what it would look like: + +[,cairo] +---- +#[starknet::contract] +pub mod MyERC6909TokenMetadata { + // 1. Import the Metadata Component + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; + use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; + use starknet::ContractAddress; + + // 2. Declare the component to access its storage and events + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); + component!(path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent); + + // 3. Embed ABI to access external functions + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; + #[abi(embed_v0)] + impl ERC6909MetadataComponentImpl = ERC6909MetadataComponent::ERC6909MetadataImpl; + + // 4. Implement internal implementations to access internal functions + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; + impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; + + // 5. Include component storage and events + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + #[substorage(v0)] + erc6909_metadata: ERC6909MetadataComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event, + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + #[flat] + ERC6909MetadataEvent: ERC6909MetadataComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray + ) { + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri); + } + + // 6. Use the `_update_token_metadata` internal function to update token metadata during mints + impl ERC6909MetadataHooksImpl< + TContractState, + impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, + impl HasComponent: ERC6909Component::HasComponent, + +Drop + > of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) { + let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); + + let name = "MyERC6909Token"; + let symbol = "MET"; + let decimals = 18; + + // `_update_token_metadata` is only called if this is a mint + erc6909_metadata_component._update_token_metadata(from, id, name, symbol, decimals); + } + } +} +--- + +The `ERC6909Metadata` component has a function to check and update metadata if it hasn't been set yet. The `_update_token_metadata` +updates token metadata only upon mints, not transfers or burns. Thus while minting a new token ID, if it has not metadata associated with it +we can make use of the `after_update` hook to set the new metadata. + +In this case we used a fixed name and symbol, but during the hook you could define your own logic. For example, if the underlying deposit +is something like an LP Token, you could get the symbol of each token in the LP and use both as symbol, etc. + +The rest of the contract is identical to the `ContentURI` implementation shown above. + +== ERC6909 Token Supply + +Keeping track of each token ID supply in our ERC6909 contract is also possible by importing the `ERC6909TokenSupplyComponent` extension . The mechanism is the same as +the `ERC6909Metadata` implementation. + +The `ERC6909TokenSupplyComponent` implementation has a function to be used in the ERC6909 hooks to update supply upon mints and burns. + +Here is an example of how to implement it: + +[,cairo] +---- +#[starknet::contract] +pub mod MyERC6909TokenTotalSupply { + // 1. Import the Metadata Component + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; + use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; + use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; + use starknet::ContractAddress; + + // 2. Declare the component to access its storage and events + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); + component!(path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent); + component!(path: ERC6909TokenSupplyComponent, storage: erc6909_token_supply, event: ERC6909TokenSupplyEvent); + + // 3. Embed ABI to access external functions + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; + #[abi(embed_v0)] + impl ERC6909MetadataComponentImpl = ERC6909MetadataComponent::ERC6909MetadataImpl; + #[abi(embed_v0)] + impl ERC6909TokenSupplyComponentImpl = ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; + + // 4. Implement internal implementations to access internal functions + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; + impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; + impl ERC6909TokenSuppplyInternalImpl = ERC6909TokenSupplyComponent::InternalImpl; + + // 5. Include component storage and events + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + #[substorage(v0)] + erc6909_metadata: ERC6909MetadataComponent::Storage, + #[substorage(v0)] + erc6909_token_supply: ERC6909TokenSupplyComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event, + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + #[flat] + ERC6909MetadataEvent: ERC6909MetadataComponent::Event, + #[flat] + ERC6909TokenSupplyEvent: ERC6909TokenSupplyComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray + ) { + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri); + } + + // 6. Use the `_update_token_supply` to update Token ID supply during mints and burns. + impl ERC6909TokenSupplyHooksImpl< + TContractState, + impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, + impl ERC6909TokenSupply: ERC6909TokenSupplyComponent::HasComponent, + impl HasComponent: ERC6909Component::HasComponent, + +Drop + > of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) { + let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); + erc6909_metadata_component._update_token_metadata(from, id, "MyERC6909Token", "MET", 18); + + // Will only update during mints and burns + let mut erc6909_token_supply_component = get_dep_component_mut!(ref self, ERC6909TokenSupply); + erc6909_token_supply_component._update_token_supply(from, recipient, id, amount); + } + } +} +--- + +The logic is the exact same as when implementing the Metadata component. The `ERC6909TokenSupplyComponent` has an internal +function (`_update_token_supply`) which updates the supply of a token ID only upon mints and/or burns. From fa342de1b14d1857fd3ed83111d922bc32f35f64 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Sun, 7 Jul 2024 22:01:07 +0200 Subject: [PATCH 18/44] erc6909 api --- docs/modules/ROOT/pages/api/erc6909.adoc | 506 ++++++----------------- 1 file changed, 135 insertions(+), 371 deletions(-) diff --git a/docs/modules/ROOT/pages/api/erc6909.adoc b/docs/modules/ROOT/pages/api/erc6909.adoc index 20407e46d..b89d0b263 100644 --- a/docs/modules/ROOT/pages/api/erc6909.adoc +++ b/docs/modules/ROOT/pages/api/erc6909.adoc @@ -1,6 +1,8 @@ :github-icon: pass:[] :eip6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] :erc6909-guide: xref:erc6909.adoc[ERC6909 guide] +:casing-discussion: https://github.com/OpenZeppelin/cairo-contracts/discussions/34[here] +//:custom-decimals: xref:/erc20.adoc#customizing_decimals[Customizing decimals] = ERC6909 @@ -59,7 +61,7 @@ Returns the amount owned by `owner` of `id`. Returns the remaining number of `id` tokens that `spender` is allowed to spend on behalf of `owner` through <>. This is zero by default. -This value changes when <> or <> are called. +This value changes when <> or <> are called, unless called by an operator. [.contract-item] [[IERC6909-is_operator]] @@ -116,7 +118,7 @@ Checks if a contract implements `interface_id`. [[IERC6909-Transfer]] ==== `[.contract-item-name]#++Transfer++#++(caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# -Emitted when `amount` of `id` are moved from one address (`sender`) to another (`receiver`). +Emitted when `amount` of `id` are moved from `sender` to `receiver`. Note that `amount` may be zero. @@ -133,54 +135,6 @@ Emitted when the allowance of a `spender` for an `owner` is set over a token `id Emitted when an operator (`spender`) is set or unset for `owner`. `approved` is the new status of the operator. -// [.contract] -// [[IERC6909Metadata]] -// === `++IERC6909Metadata++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc20/interface.cairo#L19[{github-icon},role=heading-link] -// -// [.hljs-theme-dark] -// ```cairo -// use openzeppelin::token::erc20::interface::IERC6909Metadata; -// ``` -// -// Interface for the optional metadata functions in {eip20}. -// -// [.contract-index] -// .Functions -// -- -// * xref:#IERC6909Metadata-name[`++name()++`] -// * xref:#IERC6909Metadata-symbol[`++symbol()++`] -// * xref:#IERC6909Metadata-decimals[`++decimals()++`] -// -- -// -// [#IERC6909Metadata-Functions] -// ==== Functions -// -// [.contract-item] -// [[IERC6909Metadata-name]] -// ==== `[.contract-item-name]#++name++#++() → ByteArray++` [.item-kind]#external# -// -// Returns the name of the token. -// -// [.contract-item] -// [[IERC6909Metadata-symbol]] -// ==== `[.contract-item-name]#++symbol++#++() → ByteArray++` [.item-kind]#external# -// -// Returns the ticker symbol of the token. -// -// [.contract-item] -// [[IERC6909Metadata-decimals]] -// ==== `[.contract-item-name]#++decimals++#++() → u8++` [.item-kind]#external# -// -// Returns the number of decimals the token uses - e.g. `8` means to divide the token amount by `100000000` to get its user-readable representation. -// -// For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`). -// -// Tokens usually opt for a value of `18`, imitating the relationship between Ether and Wei. -// This is the default value returned by this function. -// To create a custom decimals implementation, see {custom-decimals}. -// -// NOTE: This information is only used for _display_ purposes: it in no way affects any of the arithmetic of the contract. - [.contract] [[ERC6909Component]] === `++ERC6909Component++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/erc6909.cairo[{github-icon},role=heading-link] @@ -208,7 +162,6 @@ NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how are hooks used. .ERC6909MixinImpl * xref:#ERC6909Component-Embeddable-Impls-ERC6909Impl[`++ERC6909Impl++`] * xref:#ERC6909Component-Embeddable-Impls-ERC6909CamelOnlyImpl[`++ERC6909CamelOnlyImpl++`] -// * xref:#ERC6909Component-Embeddable-Impls-ERC6909MetadataImpl[`++ERC6909MetadataImpl++`] -- [.contract-index#ERC6909Component-Embeddable-Impls] @@ -225,12 +178,6 @@ NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how are hooks used. * xref:#ERC6909Component-set_operator[`++set_operator(self, spender, approved)++`] * xref:#ERC6909Component-supports_interface[`++supports_interface(self, interface_id)++`] -// [.sub-index#ERC6909Component-Embeddable-Impls-ERC6909MetadataImpl] -// .ERC6909MetadataImpl -// * xref:#ERC6909Component-name[`++name(self)++`] -// * xref:#ERC6909Component-symbol[`++symbol(self)++`] -// * xref:#ERC6909Component-decimals[`++decimals(self)++`] - [.sub-index#ERC6909Component-Embeddable-Impls-ERC6909CamelOnlyImpl] .ERC6909CamelOnlyImpl * xref:#ERC6909Component-balanceOf[`++balanceOf(self, owner, id)++`] @@ -247,14 +194,10 @@ NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how are hooks used. * xref:#ERC6909Component-mint[`++mint(self, receiver, id, amount)++`] * xref:#ERC6909Component-burn[`++burn(self, account, id, amount)++`] * xref:#ERC6909Component-update[`++update(self, caller, sender, receiver, id, amount)++`] -* xref:#ERC6909Component-_transfer[`++_transfer(self, sender, receiver, id, amount)++`] -* xref:#ERC6909Component-_approve[`++_approve(self, owner, spender, id, amount)++`] -* xref:#ERC6909Component-_spend_allowance[`++_spend_allowance(self, owner, spender, id, amount)++`] -* xref:#ERC6909Component-_set_contract_uri[`++_set_contract_uri(self, contract_uri)++`] -* xref:#ERC6909Component-_set_token_name[`++_set_token_name(self, id, name)++`] -* xref:#ERC6909Component-_set_token_symbol[`++_set_token_symbol(self, id, symbol)++`] -* xref:#ERC6909Component-_set_token_decimals[`++_set_token_decimals(self, id, decimals)++`] * xref:#ERC6909Component-_set_operator[`++_set_operator(self, owner, spender, approved)++`] +* xref:#ERC6909Component-_spend_allowance[`++_spend_allowance(self, sender, spender, id, amount)++`] +* xref:#ERC6909Component-_approve[`++_approve(self, owner, spender, id, amount)++`] +* xref:#ERC6909Component-_transfer[`++_approve(self, caller, sender, receiver, id, amount)++`] -- [.contract-index] @@ -290,15 +233,9 @@ Function executed at the end of the xref:#ERC6909Component-update[update] functi [#ERC6909Component-Embeddable-functions] ==== Embeddable functions -//[.contract-item] -//[[ERC6909Component-total_supply]] -//==== `[.contract-item-name]#++total_supply++#++(@self: ContractState, id: u256) → u256++` [.item-kind]#external# -// -//See <>. - [.contract-item] [[ERC6909Component-balance_of]] -==== `[.contract-item-name]#++balance_of++#++(@self: ContractState, account: ContractAddress, id: u256) → u256++` [.item-kind]#external# +==== `[.contract-item-name]#++balance_of++#++(@self: ContractState, owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# See <>. @@ -308,6 +245,12 @@ See <>. See <>. +[.contract-item] +[[ERC6909Component-is_operator]] +==== `[.contract-item-name]#++is_operator++#++(@self: ContractState, owner: ContractAddress, spender: ContractAddress) → bool++` [.item-kind]#external# + +See <>. + [.contract-item] [[ERC6909Component-transfer]] ==== `[.contract-item-name]#++transfer++#++(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# @@ -317,7 +260,7 @@ See <>. Requirements: - `receiver` cannot be the zero address. -- The caller must have a balance of `id` of at least `amount`. +- The caller must have a balance of at least `amount`. [.contract-item] [[ERC6909Component-transfer_from]] @@ -328,9 +271,9 @@ See <>. Requirements: - `sender` cannot be the zero address. -- `sender` must have a balance of `id` of at least `amount`. +- `sender` must have a balance of at least `amount`. - `receiver` cannot be the zero address. -- The caller must have allowance of `id` for ``sender``'s tokens of at least `amount` or be an operator. +- The caller must have allowance for ``sender``'s tokens of at least `amount`. [.contract-item] [[ERC6909Component-approve]] @@ -342,48 +285,58 @@ Requirements: - `spender` cannot be the zero address. -//[.contract-item] -//[[ERC6909Component-name]] -//==== `[.contract-item-name]#++name++#++() → ByteArray++` [.item-kind]#external# -// -//See <>. -// -//[.contract-item] -//[[ERC6909Component-symbol]] -//==== `[.contract-item-name]#++symbol++#++() → ByteArray++` [.item-kind]#external# -// -//See <>. -// -//[.contract-item] -//[[ERC6909Component-decimals]] -//==== `[.contract-item-name]#++decimals++#++() → u8++` [.item-kind]#external# -// -//See <>. -// -//[.contract-item] -//[[ERC6909Component-totalSupply]] -//==== `[.contract-item-name]#++totalSupply++#++(self: @ContractState) → u256++` [.item-kind]#external# -// -//See <>. -// -//Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. +[.contract-item] +[[ERC6909Component-set_operator]] +==== `[.contract-item-name]#++set_operator++#++(ref self: ContractState, spender: ContractAddress, approved: bool) → bool++` [.item-kind]#external# + +See <>. + +[.contract-item] +[[ERC6909Component-supports_interface]] +==== `[.contract-item-name]#++supports_interface++#++(self: @ContractState, interface_id: felt252) → bool++` [.item-kind]#external# + +See <>. [.contract-item] [[ERC6909Component-balanceOf]] -==== `[.contract-item-name]#++balanceOf++#++(self: @ContractState, owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# +==== `[.contract-item-name]#++balanceOf++#++(@self: ContractState, owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# See <>. Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. +[.contract-item] +[[ERC6909Component-isOperator]] +==== `[.contract-item-name]#++isOperator++#++(@self: ContractState, owner: ContractAddress, spender: ContractAddress) → bool++` [.item-kind]#external# + +See <>. + +Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. + [.contract-item] [[ERC6909Component-transferFrom]] -==== `[.contract-item-name]#++transferFrom++#++(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# +==== `[.contract-item-name]#++transferFrom++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# See <>. Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. +[.contract-item] +[[ERC6909Component-setOperator]] +==== `[.contract-item-name]#++setOperator++#++(ref self: ContractState, operator: ContractAddress, approved: bool) → bool++` [.item-kind]#external# + +See <>. + +Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. + +[.contract-item] +[[ERC6909Component-supportsInterface]] +==== `[.contract-item-name]#++supportsInterface++#++(ref self: ContractState, interface_id: felt252) → bool++` [.item-kind]#external# + +See <>. + +Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. + [#ERC6909Component-Internal-functions] ==== Internal functions @@ -393,7 +346,7 @@ Supports the Cairo v0 convention of writing external methods in camelCase as dis Creates an `amount` number of `id` tokens and assigns them to `receiver`. -Emits a <> event with `sender` being the zero address. +Emits a <> event with `from` being the zero address. Requirements: @@ -405,7 +358,7 @@ Requirements: Destroys `amount` number of `id` tokens from `account`. -Emits a <> event with `receiver` set to the zero address. +Emits a <> event with `to` set to the zero address. Requirements: @@ -415,7 +368,7 @@ Requirements: [[ERC6909Component-update]] ==== `[.contract-item-name]#++update++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# -Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or burns) if `sender` (or `receiver`) is +Transfers an `amount` of `id` tokens from `from` to `to`, or alternatively mints (or burns) if `from` (or `to`) is the zero address. NOTE: This function can be extended using the xref:ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait], to add @@ -425,9 +378,9 @@ Emits a <> event. [.contract-item] [[ERC6909Component-_transfer]] -==== `[.contract-item-name]#++_transfer++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# +==== `[.contract-item-name]#++_transfer++#++(ref self: ContractState, caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# -Moves `amount` of `id` tokens from `sender` to `receiver`. +Moves `amount` of `id` tokens from `from` to `to`. This internal function does not check for access permissions but can be useful as a building block, for example to implement automatic token fees, slashing mechanisms, etc. @@ -435,9 +388,9 @@ Emits a <> event. Requirements: -- `sender` cannot be the zero address. -- `receiver` cannot be the zero address. -- `sender` must have a balance of `id` tokens of at least `amount`. +- `from` cannot be the zero address. +- `to` cannot be the zero address. +- `from` must have a balance of `id` tokens of at least `amount`. [.contract-item] [[ERC6909Component-_approve]] @@ -458,9 +411,9 @@ Requirements: [[ERC6909Component-_spend_allowance]] ==== `[.contract-item-name]#++_spend_allowance++#++(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# -Updates ``owner``'s allowance for `spender` for `id` token based on spent `amount`. +Updates ``owner``'s allowance for `spender` based on spent `amount` for `id` tokens. -This internal function does not update the allowance value in the case of infinite allowance or if called by an operator. +This internal function does not update the allowance value in the case of infinite allowance or if spender is operator. Possibly emits an <> event. @@ -475,7 +428,7 @@ See <>. [.contract-item] [[ERC6909Component-Approval]] -==== `[.contract-item-name]#++Approval++#++(owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# +==== `[.contract-item-name]#++Approval++#++(owner: ContractAddress, spender: ContractAddress, value: u256)++` [.item-kind]#event# See <>. @@ -487,262 +440,73 @@ See <>. == Extensions -//[.contract] -//[[ERC6909VotesComponent]] -//=== `++ERC6909VotesComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc20/extensions/erc20_votes.cairo[{github-icon},role=heading-link] -// -//```cairo -//use openzeppelin::token::extensions::ERC6909VotesComponent; -//``` -// -//:DelegateChanged: xref:ERC6909VotesComponent-DelegateChanged[DelegateChanged] -//:DelegateVotesChanged: xref:ERC6909VotesComponent-DelegateVotesChanged[DelegateVotesChanged] -// -//Extension of ERC6909 to support voting and delegation. -// -//NOTE: Implementing xref:#ERC6909Component[ERC6909Component] is a requirement for this component to be implemented. -// -//WARNING: To track voting units, this extension requires that the -//xref:#ERC6909VotesComponent-transfer_voting_units[transfer_voting_units] function is called after every transfer, -//mint, or burn operation. For this, the xref:ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait] must be used. -// -// -//This extension keeps a history (checkpoints) of each account’s vote power. Vote power can be delegated either by calling -//the xref:#ERC6909VotesComponent-delegate[delegate] function directly, or by providing a signature to be used with -//xref:#ERC6909VotesComponent-delegate_by_sig[delegate_by_sig]. Voting power can be queried through the public accessors -//xref:#ERC6909VotesComponent-get_votes[get_votes] and xref:#ERC6909VotesComponent-get_past_votes[get_past_votes]. -// -//By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. -// -//[.contract-index#ERC6909VotesComponent-Embeddable-Impls] -//.Embeddable Implementations -//-- -//[.sub-index#ERC6909VotesComponent-Embeddable-Impls-ERC6909VotesImpl] -//.ERC6909VotesImpl -//* xref:#ERC6909VotesComponent-get_votes[`++get_votes(self, account)++`] -//* xref:#ERC6909VotesComponent-get_past_votes[`++get_past_votes(self, account, timepoint)++`] -//* xref:#ERC6909VotesComponent-get_past_total_supply[`++get_past_total_supply(self, timepoint)++`] -//* xref:#ERC6909VotesComponent-delegates[`++delegates(self, account)++`] -//* xref:#ERC6909VotesComponent-delegate[`++delegate(self, delegatee)++`] -//* xref:#ERC6909VotesComponent-delegate_by_sig[`++delegate_by_sig(self, delegator, delegatee, nonce, expiry, signature)++`] -//-- -// -//[.contract-index] -//.Internal implementations -//-- -//.InternalImpl -//* xref:#ERC6909VotesComponent-get_total_supply[`++get_total_supply(self)++`] -//* xref:#ERC6909VotesComponent-_delegate[`++_delegate(self, account, delegatee)++`] -//* xref:#ERC6909VotesComponent-move_delegate_votes[`++move_delegate_votes(self, from, to, amount)++`] -//* xref:#ERC6909VotesComponent-transfer_voting_units[`++transfer_voting_units(self, from, to, amount)++`] -//* xref:#ERC6909VotesComponent-checkpoints[`++checkpoints(self, account, pos)++`] -//* xref:#ERC6909VotesComponent-get_voting_units[`++get_voting_units(self, account)++`] -//-- -// -//[.contract-index] -//.Events -//-- -//* xref:#ERC6909VotesComponent-DelegateChanged[`++DelegateChanged(delegator, from_delegate, to_delegate)++`] -//* xref:#ERC6909VotesComponent-DelegateVotesChanged[`++DelegateVotesChanged(delegate, previous_votes, new_votes)++`] -//-- -// -//[#ERC6909VotesComponent-Embeddable-functions] -//==== Embeddable functions -// -//[.contract-item] -//[[ERC6909VotesComponent-get_votes]] -//==== `[.contract-item-name]#++get_votes++#++(self: @ContractState, account: ContractAddress) → u256++` [.item-kind]#external# -// -//Returns the current amount of votes that `account` has. -// -//[.contract-item] -//[[ERC6909VotesComponent-get_past_votes]] -//==== `[.contract-item-name]#++get_past_votes++#++(self: @ContractState, account: ContractAddress, timepoint: u64) → u256++` [.item-kind]#external# -// -//Returns the amount of votes that `account` had at a specific moment in the past. -// -//Requirements: -// -//- `timepoint` must be in the past. -// -//[.contract-item] -//[[ERC6909VotesComponent-get_past_total_supply]] -//==== `[.contract-item-name]#++get_past_total_supply++#++(self: @ContractState, timepoint: u64) → u256++` [.item-kind]#external# -// -//Returns the total supply of votes available at a specific moment in the past. -// -//NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. -//Votes that have not been delegated are still part of total supply, even though they would not participate in a -//vote. -// -//[.contract-item] -//[[ERC6909VotesComponent-delegates]] -//==== `[.contract-item-name]#++delegates++#++(self: @ContractState, account: ContractAddress) → ContractAddress++` [.item-kind]#external# -// -//Returns the delegate that `account` has chosen. -// -//[.contract-item] -//[[ERC6909VotesComponent-delegate]] -//==== `[.contract-item-name]#++delegate++#++(ref self: ContractState, delegatee: ContractAddress)++` [.item-kind]#external# -// -//Delegates votes from the caller to `delegatee`. -// -//Emits a {DelegateChanged} event. -// -//May emit one or two {DelegateVotesChanged} events. -// -//[.contract-item] -//[[ERC6909VotesComponent-delegate_by_sig]] -//==== `[.contract-item-name]#++delegate_by_sig++#++(ref self: ContractState, delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Array)++` [.item-kind]#external# -// -//Delegates votes from `delegator` to `delegatee` through a SNIP12 message signature validation. -// -//Requirements: -// -//- `expiry` must not be in the past. -//- `nonce` must match the account's current nonce. -//- `delegator` must implement `SRC6::is_valid_signature`. -//- `signature` should be valid for the message hash. -// -//Emits a {DelegateChanged} event. -// -//May emit one or two {DelegateVotesChanged} events. -// -//[#ERC6909VotesComponent-Internal-functions] -//==== Internal functions -// -//[.contract-item] -//[[ERC6909VotesComponent-get_total_supply]] -//==== `[.contract-item-name]#++get_total_supply++#++(self: @ContractState) → u256++` [.item-kind]#internal# -// -//Returns the current total supply of votes. -// -//[.contract-item] -//[[ERC6909VotesComponent-_delegate]] -//==== `[.contract-item-name]#++_delegate++#++(ref self: ContractState, account: ContractAddress, delegatee: ContractAddress)++` [.item-kind]#internal# -// -//Delegates all of ``account``'s voting units to `delegatee`. -// -//Emits a {DelegateChanged} event. -// -//May emit one or two {DelegateVotesChanged} events. -// -//[.contract-item] -//[[ERC6909VotesComponent-move_delegate_votes]] -//==== `[.contract-item-name]#++move_delegate_votes++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# -// -//Moves `amount` of delegated votes from `from` to `to`. -// -//May emit one or two {DelegateVotesChanged} events. -// -//[.contract-item] -//[[ERC6909VotesComponent-transfer_voting_units]] -//==== `[.contract-item-name]#++transfer_voting_units++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# -// -//Transfers, mints, or burns voting units. -// -//To register a mint, `from` should be zero. To register a burn, `to` -//should be zero. Total supply of voting units will be adjusted with mints and burns. -// -//May emit one or two {DelegateVotesChanged} events. -// -//[.contract-item] -//[[ERC6909VotesComponent-num_checkpoints]] -//==== `[.contract-item-name]#++num_checkpoints++#++(self: @ContractState, account: ContractAddress) → u32++` [.item-kind]#internal# -// -//Returns the number of checkpoints for `account`. -// -//[.contract-item] -//[[ERC6909VotesComponent-checkpoints]] -//==== `[.contract-item-name]#++checkpoints++#++(self: @ContractState, account: ContractAddress, pos: u32) → Checkpoint++` [.item-kind]#internal# -// -//Returns the `pos`-th checkpoint for `account`. -// -//[.contract-item] -//[[ERC6909VotesComponent-get_voting_units]] -//==== `[.contract-item-name]#++get_voting_units++#++(self: @ContractState, account: ContractAddress) → u256++` [.item-kind]#internal# -// -//Returns the voting units of an `account`. -// -//[#ERC6909VotesComponent-Events] -//==== Events -// -//[.contract-item] -//[[ERC6909VotesComponent-DelegateChanged]] -//==== `[.contract-item-name]#++DelegateChanged++#++(delegator: ContractAddress, from_delegate: ContractAddress, to_delegate: ContractAddress)++` [.item-kind]#event# -// -//Emitted when `delegator` delegates their votes from `from_delegate` to `to_delegate`. -// -//[.contract-item] -//[[ERC6909VotesComponent-DelegateVotesChanged]] -//==== `[.contract-item-name]#++DelegateVotesChanged++#++(delegate: ContractAddress, previous_votes: u256, new_votes: u256)++` [.item-kind]#event# -// -//Emitted when `delegate` votes are updated from `previous_votes` to `new_votes`. -// -//== Presets -// -//[.contract] -//[[ERC6909Upgradeable]] -//=== `++ERC6909Upgradeable++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/presets/erc20.cairo[{github-icon},role=heading-link] -// -//```cairo -//use openzeppelin::presets::ERC6909Upgradeable; -//``` -// -//Upgradeable ERC6909 contract leveraging xref:#ERC6909Component[ERC6909Component] with a fixed-supply mechanism for token distribution. -// -//include::../utils/_class_hashes.adoc[] -// -//[.contract-index] -//.{presets-page} -//-- -//{ERC6909Upgradeable-class-hash} -//-- -// -//[.contract-index] -//.Constructor -//-- -//* xref:#ERC6909Upgradeable-constructor[`++constructor(self, name, symbol, fixed_supply, recipient, owner)++`] -//-- -// -//[.contract-index] -//.Embedded Implementations -//-- -//.ERC6909MixinImpl -// -//* xref:#ERC6909Component-Embeddable-Mixin-Impl[`++ERC6909MixinImpl++`] -// -//.OwnableMixinImpl -// -//* xref:/api/access.adoc#OwnableComponent-Mixin-Impl[`++OwnableMixinImpl++`] -//-- -// -//[.contract-index] -//.External Functions -//-- -//* xref:#ERC6909Upgradeable-upgrade[`++upgrade(self, new_class_hash)++`] -//-- -// -//[#ERC6909Upgradeable-constructor-section] -//==== Constructor -// -//[.contract-item] -//[[ERC6909Upgradeable-constructor]] -//==== `[.contract-item-name]#++constructor++#++(ref self: ContractState, name: ByteArray, symbol: ByteArray, fixed_supply: u256, recipient: ContractAddress, owner: ContractAddress)++` [.item-kind]#constructor# -// -//Sets the `name` and `symbol` and mints `fixed_supply` tokens to `recipient`. -//Assigns `owner` as the contract owner with permissions to upgrade. -// -//[#ERC6909Upgradeable-external-functions] -//==== External functions -// -//[.contract-item] -//[[ERC6909Upgradeable-upgrade]] -//==== `[.contract-item-name]#++upgrade++#++(ref self: ContractState, new_class_hash: ClassHash)++` [.item-kind]#external# -// -//Upgrades the contract to a new implementation given by `new_class_hash`. -// -//Requirements: -// -//- The caller is the contract owner. -//- `new_class_hash` cannot be zero. +[.contract] +[[ERC6909ContentURIComponent]] +=== `++ERC6909ContentURIComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/extensions/erc6909_content_uri.cairo[{github-icon},role=heading-link] + +```cairo +use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; +``` + +Extension of ERC6909 to support contract and token URIs. + +NOTE: Implementing xref:#ERC6909Component[ERC6909Component] is a requirement for this component to be implemented. + +This extension allows to set the contract URI (ideally) in the constructor via `initializer(uri: ByteArray)`. + +[.contract-index#ERC6909ContentURIComponent-Embeddable-Impls] +.Embeddable Implementations +-- +[.sub-index#ERC6909ContentURIComponent-Embeddable-Impls-ERC6909ContentURIImpl] +.ERC6909ContentURIImpl +* xref:#ERC6909ContentURIComponent-contract_uri[`++contract_uri(self)++`] +* xref:#ERC6909ContentURIComponent-token_uri[`++token_uri(self, id)++`] +-- + +[.contract-index] +.Internal implementations +-- +.InternalImpl +* xref:#ERC6909ContentURIComponent-initializer[`++initializer(self, contract_uri)++`] +-- + +[#ERC6909ContentURI-Embeddable-functions] +==== Embeddable functions + +[.contract-item] +[[ERC6909ContentURI-contract_uri]] +==== `[.contract-item-name]#++contract_uri++#++(self: @ContractState) → ByteArray++` [.item-kind]#external# + +Returns the contract URI. + +[.contract-item] +[[ERC6909ContentURI-token_uri]] +==== `[.contract-item-name]#++token_uri++#++(self: @ContractState, id: u256) → ByteArray++` [.item-kind]#external# + +Returns the token URI for `id` token + +[#ERC6909ContentURI-Internal-functions] +==== Internal functions + +[.contract-item] +[[ERC6909ContentURI-initializer]] +==== `[.contract-item-name]#++initializer++#++(ref self: ContractState, contract_uri: ByteArray)++` [.item-kind]#internal# + +Initializes the contract URI. +This should be used inside of the contract's constructor. + +[.contract] +[[ERC6909MetadataComponent]] +=== `++ERC6909MetadataComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/extensions/erc6909_metadata.cairo[{github-icon},role=heading-link] + +```cairo +use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; +``` + +Extension of ERC6909 to support contract metadata. + +NOTE: Implementing xref:#ERC6909Component[ERC6909Component] is a requirement for this component to be implemented. + +WARNING: To individual token metadata, this extension requires that the +xref:#ERC6909MetadataComponent-_update_token_metadata[_update_token_metadata] function is called after every mint. For this, the xref:ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait] must be used. + From d34b6e6c6cb7a67d2928d3832509c370139d3c85 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Mon, 8 Jul 2024 21:48:16 +0200 Subject: [PATCH 19/44] make internalimpl mocks consistent --- .../test_common/src/mocks/erc6909_content_uri_mocks.cairo | 5 +++-- .../test_common/src/mocks/erc6909_metadata_mocks.cairo | 8 ++++---- .../src/mocks/erc6909_token_supply_mocks.cairo | 5 +++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo b/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo index 2b0760bbd..452d2f72f 100644 --- a/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo @@ -1,6 +1,5 @@ #[starknet::contract] pub(crate) mod DualCaseERC6909ContentURIMock { - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent::InternalTrait as ERC6909ContentURIInternalTrait; use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; use starknet::ContractAddress; @@ -20,7 +19,9 @@ pub(crate) mod DualCaseERC6909ContentURIMock { // ERC6909Mixin #[abi(embed_v0)] impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - impl InternalImpl = ERC6909Component::InternalImpl; + + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; #[storage] struct Storage { diff --git a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo index b0eb84306..51a009d32 100644 --- a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo @@ -1,7 +1,6 @@ #[starknet::contract] pub(crate) mod DualCaseERC6909MetadataMock { use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent::InternalTrait; use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; use starknet::ContractAddress; @@ -12,13 +11,14 @@ pub(crate) mod DualCaseERC6909MetadataMock { // ERC6909Metadata #[abi(embed_v0)] - impl ERC6909MetadataComponentImpl = - ERC6909MetadataComponent::ERC6909MetadataImpl; + impl ERC6909MetadataComponentImpl = ERC6909MetadataComponent::ERC6909MetadataImpl; // ERC6909Mixin #[abi(embed_v0)] impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - impl InternalImpl = ERC6909Component::InternalImpl; + + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; #[storage] struct Storage { diff --git a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo index 23ef59db6..e0f123575 100644 --- a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo @@ -1,7 +1,6 @@ #[starknet::contract] pub(crate) mod DualCaseERC6909TokenSupplyMock { use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent::InternalTrait; use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; use starknet::ContractAddress; @@ -20,7 +19,9 @@ pub(crate) mod DualCaseERC6909TokenSupplyMock { // ERC6909Mixin #[abi(embed_v0)] impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - impl InternalImpl = ERC6909Component::InternalImpl; + + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909TokenSupplyInternalImpl = ERC6909TokenSupplyComponent::InternalImpl; #[storage] struct Storage { From 91c0f4ee9c28f350080485508c6903c17dde2a08 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Mon, 8 Jul 2024 21:56:39 +0200 Subject: [PATCH 20/44] update erc6909 docs --- docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/guides/erc6909-extensions.adoc | 76 +++++++++++++------ 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index d236bfbab..70666c3a2 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -54,6 +54,7 @@ *** xref:erc4626.adoc[ERC4626] **** xref:/api/erc20.adoc#ERC4626Component[API Reference] *** xref:erc6909.adoc[ERC6909] +**** xref:/guides/erc6909-extensions.adoc[Extensions] **** xref:/api/erc6909.adoc[API Reference] *** xref:/api/token_common.adoc[Common] diff --git a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc index 30d5f92ba..56a1de400 100644 --- a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc +++ b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc @@ -2,11 +2,11 @@ :eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] -{eip-6909} is a multi-token standard with functionality similar to ERC20s, but does not define +{eip-6909} is a fungible-agnostic multi-token standard, but does not define certain characteristics typically found across fungible tokens: Such as metadata and token supplies. -There are 3 optional extensions which can also be imported into `MyERC6909Token` out of the box: +This is why there are 3 optional extensions which can also be imported into `MyERC6909Token` out of the box to be more accessible: * `ERC6909ContentURI` - Allows to set the base contract URI and thus show individual token URIs. * `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. @@ -30,20 +30,25 @@ this we can make use of the `ERC6909ContentURI` extension. ---- #[starknet::contract] pub mod MyERC6909ContentURI { + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; // 1. Import the Content URI Component use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; use starknet::ContractAddress; // 2. Declare the component to access its storage and events component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); + component!( + path: ERC6909ContentURIComponent, + storage: erc6909_content_uri, + event: ERC6909ContentURIEvent + ); // 3. Embed ABI to access external functions #[abi(embed_v0)] impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; #[abi(embed_v0)] - impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; + impl ERC6909ContentURIComponentImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; // 4. Implement internal implementations to access internal functions impl ERC6909InternalImpl = ERC6909Component::InternalImpl; @@ -69,7 +74,9 @@ pub mod MyERC6909ContentURI { // 6. Initialize contract URI in the constructor via the component's internal `initializer` function #[constructor] - fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray) { + fn constructor( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray + ) { self.erc6909.mint(receiver, id, amount); self.erc6909_content_uri.initializer(uri); } @@ -118,16 +125,24 @@ pub mod MyERC6909TokenMetadata { // 2. Declare the component to access its storage and events component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); - component!(path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent); + component!( + path: ERC6909ContentURIComponent, + storage: erc6909_content_uri, + event: ERC6909ContentURIEvent + ); + component!( + path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent + ); // 3. Embed ABI to access external functions #[abi(embed_v0)] impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; #[abi(embed_v0)] - impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; + impl ERC6909ContentURIComponentImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; #[abi(embed_v0)] - impl ERC6909MetadataComponentImpl = ERC6909MetadataComponent::ERC6909MetadataImpl; + impl ERC6909MetadataComponentImpl = + ERC6909MetadataComponent::ERC6909MetadataImpl; // 4. Implement internal implementations to access internal functions impl ERC6909InternalImpl = ERC6909Component::InternalImpl; @@ -164,7 +179,6 @@ pub mod MyERC6909TokenMetadata { self.erc6909_content_uri.initializer(uri); } - // 6. Use the `_update_token_metadata` internal function to update token metadata during mints impl ERC6909MetadataHooksImpl< TContractState, impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, @@ -179,6 +193,7 @@ pub mod MyERC6909TokenMetadata { amount: u256 ) {} + // Update after any transfer fn after_update( ref self: ERC6909Component::ComponentState, from: ContractAddress, @@ -192,12 +207,11 @@ pub mod MyERC6909TokenMetadata { let symbol = "MET"; let decimals = 18; - // `_update_token_metadata` is only called if this is a mint erc6909_metadata_component._update_token_metadata(from, id, name, symbol, decimals); } } } ---- +---- The `ERC6909Metadata` component has a function to check and update metadata if it hasn't been set yet. The `_update_token_metadata` updates token metadata only upon mints, not transfers or burns. Thus while minting a new token ID, if it has not metadata associated with it @@ -230,19 +244,32 @@ pub mod MyERC6909TokenTotalSupply { // 2. Declare the component to access its storage and events component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); - component!(path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent); - component!(path: ERC6909TokenSupplyComponent, storage: erc6909_token_supply, event: ERC6909TokenSupplyEvent); + component!( + path: ERC6909ContentURIComponent, + storage: erc6909_content_uri, + event: ERC6909ContentURIEvent + ); + component!( + path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent + ); + component!( + path: ERC6909TokenSupplyComponent, + storage: erc6909_token_supply, + event: ERC6909TokenSupplyEvent + ); // 3. Embed ABI to access external functions #[abi(embed_v0)] impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; #[abi(embed_v0)] - impl ERC6909ContentURIComponentImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; + impl ERC6909ContentURIComponentImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; #[abi(embed_v0)] - impl ERC6909MetadataComponentImpl = ERC6909MetadataComponent::ERC6909MetadataImpl; + impl ERC6909MetadataComponentImpl = + ERC6909MetadataComponent::ERC6909MetadataImpl; #[abi(embed_v0)] - impl ERC6909TokenSupplyComponentImpl = ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; + impl ERC6909TokenSupplyComponentImpl = + ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; // 4. Implement internal implementations to access internal functions impl ERC6909InternalImpl = ERC6909Component::InternalImpl; @@ -284,7 +311,6 @@ pub mod MyERC6909TokenTotalSupply { self.erc6909_content_uri.initializer(uri); } - // 6. Use the `_update_token_supply` to update Token ID supply during mints and burns. impl ERC6909TokenSupplyHooksImpl< TContractState, impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, @@ -308,15 +334,17 @@ pub mod MyERC6909TokenTotalSupply { amount: u256 ) { let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); - erc6909_metadata_component._update_token_metadata(from, id, "MyERC6909Token", "MET", 18); + erc6909_metadata_component + ._update_token_metadata(from, id, "MyERC6909Token", "MET", 18); - // Will only update during mints and burns - let mut erc6909_token_supply_component = get_dep_component_mut!(ref self, ERC6909TokenSupply); + let mut erc6909_token_supply_component = get_dep_component_mut!( + ref self, ERC6909TokenSupply + ); erc6909_token_supply_component._update_token_supply(from, recipient, id, amount); } } } ---- +---- The logic is the exact same as when implementing the Metadata component. The `ERC6909TokenSupplyComponent` has an internal function (`_update_token_supply`) which updates the supply of a token ID only upon mints and/or burns. From bec24f8735831e90e0919f3599ac404d0ecd3968 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Mon, 8 Jul 2024 22:02:51 +0200 Subject: [PATCH 21/44] resolve docs `supports_interface` --- docs/modules/ROOT/pages/api/erc6909.adoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/api/erc6909.adoc b/docs/modules/ROOT/pages/api/erc6909.adoc index b89d0b263..ac5c54111 100644 --- a/docs/modules/ROOT/pages/api/erc6909.adoc +++ b/docs/modules/ROOT/pages/api/erc6909.adoc @@ -2,7 +2,6 @@ :eip6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] :erc6909-guide: xref:erc6909.adoc[ERC6909 guide] :casing-discussion: https://github.com/OpenZeppelin/cairo-contracts/discussions/34[here] -//:custom-decimals: xref:/erc20.adoc#customizing_decimals[Customizing decimals] = ERC6909 @@ -106,7 +105,7 @@ Sets or unsets `spender` as an operator for the caller. Emits an <> event. [.contract-item] -[[IERC6909-set_operator]] +[[IERC6909-supports_interface]] ==== `[.contract-item-name]#++supports_interface++#++(interface_id: felt252) → bool++` [.item-kind]#external# Checks if a contract implements `interface_id`. From 12351a466b6a9d1ee2a0785486294e7059d07286 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Mon, 8 Jul 2024 22:26:28 +0200 Subject: [PATCH 22/44] comments on docs --- docs/modules/ROOT/pages/guides/erc6909-extensions.adoc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc index 56a1de400..2410ea70b 100644 --- a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc +++ b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc @@ -30,9 +30,9 @@ this we can make use of the `ERC6909ContentURI` extension. ---- #[starknet::contract] pub mod MyERC6909ContentURI { - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; // 1. Import the Content URI Component use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; use starknet::ContractAddress; // 2. Declare the component to access its storage and events @@ -179,7 +179,8 @@ pub mod MyERC6909TokenMetadata { self.erc6909_content_uri.initializer(uri); } - impl ERC6909MetadataHooksImpl< + // 6. Implement the hook to set update metadata upon mints + impl ERC6909HooksImpl< TContractState, impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, impl HasComponent: ERC6909Component::HasComponent, @@ -193,7 +194,6 @@ pub mod MyERC6909TokenMetadata { amount: u256 ) {} - // Update after any transfer fn after_update( ref self: ERC6909Component::ComponentState, from: ContractAddress, @@ -311,7 +311,8 @@ pub mod MyERC6909TokenTotalSupply { self.erc6909_content_uri.initializer(uri); } - impl ERC6909TokenSupplyHooksImpl< + // 6. Implement the hook to update total supply upon mints and burns + impl ERC6909HooksImpl< TContractState, impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, impl ERC6909TokenSupply: ERC6909TokenSupplyComponent::HasComponent, From 874c2a62a264b5104f0e68cfccd5916fd303d00d Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Mon, 8 Jul 2024 22:55:37 +0200 Subject: [PATCH 23/44] lint and typos --- docs/modules/ROOT/pages/guides/erc6909-extensions.adoc | 2 +- packages/test_common/src/mocks/erc6909_metadata_mocks.cairo | 3 ++- packages/token/src/erc6909/erc6909.cairo | 4 ++-- packages/token/src/erc6909/extensions/erc6909_metadata.cairo | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc index 2410ea70b..31cb1cb69 100644 --- a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc +++ b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc @@ -97,7 +97,7 @@ We then embed the ABI for both components so each function in the implementation Notice that we are also implementing the `ERC6909InternalImpl` and `ERC6909COntentURIInternalImpl` to access the internal functions of each component (such as `mint` or `initializer`). Finally, in the constructor we mint an initial token supply to `receiver` and set the contract URI via `ERC6909ContentURIComponent` initializer. Notice that the `initializer` -function is called in the constructor in this case, but since it is an internal function it can be called anytime, however it is usually recomended to set it once in the +function is called in the constructor in this case, but since it is an internal function it can be called anytime, however it is usually recommended to set it once in the constructor to not be accessible again. == ERC6909 Metadata diff --git a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo index 51a009d32..3071e5ddf 100644 --- a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo +++ b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo @@ -11,7 +11,8 @@ pub(crate) mod DualCaseERC6909MetadataMock { // ERC6909Metadata #[abi(embed_v0)] - impl ERC6909MetadataComponentImpl = ERC6909MetadataComponent::ERC6909MetadataImpl; + impl ERC6909MetadataComponentImpl = + ERC6909MetadataComponent::ERC6909MetadataImpl; // ERC6909Mixin #[abi(embed_v0)] diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 5e4e8c745..395a5d98c 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -82,9 +82,9 @@ pub mod ERC6909Component { pub const INSUFFICIENT_BALANCE: felt252 = 'ERC6909: insufficient balance'; /// @dev Thrown when spender allowance for id is insufficient. pub const INSUFFICIENT_ALLOWANCE: felt252 = 'ERC6909: insufficient allowance'; - /// @dev Thrown when transfering from the zero address + /// @dev Thrown when transferring from the zero address pub const TRANSFER_FROM_ZERO: felt252 = 'ERC6909: transfer from 0'; - /// @dev Thrown when transfering to the zero address + /// @dev Thrown when transferring to the zero address pub const TRANSFER_TO_ZERO: felt252 = 'ERC6909: transfer to 0'; /// @dev Thrown when minting to the zero address pub const MINT_TO_ZERO: felt252 = 'ERC6909: mint to 0'; diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index 0ec1ed461..5f1e2956f 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -118,7 +118,9 @@ pub mod ERC6909MetadataComponent { /// @notice Sets the token symbol. /// @param id The id of the token. /// @param symbol The symbol of the token. - fn _set_token_symbol(ref self: ComponentState, id: u256, symbol: ByteArray) { + fn _set_token_symbol( + ref self: ComponentState, id: u256, symbol: ByteArray + ) { self.ERC6909Metadata_symbol.write(id, symbol); } From 5e9fb6ab28c72a9febaa50c4d7ec259090907a5a Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 9 Jul 2024 17:26:42 +0200 Subject: [PATCH 24/44] make imports consistent --- packages/token/src/erc6909/erc6909.cairo | 3 ++- .../src/erc6909/extensions/erc6909_token_supply.cairo | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 395a5d98c..2877885ff 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -11,9 +11,10 @@ use core::starknet::{ContractAddress}; pub mod ERC6909Component { use core::integer::BoundedInt; use core::num::traits::Zero; - use core::starknet::{ContractAddress, get_caller_address}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::token::erc6909::interface; + use starknet::ContractAddress; + use starknet::get_caller_address; #[storage] struct Storage { diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index 48ed8986c..b239a0be5 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -10,10 +10,9 @@ use starknet::ContractAddress; #[starknet::component] pub mod ERC6909TokenSupplyComponent { use core::num::traits::Zero; - use core::starknet::{ContractAddress}; use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::interface::IERC6909; - use openzeppelin::token::erc6909::interface::IERC6909TokenSupply; + use openzeppelin::token::erc6909::interface; + use starknet::ContractAddress; #[storage] struct Storage { @@ -27,7 +26,7 @@ pub mod ERC6909TokenSupplyComponent { +ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, +Drop - > of IERC6909TokenSupply> { + > of interface::IERC6909TokenSupply> { /// @notice Total supply of a token. /// @param id The id of the token. /// @return The total supply of the token. From 00668791a3e6528032f7b1108012097494e55e46 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 9 Aug 2024 12:51:08 +0200 Subject: [PATCH 25/44] remove dual dispatchers and update comments --- packages/token/src/erc6909.cairo | 5 +- packages/token/src/erc6909/dual6909.cairo | 137 ------------ packages/token/src/erc6909/erc6909.cairo | 242 ++------------------- packages/token/src/erc6909/interface.cairo | 184 ---------------- 4 files changed, 22 insertions(+), 546 deletions(-) delete mode 100644 packages/token/src/erc6909/dual6909.cairo diff --git a/packages/token/src/erc6909.cairo b/packages/token/src/erc6909.cairo index 00d355fe5..ce080adf5 100644 --- a/packages/token/src/erc6909.cairo +++ b/packages/token/src/erc6909.cairo @@ -1,9 +1,8 @@ -pub mod dual6909; pub mod erc6909; pub mod extensions; pub mod interface; pub use erc6909::ERC6909Component; pub use erc6909::ERC6909HooksEmptyImpl; -pub use interface::ERC6909ABIDispatcher; -pub use interface::ERC6909ABIDispatcherTrait; +pub use interface::IERC6909Dispatcher; +pub use interface::IERC6909DispatcherTrait; diff --git a/packages/token/src/erc6909/dual6909.cairo b/packages/token/src/erc6909/dual6909.cairo deleted file mode 100644 index 470051f52..000000000 --- a/packages/token/src/erc6909/dual6909.cairo +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: MIT -use openzeppelin::utils::UnwrapAndCast; -use openzeppelin::utils::selectors; -use openzeppelin::utils::serde::SerializedAppend; -use openzeppelin::utils::try_selector_with_fallback; -use starknet::ContractAddress; -use starknet::SyscallResultTrait; -use starknet::syscalls::call_contract_syscall; - -#[derive(Copy, Drop)] -pub struct DualCaseERC6909 { - pub contract_address: ContractAddress -} - -pub trait DualCaseERC6909Trait { - fn balance_of(self: @DualCaseERC6909, owner: ContractAddress, id: u256) -> u256; - fn allowance( - self: @DualCaseERC6909, owner: ContractAddress, spender: ContractAddress, id: u256 - ) -> u256; - fn is_operator( - self: @DualCaseERC6909, owner: ContractAddress, spender: ContractAddress - ) -> bool; - fn transfer(self: @DualCaseERC6909, receiver: ContractAddress, id: u256, amount: u256) -> bool; - fn transfer_from( - self: @DualCaseERC6909, - sender: ContractAddress, - receiver: ContractAddress, - id: u256, - amount: u256 - ) -> bool; - fn approve(self: @DualCaseERC6909, spender: ContractAddress, id: u256, amount: u256) -> bool; - fn set_operator(self: @DualCaseERC6909, spender: ContractAddress, approved: bool) -> bool; - fn supports_interface(self: @DualCaseERC6909, interface_id: felt252) -> bool; -} - -impl DualCaseERC6909Impl of DualCaseERC6909Trait { - fn balance_of(self: @DualCaseERC6909, owner: ContractAddress, id: u256) -> u256 { - let mut args = array![]; - args.append_serde(owner); - args.append_serde(id); - - try_selector_with_fallback( - *self.contract_address, selectors::balance_of, selectors::balanceOf, args.span() - ) - .unwrap_and_cast() - } - - fn allowance( - self: @DualCaseERC6909, owner: ContractAddress, spender: ContractAddress, id: u256 - ) -> u256 { - let mut args = array![]; - args.append_serde(owner); - args.append_serde(spender); - args.append_serde(id); - - call_contract_syscall(*self.contract_address, selectors::allowance, args.span()) - .unwrap_and_cast() - } - - fn is_operator( - self: @DualCaseERC6909, owner: ContractAddress, spender: ContractAddress - ) -> bool { - let mut args = array![]; - args.append_serde(owner); - args.append_serde(spender); - - let is_operator: felt252 = selectors::is_operator; - let isOperator: felt252 = selectors::isOperator; - - try_selector_with_fallback(*self.contract_address, is_operator, isOperator, args.span()) - .unwrap_and_cast() - } - - fn transfer(self: @DualCaseERC6909, receiver: ContractAddress, id: u256, amount: u256) -> bool { - let mut args = array![]; - args.append_serde(receiver); - args.append_serde(id); - args.append_serde(amount); - - call_contract_syscall(*self.contract_address, selectors::transfer, args.span()) - .unwrap_and_cast() - } - - fn transfer_from( - self: @DualCaseERC6909, - sender: ContractAddress, - receiver: ContractAddress, - id: u256, - amount: u256 - ) -> bool { - let mut args = array![]; - args.append_serde(sender); - args.append_serde(receiver); - args.append_serde(id); - args.append_serde(amount); - - try_selector_with_fallback( - *self.contract_address, selectors::transfer_from, selectors::transferFrom, args.span() - ) - .unwrap_and_cast() - } - - fn approve(self: @DualCaseERC6909, spender: ContractAddress, id: u256, amount: u256) -> bool { - let mut args = array![]; - args.append_serde(spender); - args.append_serde(id); - args.append_serde(amount); - - call_contract_syscall(*self.contract_address, selectors::approve, args.span()) - .unwrap_and_cast() - } - - fn set_operator(self: @DualCaseERC6909, spender: ContractAddress, approved: bool) -> bool { - let mut args = array![]; - args.append_serde(spender); - args.append_serde(approved); - - let set_operator: felt252 = selectors::set_operator; - let setOperator: felt252 = selectors::setOperator; - - try_selector_with_fallback(*self.contract_address, set_operator, setOperator, args.span()) - .unwrap_and_cast() - } - - fn supports_interface(self: @DualCaseERC6909, interface_id: felt252) -> bool { - let mut args = array![]; - args.append_serde(interface_id); - - let supports_interface: felt252 = selectors::supports_interface; - let supportsInterface: felt252 = selectors::supportsInterface; - - try_selector_with_fallback( - *self.contract_address, supports_interface, supportsInterface, args.span() - ) - .unwrap_and_cast() - } -} diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 2877885ff..11ec48864 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -31,12 +31,7 @@ pub mod ERC6909Component { OperatorSet: OperatorSet } - /// @notice The event emitted when a transfer occurs. - /// @param caller The caller of the transfer. - /// @param sender The address of the sender. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. + /// Emitted when `id` tokens are moved from address `from` to address `to`. #[derive(Drop, PartialEq, starknet::Event)] pub struct Transfer { pub caller: ContractAddress, @@ -49,11 +44,8 @@ pub mod ERC6909Component { pub amount: u256, } - /// @notice The event emitted when an approval occurs. - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @param id The id of the token. - /// @param amount The amount of the token. + /// Emitted when the allowance of a `spender` for an `owner` is set by a call + /// to `approve` over `id` #[derive(Drop, PartialEq, starknet::Event)] pub struct Approval { #[key] @@ -65,10 +57,8 @@ pub mod ERC6909Component { pub amount: u256 } - /// @notice The event emitted when an operator is set. - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @param approved The approval status. + /// Emitted when `account` enables or disables (`approved`) `spender` to manage + /// all of its assets. #[derive(Drop, PartialEq, starknet::Event)] pub struct OperatorSet { #[key] @@ -79,25 +69,20 @@ pub mod ERC6909Component { } pub mod Errors { - /// @dev Thrown when owner balance for id is insufficient. pub const INSUFFICIENT_BALANCE: felt252 = 'ERC6909: insufficient balance'; - /// @dev Thrown when spender allowance for id is insufficient. pub const INSUFFICIENT_ALLOWANCE: felt252 = 'ERC6909: insufficient allowance'; - /// @dev Thrown when transferring from the zero address pub const TRANSFER_FROM_ZERO: felt252 = 'ERC6909: transfer from 0'; - /// @dev Thrown when transferring to the zero address pub const TRANSFER_TO_ZERO: felt252 = 'ERC6909: transfer to 0'; - /// @dev Thrown when minting to the zero address pub const MINT_TO_ZERO: felt252 = 'ERC6909: mint to 0'; - /// @dev Thrown when burning from the zero address pub const BURN_FROM_ZERO: felt252 = 'ERC6909: burn from 0'; - /// @dev Thrown when approving from the zero address pub const APPROVE_FROM_ZERO: felt252 = 'ERC6909: approve from 0'; - /// @dev Thrown when approving to the zero address pub const APPROVE_TO_ZERO: felt252 = 'ERC6909: approve to 0'; } - /// Hooks + // + // Hooks + // + pub trait ERC6909HooksTrait { fn before_update( ref self: ComponentState, @@ -120,21 +105,16 @@ pub mod ERC6909Component { impl ERC6909< TContractState, +HasComponent, +ERC6909HooksTrait > of interface::IERC6909> { - /// @notice Owner balance of an id. - /// @param owner The address of the owner. - /// @param id The id of the token. - /// @return The balance of the token. + /// Returns the amount of `id` tokens owned by `account`. fn balance_of( self: @ComponentState, owner: ContractAddress, id: u256 ) -> u256 { self.ERC6909_balances.read((owner, id)) } - /// @notice Spender allowance of an id. - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @param id The id of the token. - /// @return The allowance of the token. + /// Returns the remaining number of `id` tokens that `spender` is + /// allowed to spend on behalf of `owner` through `transfer_from`. + /// This is zero by default. fn allowance( self: @ComponentState, owner: ContractAddress, @@ -144,20 +124,14 @@ pub mod ERC6909Component { self.ERC6909_allowances.read((owner, spender, id)) } - /// @notice Checks if a spender is approved by an owner as an operator - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @return The approval status. + /// Returns if a spender is approved by an owner as an operator fn is_operator( self: @ComponentState, owner: ContractAddress, spender: ContractAddress ) -> bool { self.ERC6909_operators.read((owner, spender)) } - /// @notice Transfers an amount of an id from the caller to a receiver. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. + /// Transfers an amount of an id to a receiver. fn transfer( ref self: ComponentState, receiver: ContractAddress, @@ -169,11 +143,7 @@ pub mod ERC6909Component { true } - /// @notice Transfers an amount of an id from a sender to a receiver. - /// @param sender The address of the sender. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. + /// Transfers an amount of an id from a sender to a receiver. fn transfer_from( ref self: ComponentState, sender: ContractAddress, @@ -187,10 +157,7 @@ pub mod ERC6909Component { true } - /// @notice Approves an amount of an id to a spender. - /// @param spender The address of the spender. - /// @param id The id of the token. - /// @param amount The amount of the token. + /// Approves an amount of an id to a spender. fn approve( ref self: ComponentState, spender: ContractAddress, @@ -202,9 +169,7 @@ pub mod ERC6909Component { true } - /// @notice Sets or unsets a spender as an operator for the caller. - /// @param spender The address of the spender. - /// @param approved The approval status. + /// Sets or unsets a spender as an operator for the caller. fn set_operator( ref self: ComponentState, spender: ContractAddress, approved: bool ) -> bool { @@ -213,9 +178,7 @@ pub mod ERC6909Component { true } - /// @notice Checks if a contract implements an interface. - /// @param interfaceId The interface identifier, as specified in ERC-165. - /// @return True if the contract implements `interfaceId` and `interfaceId` is not 0xffffffff, false otherwise. + /// Checks if a contract implements an interface. fn supports_interface( self: @ComponentState, interface_id: felt252 ) -> bool { @@ -223,63 +186,7 @@ pub mod ERC6909Component { } } - #[embeddable_as(ERC6909CamelOnlyImpl)] - impl ERC6909CamelOnly< - TContractState, +HasComponent, +ERC6909HooksTrait - > of interface::IERC6909CamelOnly> { - /// @notice Owner balance of an id. - /// @param owner The address of the owner. - /// @param id The id of the token. - /// @return The balance of the token. - fn balanceOf( - self: @ComponentState, owner: ContractAddress, id: u256 - ) -> u256 { - ERC6909::balance_of(self, owner, id) - } - - /// @notice Checks if a spender is approved by an owner as an operator - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @return The approval status. - fn isOperator( - self: @ComponentState, owner: ContractAddress, spender: ContractAddress - ) -> bool { - ERC6909::is_operator(self, owner, spender) - } - - /// @notice Transfers an amount of an id from a sender to a receiver. - /// @param sender The address of the sender. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. - fn transferFrom( - ref self: ComponentState, - sender: ContractAddress, - receiver: ContractAddress, - id: u256, - amount: u256 - ) -> bool { - ERC6909::transfer_from(ref self, sender, receiver, id, amount) - } - - /// @notice Sets or unsets a spender as an operator for the caller. - /// @param spender The address of the spender. - /// @param approved The approval status. - fn setOperator( - ref self: ComponentState, spender: ContractAddress, approved: bool - ) -> bool { - ERC6909::set_operator(ref self, spender, approved) - } - - /// @notice Checks if a contract implements an interface. - /// @param interfaceId The interface identifier, as specified in ERC-165. - /// @return True if the contract implements `interfaceId` and `interfaceId` is not 0xffffffff, false otherwise. - fn supportsInterface(self: @ComponentState, interface_id: felt252) -> bool { - ERC6909::supports_interface(self, interface_id) - } - } - /// internal #[generate_trait] pub impl InternalImpl< TContractState, +HasComponent, impl Hooks: ERC6909HooksTrait @@ -355,10 +262,7 @@ pub mod ERC6909Component { Hooks::after_update(ref self, sender, receiver, id, amount); } - /// @notice Sets or unsets a spender as an operator for the caller. - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @param approved The approval status. + /// Sets or unsets a spender as an operator for the caller. fn _set_operator( ref self: ComponentState, owner: ContractAddress, @@ -433,112 +337,6 @@ pub mod ERC6909Component { self.update(caller, sender, receiver, id, amount); } } - - #[embeddable_as(ERC6909MixinImpl)] - impl ERC6909Mixin< - TContractState, +HasComponent, +ERC6909HooksTrait - > of interface::ERC6909ABI> { - // - // ABI - // - - fn balance_of( - self: @ComponentState, owner: ContractAddress, id: u256 - ) -> u256 { - ERC6909::balance_of(self, owner, id) - } - - fn allowance( - self: @ComponentState, - owner: ContractAddress, - spender: ContractAddress, - id: u256 - ) -> u256 { - ERC6909::allowance(self, owner, spender, id) - } - - fn is_operator( - self: @ComponentState, owner: ContractAddress, spender: ContractAddress - ) -> bool { - ERC6909::is_operator(self, owner, spender) - } - - fn transfer( - ref self: ComponentState, - receiver: ContractAddress, - id: u256, - amount: u256 - ) -> bool { - ERC6909::transfer(ref self, receiver, id, amount) - } - - fn transfer_from( - ref self: ComponentState, - sender: ContractAddress, - receiver: ContractAddress, - id: u256, - amount: u256 - ) -> bool { - ERC6909::transfer_from(ref self, sender, receiver, id, amount) - } - - fn approve( - ref self: ComponentState, - spender: ContractAddress, - id: u256, - amount: u256 - ) -> bool { - ERC6909::approve(ref self, spender, id, amount) - } - - fn set_operator( - ref self: ComponentState, spender: ContractAddress, approved: bool - ) -> bool { - ERC6909::set_operator(ref self, spender, approved) - } - - fn supports_interface( - self: @ComponentState, interface_id: felt252 - ) -> bool { - ERC6909::supports_interface(self, interface_id) - } - - // - // CamelCase - // - - fn balanceOf( - self: @ComponentState, owner: ContractAddress, id: u256 - ) -> u256 { - ERC6909::balance_of(self, owner, id) - } - - fn isOperator( - self: @ComponentState, owner: ContractAddress, spender: ContractAddress - ) -> bool { - ERC6909::is_operator(self, owner, spender) - } - - fn transferFrom( - ref self: ComponentState, - sender: ContractAddress, - receiver: ContractAddress, - id: u256, - amount: u256 - ) -> bool { - ERC6909::transfer_from(ref self, sender, receiver, id, amount) - } - - fn setOperator( - ref self: ComponentState, spender: ContractAddress, approved: bool - ) -> bool { - ERC6909::set_operator(ref self, spender, approved) - } - - fn supportsInterface(self: @ComponentState, interfaceId: felt252) -> bool { - ERC6909::supports_interface(self, interfaceId) - } - } } /// An empty implementation of the ERC6909 hooks to be used in basic ERC6909 preset contracts. diff --git a/packages/token/src/erc6909/interface.cairo b/packages/token/src/erc6909/interface.cairo index 0b0c44485..65bc5cac6 100644 --- a/packages/token/src/erc6909/interface.cairo +++ b/packages/token/src/erc6909/interface.cairo @@ -2,161 +2,10 @@ use starknet::ContractAddress; // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol -// To generate Starknet IDs: https://community.starknet.io/t/starknet-standard-interface-detection pub const IERC6909_ID: felt252 = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee; #[starknet::interface] pub trait IERC6909 { - /// @notice Owner balance of an id. - /// @param owner The address of the owner. - /// @param id The id of the token. - /// @return The balance of the token. - fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; - - /// @notice Spender allowance of an id. - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @param id The id of the token. - /// @return The allowance of the token. - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; - - /// @notice Checks if a spender is approved by an owner as an operator - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @return The approval status. - fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - - /// @notice Transfers an amount of an id from the caller to a receiver. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. - fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; - - /// @notice Transfers an amount of an id from a sender to a receiver. - /// @param sender The address of the sender. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. - fn transfer_from( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool; - - /// @notice Approves an amount of an id to a spender. - /// @param spender The address of the spender. - /// @param id The id of the token. - /// @param amount The amount of the token. - fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; - - /// @notice Sets or removes a spender as an operator for the caller. - /// @param spender The address of the spender. - /// @param approved The approval status. - fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - - // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC165.sol - /// @notice Checks if a contract implements an interface. - /// @param interfaceId The interface identifier, as specified in ERC-165. - /// @return True if the contract implements `interfaceId` and - /// `interfaceId` is not 0xffffffff, false otherwise. - fn supports_interface(self: @TState, interface_id: felt252) -> bool; -} - -#[starknet::interface] -pub trait IERC6909Camel { - /// @notice Owner balance of an id. - /// @param owner The address of the owner. - /// @param id The id of the token. - /// @return The balance of the token. - fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; - - /// @notice Spender allowance of an id. - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @param id The id of the token. - /// @return The allowance of the token. - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; - - /// @notice Checks if a spender is approved by an owner as an operator - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @return The approval status. - fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - - /// @notice Transfers an amount of an id from the caller to a receiver. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. - fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; - - /// @notice Transfers an amount of an id from a sender to a receiver. - /// @param sender The address of the sender. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. - fn transferFrom( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool; - - /// @notice Approves an amount of an id to a spender. - /// @param spender The address of the spender. - /// @param id The id of the token. - /// @param amount The amount of the token. - fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; - - /// @notice Sets or removes a spender as an operator for the caller. - /// @param spender The address of the spender. - /// @param approved The approval status. - fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - - // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC165.sol - /// @notice Checks if a contract implements an interface. - /// @param interfaceId The interface identifier, as specified in ERC-165. - /// @return True if the contract implements `interfaceId` and - /// `interfaceId` is not 0xffffffff, false otherwise. - fn supportsInterface(self: @TState, interface_id: felt252) -> bool; -} - - -#[starknet::interface] -pub trait IERC6909CamelOnly { - /// @notice Owner balance of an id. - /// @param owner The address of the owner. - /// @param id The id of the token. - /// @return The balance of the token. - fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; - - /// @notice Checks if a spender is approved by an owner as an operator - /// @param owner The address of the owner. - /// @param spender The address of the spender. - /// @return The approval status. - fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - - /// @notice Transfers an amount of an id from a sender to a receiver. - /// @param sender The address of the sender. - /// @param receiver The address of the receiver. - /// @param id The id of the token. - /// @param amount The amount of the token. - fn transferFrom( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool; - - /// @notice Sets or removes a spender as an operator for the caller. - /// @param spender The address of the spender. - /// @param approved The approval status. - fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - - // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC165.sol - /// @notice Checks if a contract implements an interface. - /// @param interfaceId The interface identifier, as specified in ERC-165. - /// @return True if the contract implements `interfaceId` and - /// `interfaceId` is not 0xffffffff, false otherwise. - fn supportsInterface(self: @TState, interface_id: felt252) -> bool; -} - - -// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol -#[starknet::interface] -pub trait ERC6909ABI { - /// @notice IERC6909 standard interface fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; @@ -167,58 +16,25 @@ pub trait ERC6909ABI { fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; fn supports_interface(self: @TState, interface_id: felt252) -> bool; - - /// @notice IERC6909Camel - fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; - fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - fn transferFrom( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool; - fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - fn supportsInterface(self: @TState, interfaceId: felt252) -> bool; } -// -// Extensions -// - // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909Metadata.sol #[starknet::interface] pub trait IERC6909Metadata { - /// @notice Name of a given token. - /// @param id The id of the token. - /// @return The name of the token. fn name(self: @TState, id: u256) -> ByteArray; - - /// @notice Symbol of a given token. - /// @param id The id of the token. - /// @return The symbol of the token. fn symbol(self: @TState, id: u256) -> ByteArray; - - /// @notice Decimals of a given token. - /// @param id The id of the token. - /// @return The decimals of the token. fn decimals(self: @TState, id: u256) -> u8; } // https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909TokenSupply.sol #[starknet::interface] pub trait IERC6909TokenSupply { - /// @notice Total supply of a token - /// @param id The id of the token. - /// @return The total supply of the token. fn total_supply(self: @TState, id: u256) -> u256; } //https://github.com/jtriley-eth/ERC-6909/blob/main/src/ERC6909ContentURI.sol #[starknet::interface] pub trait IERC6909ContentURI { - /// @notice Contract level URI - /// @return The contract level URI. fn contract_uri(self: @TState) -> ByteArray; - - /// @notice Token level URI - /// @param id The id of the token. - /// @return The token level URI. fn token_uri(self: @TState, id: u256) -> ByteArray; } From ce62b526fecebadac575cecea06e7eaf6e42f881 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Fri, 9 Aug 2024 12:59:23 +0200 Subject: [PATCH 26/44] update comments --- .../extensions/erc6909_content_uri.cairo | 10 ++--- .../erc6909/extensions/erc6909_metadata.cairo | 41 ++++--------------- .../extensions/erc6909_token_supply.cairo | 13 ++---- 3 files changed, 16 insertions(+), 48 deletions(-) diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index f8261b14c..52692acad 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -25,15 +25,12 @@ pub mod ERC6909ContentURIComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of interface::IERC6909ContentURI> { - /// @notice The contract level URI. - /// @return The URI of the contract. + /// Returns the contract level URI. fn contract_uri(self: @ComponentState) -> ByteArray { self.ERC6909ContentURI_contract_uri.read() } - /// @notice Token level URI - /// @param id The id of the token. - /// @return The token level URI. + /// Returns the token level URI. fn token_uri(self: @ComponentState, id: u256) -> ByteArray { let contract_uri = self.contract_uri(); if contract_uri.len() == 0 { @@ -52,8 +49,7 @@ pub mod ERC6909ContentURIComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of InternalTrait { - /// @notice Sets the base URI. - /// @param contract_uri The base contract URI + /// Sets the base URI. fn initializer(ref self: ComponentState, contract_uri: ByteArray) { self.ERC6909ContentURI_contract_uri.write(contract_uri); } diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index 5f1e2956f..efa9b9f7f 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -29,23 +29,17 @@ pub mod ERC6909MetadataComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of interface::IERC6909Metadata> { - /// @notice Name of a given token. - /// @param id The id of the token. - /// @return The name of the token. + /// Returns the name of a token ID fn name(self: @ComponentState, id: u256) -> ByteArray { self.ERC6909Metadata_name.read(id) } - /// @notice Symbol of a given token. - /// @param id The id of the token. - /// @return The symbol of the token. + /// Returns the symbol of a token ID fn symbol(self: @ComponentState, id: u256) -> ByteArray { self.ERC6909Metadata_symbol.read(id) } - /// @notice Decimals of a given token. - /// @param id The id of the token. - /// @return The decimals of the token. + /// Returns the decimals of a token ID fn decimals(self: @ComponentState, id: u256) -> u8 { self.ERC6909Metadata_decimals.read(id) } @@ -59,13 +53,7 @@ pub mod ERC6909MetadataComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of InternalTrait { - /// @notice Updates the metadata of a token ID. - /// @notice Ideally this function should be called in a `before_update` or `after_update` hook during mints. - /// @param sender The address of the sender. - /// @param id The ID of the token. - /// @param name The name of the token. - /// @param symbol The symbol of the token. - /// @param decimals The decimals of the token. + /// Updates the metadata of a token ID. fn _update_token_metadata( ref self: ComponentState, sender: ContractAddress, @@ -85,17 +73,12 @@ pub mod ERC6909MetadataComponent { } } - /// @notice Checks if a token has metadata at the time of minting. - /// @param id The ID of the token. - /// @return Whether or not the token has metadata. + /// Checks if a token has metadata at the time of minting. fn _token_metadata_exists(self: @ComponentState, id: u256) -> bool { return self.ERC6909Metadata_name.read(id).len() > 0; } - /// @notice Updates the token metadata for `id`. - /// @param id The ID of the token. - /// @param name The name of the token. - /// @param decimals The decimals of the token. + /// Updates the token metadata for `id`. fn _set_token_metadata( ref self: ComponentState, id: u256, @@ -108,25 +91,19 @@ pub mod ERC6909MetadataComponent { self._set_token_decimals(id, decimals); } - /// @notice Sets the token name. - /// @param id The id of the token. - /// @param name The name of the token. + /// Sets the token name. fn _set_token_name(ref self: ComponentState, id: u256, name: ByteArray) { self.ERC6909Metadata_name.write(id, name); } - /// @notice Sets the token symbol. - /// @param id The id of the token. - /// @param symbol The symbol of the token. + /// Sets the token symbol. fn _set_token_symbol( ref self: ComponentState, id: u256, symbol: ByteArray ) { self.ERC6909Metadata_symbol.write(id, symbol); } - /// @notice Sets the token decimals. - /// @param id The id of the token. - /// @param decimals The decimals of the token. + /// Sets the token decimals. fn _set_token_decimals(ref self: ComponentState, id: u256, decimals: u8) { self.ERC6909Metadata_decimals.write(id, decimals); } diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index b239a0be5..39c86c27a 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -27,9 +27,7 @@ pub mod ERC6909TokenSupplyComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of interface::IERC6909TokenSupply> { - /// @notice Total supply of a token. - /// @param id The id of the token. - /// @return The total supply of the token. + /// Returns the total supply of a token. fn total_supply(self: @ComponentState, id: u256) -> u256 { self.ERC6909TokenSupply_total_supply.read(id) } @@ -47,12 +45,9 @@ pub mod ERC6909TokenSupplyComponent { +ERC6909Component::ERC6909HooksTrait, +Drop > of InternalTrait { - /// @notice Updates the total supply of a token ID. - /// @notice Ideally this function should be called in a `before_update` or `after_update` hook during mints and burns. - /// @param sender The address of the sender. - /// @param receiver The address of the receiver. - /// @param id The ID of the token. - /// @param amount The amount being minted or burnt. + /// Updates the total supply of a token ID. + /// Ideally this function should be called in a `before_update` or `after_update` + /// hook during mints and burns. fn _update_token_supply( ref self: ComponentState, sender: ContractAddress, From c99c8990c77041632a32e36ff4ab218d6af8b23f Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 13 Aug 2024 13:41:36 +0200 Subject: [PATCH 27/44] add new `Map` and paths --- packages/token/src/erc6909/erc6909.cairo | 39 +++++++++++-------- .../extensions/erc6909_content_uri.cairo | 4 +- .../erc6909/extensions/erc6909_metadata.cairo | 11 +++--- .../extensions/erc6909_token_supply.cairo | 9 +++-- .../src/tests/erc6909/test_erc6909.cairo | 8 ++-- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 11ec48864..801158598 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -3,24 +3,27 @@ use core::starknet::{ContractAddress}; +/// TODO: ADD SRC5 As a component + /// # ERC6909 Component /// -/// The ERC6909 component provides an implementation of the Minimal Multi-Token standard authored by jtriley.eth -/// See https://eips.ethereum.org/EIPS/eip-6909. +/// The ERC6909 component provides an implementation of the Minimal Multi-Token standard authored by +/// jtriley.eth See https://eips.ethereum.org/EIPS/eip-6909. #[starknet::component] pub mod ERC6909Component { - use core::integer::BoundedInt; + use core::num::traits::Bounded; use core::num::traits::Zero; - use openzeppelin::introspection::interface::ISRC5_ID; - use openzeppelin::token::erc6909::interface; + use openzeppelin_account::interface::ISRC6_ID; + use openzeppelin_token::erc6909::interface; use starknet::ContractAddress; use starknet::get_caller_address; + use starknet::storage::Map; #[storage] struct Storage { - ERC6909_balances: LegacyMap<(ContractAddress, u256), u256>, - ERC6909_allowances: LegacyMap<(ContractAddress, ContractAddress, u256), u256>, - ERC6909_operators: LegacyMap<(ContractAddress, ContractAddress), bool>, + ERC6909_balances: Map<(ContractAddress, u256), u256>, + ERC6909_allowances: Map<(ContractAddress, ContractAddress, u256), u256>, + ERC6909_operators: Map<(ContractAddress, ContractAddress), bool>, } #[event] @@ -182,7 +185,7 @@ pub mod ERC6909Component { fn supports_interface( self: @ComponentState, interface_id: felt252 ) -> bool { - interface_id == interface::IERC6909_ID || interface_id == ISRC5_ID + interface_id == interface::IERC6909_ID || interface_id == ISRC6_ID } } @@ -226,12 +229,13 @@ pub mod ERC6909Component { self.update(get_caller_address(), account, Zero::zero(), id, amount); } - /// Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or burns) + /// Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or + /// burns) /// if `sender` (or `receiver`) is the zero address. /// - /// This function can be extended using the `before_update` and `after_update` hooks. - /// The implementation does not keep track of individual token supplies and this logic is left - /// to the extensions instead. + /// This function can be extended using the `before_update` and `after_update` hooks. + /// The implementation does not keep track of individual token supplies and this logic is + /// left to the extensions instead. /// /// Emits a `Transfer` event. fn update( @@ -274,7 +278,8 @@ pub mod ERC6909Component { } /// Updates `sender`s allowance for `spender` and `id` based on spent `amount`. - /// Does not update the allowance value in case of infinite allowance or if spender is operator. + /// Does not update the allowance value in case of infinite allowance or if spender is + /// operator. fn _spend_allowance( ref self: ComponentState, sender: ContractAddress, @@ -282,12 +287,12 @@ pub mod ERC6909Component { id: u256, amount: u256 ) { - // In accordance with the transferFrom method, spenders with operator permission are not subject to - // allowance restrictions (https://eips.ethereum.org/EIPS/eip-6909). + // In accordance with the transferFrom method, spenders with operator permission are not + // subject to allowance restrictions (https://eips.ethereum.org/EIPS/eip-6909). if sender != spender && !self.ERC6909_operators.read((sender, spender)) { let sender_allowance = self.ERC6909_allowances.read((sender, spender, id)); assert(sender_allowance >= amount, Errors::INSUFFICIENT_ALLOWANCE); - if sender_allowance != BoundedInt::max() { + if sender_allowance != Bounded::MAX { self._approve(sender, spender, id, sender_allowance - amount) } } diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index 52692acad..488a26326 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -9,8 +9,8 @@ use starknet::ContractAddress; /// The internal function `initializer` should be used ideally in the constructor. #[starknet::component] pub mod ERC6909ContentURIComponent { - use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::interface; + use openzeppelin_token::erc6909::ERC6909Component; + use openzeppelin_token::erc6909::interface; #[storage] struct Storage { diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index efa9b9f7f..b18454248 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -10,15 +10,16 @@ use starknet::ContractAddress; #[starknet::component] pub mod ERC6909MetadataComponent { use core::num::traits::Zero; - use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::interface; + use openzeppelin_token::erc6909::ERC6909Component; + use openzeppelin_token::erc6909::interface; use starknet::ContractAddress; + use starknet::storage::Map; #[storage] struct Storage { - ERC6909Metadata_name: LegacyMap, - ERC6909Metadata_symbol: LegacyMap, - ERC6909Metadata_decimals: LegacyMap, + ERC6909Metadata_name: Map, + ERC6909Metadata_symbol: Map, + ERC6909Metadata_decimals: Map, } #[embeddable_as(ERC6909MetadataImpl)] diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index 39c86c27a..2d82356ff 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -10,13 +10,14 @@ use starknet::ContractAddress; #[starknet::component] pub mod ERC6909TokenSupplyComponent { use core::num::traits::Zero; - use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::interface; + use openzeppelin_token::erc6909::ERC6909Component; + use openzeppelin_token::erc6909::interface; use starknet::ContractAddress; + use starknet::storage::Map; #[storage] struct Storage { - ERC6909TokenSupply_total_supply: LegacyMap, + ERC6909TokenSupply_total_supply: Map, } #[embeddable_as(ERC6909TokenSupplyImpl)] @@ -46,7 +47,7 @@ pub mod ERC6909TokenSupplyComponent { +Drop > of InternalTrait { /// Updates the total supply of a token ID. - /// Ideally this function should be called in a `before_update` or `after_update` + /// Ideally this function should be called in a `before_update` or `after_update` /// hook during mints and burns. fn _update_token_supply( ref self: ComponentState, diff --git a/packages/token/src/tests/erc6909/test_erc6909.cairo b/packages/token/src/tests/erc6909/test_erc6909.cairo index 86084145d..98979e574 100644 --- a/packages/token/src/tests/erc6909/test_erc6909.cairo +++ b/packages/token/src/tests/erc6909/test_erc6909.cairo @@ -65,7 +65,8 @@ fn test_allowance() { #[test] fn test_set_supports_interface() { let mut state = setup(); - // IERC6909_ID as defined in `interface.cairo` = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee + // IERC6909_ID as defined in `interface.cairo` = + // 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee assert!( state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee) ); @@ -82,7 +83,8 @@ fn test_set_supports_interface() { #[test] fn test_set_supportsInterface() { let mut state = setup(); - // IERC6909_ID as defined in `interface.cairo` = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee + // IERC6909_ID as defined in `interface.cairo` = + // 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee assert!( state.supportsInterface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee) ); @@ -293,7 +295,7 @@ fn test_transfer_from_to_zero_address() { state.transfer_from(OWNER(), ZERO(), TOKEN_ID, VALUE); } -// This does not check `_spend_allowance` since the owner (the zero address) +// This does not check `_spend_allowance` since the owner (the zero address) // is the sender, see `_spend_allowance` in erc6909.cairo #[test] #[should_panic(expected: ('ERC6909: transfer from 0',))] From f36e165638a3080dbbd7641bb94f8f76bd546a31 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 13 Aug 2024 14:20:56 +0200 Subject: [PATCH 28/44] use `is_non_zero` and `is_zero` and simplify update --- packages/token/src/erc6909/erc6909.cairo | 38 +++++++++---------- .../extensions/erc6909_content_uri.cairo | 4 +- .../erc6909/extensions/erc6909_metadata.cairo | 4 +- .../extensions/erc6909_token_supply.cairo | 6 +-- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 801158598..2ba295ced 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -15,8 +15,7 @@ pub mod ERC6909Component { use core::num::traits::Zero; use openzeppelin_account::interface::ISRC6_ID; use openzeppelin_token::erc6909::interface; - use starknet::ContractAddress; - use starknet::get_caller_address; + use starknet::{ContractAddress, get_caller_address}; use starknet::storage::Map; #[storage] @@ -142,7 +141,7 @@ pub mod ERC6909Component { amount: u256 ) -> bool { let caller = get_caller_address(); - self._transfer(caller, caller, receiver, id, amount); + self._transfer(caller, receiver, id, amount); true } @@ -156,7 +155,7 @@ pub mod ERC6909Component { ) -> bool { let caller = get_caller_address(); self._spend_allowance(sender, caller, id, amount); - self._transfer(caller, sender, receiver, id, amount); + self._transfer(sender, receiver, id, amount); true } @@ -208,7 +207,7 @@ pub mod ERC6909Component { amount: u256 ) { assert(!receiver.is_zero(), Errors::MINT_TO_ZERO); - self.update(get_caller_address(), Zero::zero(), receiver, id, amount); + self.update(Zero::zero(), receiver, id, amount); } /// Destroys `amount` of tokens from `account`. @@ -226,7 +225,7 @@ pub mod ERC6909Component { amount: u256 ) { assert(!account.is_zero(), Errors::BURN_FROM_ZERO); - self.update(get_caller_address(), account, Zero::zero(), id, amount); + self.update(account, Zero::zero(), id, amount); } /// Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or @@ -240,7 +239,6 @@ pub mod ERC6909Component { /// Emits a `Transfer` event. fn update( ref self: ComponentState, - caller: ContractAddress, // For the `Transfer` event sender: ContractAddress, // from receiver: ContractAddress, // to id: u256, @@ -248,20 +246,18 @@ pub mod ERC6909Component { ) { Hooks::before_update(ref self, sender, receiver, id, amount); - let zero_address = Zero::zero(); - - if (sender != zero_address) { + if (sender.is_non_zero()) { let sender_balance = self.ERC6909_balances.read((sender, id)); assert(sender_balance >= amount, Errors::INSUFFICIENT_BALANCE); self.ERC6909_balances.write((sender, id), sender_balance - amount); } - if (receiver != zero_address) { + if (receiver.is_non_zero()) { let receiver_balance = self.ERC6909_balances.read((receiver, id)); self.ERC6909_balances.write((receiver, id), receiver_balance + amount); } - self.emit(Transfer { caller, sender, receiver, id, amount }); + self.emit(Transfer { caller: get_caller_address(), sender, receiver, id, amount }); Hooks::after_update(ref self, sender, receiver, id, amount); } @@ -282,18 +278,19 @@ pub mod ERC6909Component { /// operator. fn _spend_allowance( ref self: ComponentState, - sender: ContractAddress, + owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256 ) { // In accordance with the transferFrom method, spenders with operator permission are not // subject to allowance restrictions (https://eips.ethereum.org/EIPS/eip-6909). - if sender != spender && !self.ERC6909_operators.read((sender, spender)) { - let sender_allowance = self.ERC6909_allowances.read((sender, spender, id)); - assert(sender_allowance >= amount, Errors::INSUFFICIENT_ALLOWANCE); + if owner != spender && !self.ERC6909_operators.read((owner, spender)) { + let sender_allowance = self.ERC6909_allowances.read((owner, spender, id)); + if sender_allowance != Bounded::MAX { - self._approve(sender, spender, id, sender_allowance - amount) + assert(sender_allowance >= amount, Errors::INSUFFICIENT_ALLOWANCE); + self._approve(owner, spender, id, sender_allowance - amount) } } } @@ -314,8 +311,8 @@ pub mod ERC6909Component { id: u256, amount: u256 ) { - assert(!owner.is_zero(), Errors::APPROVE_FROM_ZERO); - assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO); + assert(owner.is_non_zero(), Errors::APPROVE_FROM_ZERO); + assert(spender.is_non_zero(), Errors::APPROVE_TO_ZERO); self.ERC6909_allowances.write((owner, spender, id), amount); self.emit(Approval { owner, spender, id, amount }); } @@ -331,7 +328,6 @@ pub mod ERC6909Component { /// Emits a `Transfer` event. fn _transfer( ref self: ComponentState, - caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, @@ -339,7 +335,7 @@ pub mod ERC6909Component { ) { assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); assert(!receiver.is_zero(), Errors::TRANSFER_TO_ZERO); - self.update(caller, sender, receiver, id, amount); + self.update(sender, receiver, id, amount); } } } diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index 488a26326..ae8e6c70c 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -34,9 +34,9 @@ pub mod ERC6909ContentURIComponent { fn token_uri(self: @ComponentState, id: u256) -> ByteArray { let contract_uri = self.contract_uri(); if contract_uri.len() == 0 { - return ""; + "" } else { - return format!("{}{}", contract_uri, id); + format!("{}{}", contract_uri, id) } } } diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index b18454248..d002f66b4 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -63,10 +63,8 @@ pub mod ERC6909MetadataComponent { symbol: ByteArray, decimals: u8 ) { - let zero_address = Zero::zero(); - // In case of new ID mints update the token metadata - if (sender == zero_address) { + if (sender.is_zero()) { let token_metadata_exists = self._token_metadata_exists(id); if (!token_metadata_exists) { self._set_token_metadata(id, name, symbol, decimals) diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index 2d82356ff..73ad3bbe5 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -56,16 +56,14 @@ pub mod ERC6909TokenSupplyComponent { id: u256, amount: u256 ) { - let zero_address = Zero::zero(); - // In case of mints we increase the total supply of this token ID - if (sender == zero_address) { + if (sender.is_zero()) { let total_supply = self.ERC6909TokenSupply_total_supply.read(id); self.ERC6909TokenSupply_total_supply.write(id, total_supply + amount); } // In case of burns we decrease the total supply of this token ID - if (receiver == zero_address) { + if (receiver.is_zero()) { let total_supply = self.ERC6909TokenSupply_total_supply.read(id); self.ERC6909TokenSupply_total_supply.write(id, total_supply - amount); } From 965e8db09c8faab2972dd0da2a69042561b52b5c Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 13 Aug 2024 17:30:38 +0200 Subject: [PATCH 29/44] comments and join imports --- packages/token/src/erc6909/erc6909.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 2ba295ced..b1b0a5a4d 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -15,8 +15,8 @@ pub mod ERC6909Component { use core::num::traits::Zero; use openzeppelin_account::interface::ISRC6_ID; use openzeppelin_token::erc6909::interface; - use starknet::{ContractAddress, get_caller_address}; use starknet::storage::Map; + use starknet::{ContractAddress, get_caller_address}; #[storage] struct Storage { @@ -273,7 +273,7 @@ pub mod ERC6909Component { self.emit(OperatorSet { owner, spender, approved }); } - /// Updates `sender`s allowance for `spender` and `id` based on spent `amount`. + /// Updates `sender`'s allowance for `spender` and `id` based on spent `amount`. /// Does not update the allowance value in case of infinite allowance or if spender is /// operator. fn _spend_allowance( From 057d44f2f647fc7dba7c36def9f7f2c918bf87a9 Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Wed, 14 Aug 2024 15:54:21 +0200 Subject: [PATCH 30/44] update comments --- packages/token/src/erc6909/erc6909.cairo | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index b1b0a5a4d..9171e12b5 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -229,8 +229,7 @@ pub mod ERC6909Component { } /// Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or - /// burns) - /// if `sender` (or `receiver`) is the zero address. + /// burns) if `sender` (or `receiver`) is the zero address. /// /// This function can be extended using the `before_update` and `after_update` hooks. /// The implementation does not keep track of individual token supplies and this logic is @@ -239,8 +238,8 @@ pub mod ERC6909Component { /// Emits a `Transfer` event. fn update( ref self: ComponentState, - sender: ContractAddress, // from - receiver: ContractAddress, // to + sender: ContractAddress, + receiver: ContractAddress, id: u256, amount: u256 ) { From 57554f12485a71342664e79509ed5dc67a02c024 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 29 Oct 2025 16:12:16 +0100 Subject: [PATCH 31/44] refactor: move ERC6909 interface to interfaces & generate SRC5 ids --- packages/interfaces/src/lib.cairo | 2 +- packages/interfaces/src/token/erc6909.cairo | 81 +++++++++++++++++++++ packages/token/src/erc6909.cairo | 6 +- packages/token/src/erc6909/interface.cairo | 40 ---------- 4 files changed, 83 insertions(+), 46 deletions(-) delete mode 100644 packages/token/src/erc6909/interface.cairo diff --git a/packages/interfaces/src/lib.cairo b/packages/interfaces/src/lib.cairo index 30ccb7108..8c381e131 100644 --- a/packages/interfaces/src/lib.cairo +++ b/packages/interfaces/src/lib.cairo @@ -23,7 +23,7 @@ pub use security::{initializable, pausable}; // Token pub mod token; -pub use token::{erc1155, erc20, erc2981, erc4626, erc721}; +pub use token::{erc1155, erc20, erc2981, erc4626, erc6909, erc721}; // Upgrades pub mod upgrades; diff --git a/packages/interfaces/src/token/erc6909.cairo b/packages/interfaces/src/token/erc6909.cairo index e69de29bb..b483105db 100644 --- a/packages/interfaces/src/token/erc6909.cairo +++ b/packages/interfaces/src/token/erc6909.cairo @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +use starknet::ContractAddress; + +pub const IERC6909_ID: felt252 = 0xd5aa138060489fd9c4592f77a16011cc5615ce4d292ee1f7873ae65c43b6bb; +pub const IERC6909_METADATA_ID: felt252 = + 0x19aa0b778d120d5294054319458ee8886514766411c50dceddd9463712d6011; +pub const IERC6909_TOKEN_SUPPLY_ID: felt252 = + 0x3a632c15cb93b574eb9166de70521abbeab5c2eb4fdab9930729bba8658c41; +pub const IERC6909_CONTENT_URI_ID: felt252 = + 0x356efd8b40a01c1525c7d0ecafbe3b82a47df564fdd496727effe6336526f05; + +#[starknet::interface] +pub trait IERC6909 { + fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; + fn transfer_from( + ref self: TState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256, + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; + fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; +} + +#[starknet::interface] +pub trait IERC6909Metadata { + fn name(self: @TState, id: u256) -> ByteArray; + fn symbol(self: @TState, id: u256) -> ByteArray; + fn decimals(self: @TState, id: u256) -> u8; +} + +#[starknet::interface] +pub trait IERC6909TokenSupply { + fn total_supply(self: @TState, id: u256) -> u256; +} + + +#[starknet::interface] +pub trait IERC6909ContentURI { + fn contract_uri(self: @TState) -> ByteArray; + fn token_uri(self: @TState, id: u256) -> ByteArray; +} + +#[starknet::interface] +pub trait ERC6909ABI { + // IERC6909 + fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; + fn transfer_from( + ref self: TState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256, + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; + fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + + // ISRC5 + fn supports_interface(self: @TState, interface_id: felt252) -> bool; + + // IERC6909Metadata + fn name(self: @TState, id: u256) -> ByteArray; + fn symbol(self: @TState, id: u256) -> ByteArray; + fn decimals(self: @TState, id: u256) -> u8; + + // IERC6909TokenSupply + fn total_supply(self: @TState, id: u256) -> u256; + + // IERC6909ContentURI + fn contract_uri(self: @TState) -> ByteArray; + fn token_uri(self: @TState, id: u256) -> ByteArray; +} + diff --git a/packages/token/src/erc6909.cairo b/packages/token/src/erc6909.cairo index ce080adf5..2f5f01a73 100644 --- a/packages/token/src/erc6909.cairo +++ b/packages/token/src/erc6909.cairo @@ -1,8 +1,4 @@ pub mod erc6909; pub mod extensions; -pub mod interface; -pub use erc6909::ERC6909Component; -pub use erc6909::ERC6909HooksEmptyImpl; -pub use interface::IERC6909Dispatcher; -pub use interface::IERC6909DispatcherTrait; +pub use erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; diff --git a/packages/token/src/erc6909/interface.cairo b/packages/token/src/erc6909/interface.cairo deleted file mode 100644 index 65bc5cac6..000000000 --- a/packages/token/src/erc6909/interface.cairo +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -use starknet::ContractAddress; - -// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol -pub const IERC6909_ID: felt252 = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee; - -#[starknet::interface] -pub trait IERC6909 { - fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; - fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; - fn transfer_from( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool; - fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; - fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - fn supports_interface(self: @TState, interface_id: felt252) -> bool; -} - -// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909Metadata.sol -#[starknet::interface] -pub trait IERC6909Metadata { - fn name(self: @TState, id: u256) -> ByteArray; - fn symbol(self: @TState, id: u256) -> ByteArray; - fn decimals(self: @TState, id: u256) -> u8; -} - -// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909TokenSupply.sol -#[starknet::interface] -pub trait IERC6909TokenSupply { - fn total_supply(self: @TState, id: u256) -> u256; -} - -//https://github.com/jtriley-eth/ERC-6909/blob/main/src/ERC6909ContentURI.sol -#[starknet::interface] -pub trait IERC6909ContentURI { - fn contract_uri(self: @TState) -> ByteArray; - fn token_uri(self: @TState, id: u256) -> ByteArray; -} From 288da9a9d7122399654e9a078543312ebb88ab2b Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 29 Oct 2025 16:17:42 +0100 Subject: [PATCH 32/44] refactor: align hook trait and empty hooks impl --- packages/token/src/erc6909/erc6909.cairo | 73 ++++++++++-------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 9171e12b5..10a484dbd 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/erc6909.cairo) -use core::starknet::{ContractAddress}; +use core::starknet::ContractAddress; /// TODO: ADD SRC5 As a component @@ -11,8 +11,7 @@ use core::starknet::{ContractAddress}; /// jtriley.eth See https://eips.ethereum.org/EIPS/eip-6909. #[starknet::component] pub mod ERC6909Component { - use core::num::traits::Bounded; - use core::num::traits::Zero; + use core::num::traits::{Bounded, Zero}; use openzeppelin_account::interface::ISRC6_ID; use openzeppelin_token::erc6909::interface; use starknet::storage::Map; @@ -30,7 +29,7 @@ pub mod ERC6909Component { pub enum Event { Transfer: Transfer, Approval: Approval, - OperatorSet: OperatorSet + OperatorSet: OperatorSet, } /// Emitted when `id` tokens are moved from address `from` to address `to`. @@ -56,7 +55,7 @@ pub mod ERC6909Component { pub spender: ContractAddress, #[key] pub id: u256, - pub amount: u256 + pub amount: u256, } /// Emitted when `account` enables or disables (`approved`) `spender` to manage @@ -91,25 +90,25 @@ pub mod ERC6909Component { from: ContractAddress, recipient: ContractAddress, id: u256, - amount: u256 - ); + amount: u256, + ) {} fn after_update( ref self: ComponentState, from: ContractAddress, recipient: ContractAddress, id: u256, - amount: u256 - ); + amount: u256, + ) {} } #[embeddable_as(ERC6909Impl)] impl ERC6909< - TContractState, +HasComponent, +ERC6909HooksTrait + TContractState, +HasComponent, +ERC6909HooksTrait, > of interface::IERC6909> { /// Returns the amount of `id` tokens owned by `account`. fn balance_of( - self: @ComponentState, owner: ContractAddress, id: u256 + self: @ComponentState, owner: ContractAddress, id: u256, ) -> u256 { self.ERC6909_balances.read((owner, id)) } @@ -121,14 +120,14 @@ pub mod ERC6909Component { self: @ComponentState, owner: ContractAddress, spender: ContractAddress, - id: u256 + id: u256, ) -> u256 { self.ERC6909_allowances.read((owner, spender, id)) } /// Returns if a spender is approved by an owner as an operator fn is_operator( - self: @ComponentState, owner: ContractAddress, spender: ContractAddress + self: @ComponentState, owner: ContractAddress, spender: ContractAddress, ) -> bool { self.ERC6909_operators.read((owner, spender)) } @@ -138,7 +137,7 @@ pub mod ERC6909Component { ref self: ComponentState, receiver: ContractAddress, id: u256, - amount: u256 + amount: u256, ) -> bool { let caller = get_caller_address(); self._transfer(caller, receiver, id, amount); @@ -151,7 +150,7 @@ pub mod ERC6909Component { sender: ContractAddress, receiver: ContractAddress, id: u256, - amount: u256 + amount: u256, ) -> bool { let caller = get_caller_address(); self._spend_allowance(sender, caller, id, amount); @@ -164,7 +163,7 @@ pub mod ERC6909Component { ref self: ComponentState, spender: ContractAddress, id: u256, - amount: u256 + amount: u256, ) -> bool { let caller = get_caller_address(); self._approve(caller, spender, id, amount); @@ -173,7 +172,7 @@ pub mod ERC6909Component { /// Sets or unsets a spender as an operator for the caller. fn set_operator( - ref self: ComponentState, spender: ContractAddress, approved: bool + ref self: ComponentState, spender: ContractAddress, approved: bool, ) -> bool { let caller = get_caller_address(); self._set_operator(caller, spender, approved); @@ -182,7 +181,7 @@ pub mod ERC6909Component { /// Checks if a contract implements an interface. fn supports_interface( - self: @ComponentState, interface_id: felt252 + self: @ComponentState, interface_id: felt252, ) -> bool { interface_id == interface::IERC6909_ID || interface_id == ISRC6_ID } @@ -191,7 +190,9 @@ pub mod ERC6909Component { #[generate_trait] pub impl InternalImpl< - TContractState, +HasComponent, impl Hooks: ERC6909HooksTrait + TContractState, + +HasComponent, + impl Hooks: ERC6909HooksTrait, > of InternalTrait { /// Creates a `value` amount of tokens and assigns them to `account`. /// @@ -204,7 +205,7 @@ pub mod ERC6909Component { ref self: ComponentState, receiver: ContractAddress, id: u256, - amount: u256 + amount: u256, ) { assert(!receiver.is_zero(), Errors::MINT_TO_ZERO); self.update(Zero::zero(), receiver, id, amount); @@ -222,7 +223,7 @@ pub mod ERC6909Component { ref self: ComponentState, account: ContractAddress, id: u256, - amount: u256 + amount: u256, ) { assert(!account.is_zero(), Errors::BURN_FROM_ZERO); self.update(account, Zero::zero(), id, amount); @@ -241,7 +242,7 @@ pub mod ERC6909Component { sender: ContractAddress, receiver: ContractAddress, id: u256, - amount: u256 + amount: u256, ) { Hooks::before_update(ref self, sender, receiver, id, amount); @@ -266,7 +267,7 @@ pub mod ERC6909Component { ref self: ComponentState, owner: ContractAddress, spender: ContractAddress, - approved: bool + approved: bool, ) { self.ERC6909_operators.write((owner, spender), approved); self.emit(OperatorSet { owner, spender, approved }); @@ -280,7 +281,7 @@ pub mod ERC6909Component { owner: ContractAddress, spender: ContractAddress, id: u256, - amount: u256 + amount: u256, ) { // In accordance with the transferFrom method, spenders with operator permission are not // subject to allowance restrictions (https://eips.ethereum.org/EIPS/eip-6909). @@ -308,7 +309,7 @@ pub mod ERC6909Component { owner: ContractAddress, spender: ContractAddress, id: u256, - amount: u256 + amount: u256, ) { assert(owner.is_non_zero(), Errors::APPROVE_FROM_ZERO); assert(spender.is_non_zero(), Errors::APPROVE_TO_ZERO); @@ -330,7 +331,7 @@ pub mod ERC6909Component { sender: ContractAddress, receiver: ContractAddress, id: u256, - amount: u256 + amount: u256, ) { assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); assert(!receiver.is_zero(), Errors::TRANSFER_TO_ZERO); @@ -341,21 +342,5 @@ pub mod ERC6909Component { /// An empty implementation of the ERC6909 hooks to be used in basic ERC6909 preset contracts. pub impl ERC6909HooksEmptyImpl< - TContractState -> of ERC6909Component::ERC6909HooksTrait { - fn before_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) {} - - fn after_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) {} -} + TContractState, +> of ERC6909Component::ERC6909HooksTrait {} From fb2ec5f874111cccc9e02dcfd420e3fc26091140 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 29 Oct 2025 16:35:24 +0100 Subject: [PATCH 33/44] refactor: update imports --- packages/token/src/erc6909/erc6909.cairo | 10 ++-------- .../extensions/erc6909_content_uri.cairo | 9 ++++----- .../erc6909/extensions/erc6909_metadata.cairo | 17 +++++++---------- .../extensions/erc6909_token_supply.cairo | 13 +++++-------- packages/token/src/lib.cairo | 1 + 5 files changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 10a484dbd..19c6bcb8f 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -1,9 +1,4 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/erc6909.cairo) - -use core::starknet::ContractAddress; - -/// TODO: ADD SRC5 As a component /// # ERC6909 Component /// @@ -12,9 +7,8 @@ use core::starknet::ContractAddress; #[starknet::component] pub mod ERC6909Component { use core::num::traits::{Bounded, Zero}; - use openzeppelin_account::interface::ISRC6_ID; - use openzeppelin_token::erc6909::interface; - use starknet::storage::Map; + use openzeppelin_interfaces::erc6909 as interface; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::{ContractAddress, get_caller_address}; #[storage] diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index ae8e6c70c..a8339c401 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -1,16 +1,15 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/extensions/erc6909_votes.cairo) -use starknet::ContractAddress; - /// # ERC6909ContentURI Component /// /// The ERC6909ContentURI component allows to set the contract and token ID URIs. /// The internal function `initializer` should be used ideally in the constructor. #[starknet::component] pub mod ERC6909ContentURIComponent { + use openzeppelin_interfaces::erc6909 as interface; use openzeppelin_token::erc6909::ERC6909Component; - use openzeppelin_token::erc6909::interface; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; #[storage] struct Storage { @@ -23,7 +22,7 @@ pub mod ERC6909ContentURIComponent { +HasComponent, +ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, - +Drop + +Drop, > of interface::IERC6909ContentURI> { /// Returns the contract level URI. fn contract_uri(self: @ComponentState) -> ByteArray { @@ -47,7 +46,7 @@ pub mod ERC6909ContentURIComponent { +HasComponent, impl ERC6909: ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, - +Drop + +Drop, > of InternalTrait { /// Sets the base URI. fn initializer(ref self: ComponentState, contract_uri: ByteArray) { diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index d002f66b4..81046e90d 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -1,7 +1,4 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/extensions/erc6909_votes.cairo) - -use starknet::ContractAddress; /// # ERC6909Metadata Component /// @@ -10,10 +7,10 @@ use starknet::ContractAddress; #[starknet::component] pub mod ERC6909MetadataComponent { use core::num::traits::Zero; + use openzeppelin_interfaces::erc6909 as interface; use openzeppelin_token::erc6909::ERC6909Component; - use openzeppelin_token::erc6909::interface; use starknet::ContractAddress; - use starknet::storage::Map; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; #[storage] struct Storage { @@ -28,7 +25,7 @@ pub mod ERC6909MetadataComponent { +HasComponent, +ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, - +Drop + +Drop, > of interface::IERC6909Metadata> { /// Returns the name of a token ID fn name(self: @ComponentState, id: u256) -> ByteArray { @@ -52,7 +49,7 @@ pub mod ERC6909MetadataComponent { +HasComponent, impl ERC6909: ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, - +Drop + +Drop, > of InternalTrait { /// Updates the metadata of a token ID. fn _update_token_metadata( @@ -61,7 +58,7 @@ pub mod ERC6909MetadataComponent { id: u256, name: ByteArray, symbol: ByteArray, - decimals: u8 + decimals: u8, ) { // In case of new ID mints update the token metadata if (sender.is_zero()) { @@ -83,7 +80,7 @@ pub mod ERC6909MetadataComponent { id: u256, name: ByteArray, symbol: ByteArray, - decimals: u8 + decimals: u8, ) { self._set_token_name(id, name); self._set_token_symbol(id, symbol); @@ -97,7 +94,7 @@ pub mod ERC6909MetadataComponent { /// Sets the token symbol. fn _set_token_symbol( - ref self: ComponentState, id: u256, symbol: ByteArray + ref self: ComponentState, id: u256, symbol: ByteArray, ) { self.ERC6909Metadata_symbol.write(id, symbol); } diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index 73ad3bbe5..9b33427f4 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -1,7 +1,4 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/extensions/erc6909_votes.cairo) - -use starknet::ContractAddress; /// # ERC6909TokenSupply Component /// @@ -10,10 +7,10 @@ use starknet::ContractAddress; #[starknet::component] pub mod ERC6909TokenSupplyComponent { use core::num::traits::Zero; + use openzeppelin_interfaces::erc6909 as interface; use openzeppelin_token::erc6909::ERC6909Component; - use openzeppelin_token::erc6909::interface; use starknet::ContractAddress; - use starknet::storage::Map; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; #[storage] struct Storage { @@ -26,7 +23,7 @@ pub mod ERC6909TokenSupplyComponent { +HasComponent, +ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, - +Drop + +Drop, > of interface::IERC6909TokenSupply> { /// Returns the total supply of a token. fn total_supply(self: @ComponentState, id: u256) -> u256 { @@ -44,7 +41,7 @@ pub mod ERC6909TokenSupplyComponent { +HasComponent, impl ERC6909: ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, - +Drop + +Drop, > of InternalTrait { /// Updates the total supply of a token ID. /// Ideally this function should be called in a `before_update` or `after_update` @@ -54,7 +51,7 @@ pub mod ERC6909TokenSupplyComponent { sender: ContractAddress, receiver: ContractAddress, id: u256, - amount: u256 + amount: u256, ) { // In case of mints we increase the total supply of this token ID if (sender.is_zero()) { diff --git a/packages/token/src/lib.cairo b/packages/token/src/lib.cairo index 43b3de05d..73241e725 100644 --- a/packages/token/src/lib.cairo +++ b/packages/token/src/lib.cairo @@ -1,6 +1,7 @@ pub mod common; pub mod erc1155; pub mod erc20; +pub mod erc6909; pub mod erc721; #[cfg(test)] From 2ad7aacac66e1f6593adc7e7097823d26612c32c Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 29 Oct 2025 16:50:30 +0100 Subject: [PATCH 34/44] refactor: separate extension interfaces + ABI from main ERC-6909 interface impl --- packages/interfaces/src/token/erc6909.cairo | 60 ++++++++++++------- .../extensions/erc6909_content_uri.cairo | 2 +- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/packages/interfaces/src/token/erc6909.cairo b/packages/interfaces/src/token/erc6909.cairo index b483105db..5744978c4 100644 --- a/packages/interfaces/src/token/erc6909.cairo +++ b/packages/interfaces/src/token/erc6909.cairo @@ -27,24 +27,6 @@ pub trait IERC6909 { fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; } -#[starknet::interface] -pub trait IERC6909Metadata { - fn name(self: @TState, id: u256) -> ByteArray; - fn symbol(self: @TState, id: u256) -> ByteArray; - fn decimals(self: @TState, id: u256) -> u8; -} - -#[starknet::interface] -pub trait IERC6909TokenSupply { - fn total_supply(self: @TState, id: u256) -> u256; -} - - -#[starknet::interface] -pub trait IERC6909ContentURI { - fn contract_uri(self: @TState) -> ByteArray; - fn token_uri(self: @TState, id: u256) -> ByteArray; -} #[starknet::interface] pub trait ERC6909ABI { @@ -65,16 +47,52 @@ pub trait ERC6909ABI { // ISRC5 fn supports_interface(self: @TState, interface_id: felt252) -> bool; +} + +// +// ERC6909Metadata +// - // IERC6909Metadata +#[starknet::interface] +pub trait IERC6909Metadata { fn name(self: @TState, id: u256) -> ByteArray; fn symbol(self: @TState, id: u256) -> ByteArray; fn decimals(self: @TState, id: u256) -> u8; +} - // IERC6909TokenSupply +#[starknet::interface] +pub trait ERC6909MetadataABI { + fn name(self: @TState, id: u256) -> ByteArray; + fn symbol(self: @TState, id: u256) -> ByteArray; + fn decimals(self: @TState, id: u256) -> u8; +} + +// +// ERC6909TokenSupply +// + +#[starknet::interface] +pub trait IERC6909TokenSupply { fn total_supply(self: @TState, id: u256) -> u256; +} - // IERC6909ContentURI +#[starknet::interface] +pub trait ERC6909TokenSupplyABI { + fn total_supply(self: @TState, id: u256) -> u256; +} + +// +// ERC6909ContentURI +// + +#[starknet::interface] +pub trait IERC6909ContentUri { + fn contract_uri(self: @TState) -> ByteArray; + fn token_uri(self: @TState, id: u256) -> ByteArray; +} + +#[starknet::interface] +pub trait IERC6909ContentUriABI { fn contract_uri(self: @TState) -> ByteArray; fn token_uri(self: @TState, id: u256) -> ByteArray; } diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index a8339c401..a3d9be1d4 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -23,7 +23,7 @@ pub mod ERC6909ContentURIComponent { +ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, +Drop, - > of interface::IERC6909ContentURI> { + > of interface::IERC6909ContentUri> { /// Returns the contract level URI. fn contract_uri(self: @ComponentState) -> ByteArray { self.ERC6909ContentURI_contract_uri.read() From 63b42acb89f70a519cc47cd00e618eb02efd7c18 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 29 Oct 2025 17:14:52 +0100 Subject: [PATCH 35/44] feat: add support for SRC5 --- packages/token/src/erc6909/erc6909.cairo | 27 +++++++++++++------ .../extensions/erc6909_content_uri.cairo | 9 ++++++- .../erc6909/extensions/erc6909_metadata.cairo | 11 ++++++++ .../extensions/erc6909_token_supply.cairo | 11 ++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 19c6bcb8f..0c3ca1219 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -8,6 +8,10 @@ pub mod ERC6909Component { use core::num::traits::{Bounded, Zero}; use openzeppelin_interfaces::erc6909 as interface; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_introspection::src5::SRC5Component::{ + InternalTrait as SRC5InternalTrait, SRC5Impl, + }; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::{ContractAddress, get_caller_address}; @@ -96,9 +100,14 @@ pub mod ERC6909Component { ) {} } + #[embeddable_as(ERC6909Impl)] impl ERC6909< - TContractState, +HasComponent, +ERC6909HooksTrait, + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +ERC6909HooksTrait, + +Drop, > of interface::IERC6909> { /// Returns the amount of `id` tokens owned by `account`. fn balance_of( @@ -172,13 +181,6 @@ pub mod ERC6909Component { self._set_operator(caller, spender, approved); true } - - /// Checks if a contract implements an interface. - fn supports_interface( - self: @ComponentState, interface_id: felt252, - ) -> bool { - interface_id == interface::IERC6909_ID || interface_id == ISRC6_ID - } } @@ -186,8 +188,17 @@ pub mod ERC6909Component { pub impl InternalImpl< TContractState, +HasComponent, + impl SRC5: SRC5Component::HasComponent, impl Hooks: ERC6909HooksTrait, + +Drop, > of InternalTrait { + /// Initializes the contract by regisrtering the supported interfaces + /// This should only be used inside the contract's constructor. + fn initializer(ref self: ComponentState) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(interface::IERC6909_ID); + } + /// Creates a `value` amount of tokens and assigns them to `account`. /// /// Requirements: diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index a3d9be1d4..dac60cba1 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -8,6 +8,8 @@ #[starknet::component] pub mod ERC6909ContentURIComponent { use openzeppelin_interfaces::erc6909 as interface; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin_token::erc6909::ERC6909Component; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; @@ -46,11 +48,16 @@ pub mod ERC6909ContentURIComponent { +HasComponent, impl ERC6909: ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, + impl SRC5: SRC5Component::HasComponent, +Drop, > of InternalTrait { - /// Sets the base URI. + /// Initializes the contract by setting the contract uri and declaring support + /// for the `IERC6909ContentUri` interface id. fn initializer(ref self: ComponentState, contract_uri: ByteArray) { self.ERC6909ContentURI_contract_uri.write(contract_uri); + + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(interface::IERC6909_CONTENT_URI_ID); } } } diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index 81046e90d..af449ff4f 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -8,6 +8,8 @@ pub mod ERC6909MetadataComponent { use core::num::traits::Zero; use openzeppelin_interfaces::erc6909 as interface; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin_token::erc6909::ERC6909Component; use starknet::ContractAddress; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; @@ -49,8 +51,16 @@ pub mod ERC6909MetadataComponent { +HasComponent, impl ERC6909: ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, + impl SRC5: SRC5Component::HasComponent, +Drop, > of InternalTrait { + /// Initializes the contract by declaring support for the `IERC6909Metadata` + /// interface id. + fn initialize(ref self: ComponentState) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(interface::IERC6909_METADATA_ID); + } + /// Updates the metadata of a token ID. fn _update_token_metadata( ref self: ComponentState, @@ -105,3 +115,4 @@ pub mod ERC6909MetadataComponent { } } } + diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index 9b33427f4..519bbc845 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -8,6 +8,8 @@ pub mod ERC6909TokenSupplyComponent { use core::num::traits::Zero; use openzeppelin_interfaces::erc6909 as interface; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin_token::erc6909::ERC6909Component; use starknet::ContractAddress; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; @@ -41,8 +43,16 @@ pub mod ERC6909TokenSupplyComponent { +HasComponent, impl ERC6909: ERC6909Component::HasComponent, +ERC6909Component::ERC6909HooksTrait, + impl SRC5: SRC5Component::HasComponent, +Drop, > of InternalTrait { + /// Initializes the contract by declaring support for the `IERC6909TokenSupply` + /// interface id. + fn initialize(ref self: ComponentState) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(interface::IERC6909_TOKEN_SUPPLY_ID); + } + /// Updates the total supply of a token ID. /// Ideally this function should be called in a `before_update` or `after_update` /// hook during mints and burns. @@ -67,3 +77,4 @@ pub mod ERC6909TokenSupplyComponent { } } } + From bc54abf5e351a5eb1351274adb2c69383d218514 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 30 Oct 2025 12:16:39 +0100 Subject: [PATCH 36/44] refactor: simplify errors --- packages/token/src/erc6909/erc6909.cairo | 35 ++++++++++++------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 0c3ca1219..63cd40c1c 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -70,14 +70,13 @@ pub mod ERC6909Component { pub mod Errors { pub const INSUFFICIENT_BALANCE: felt252 = 'ERC6909: insufficient balance'; pub const INSUFFICIENT_ALLOWANCE: felt252 = 'ERC6909: insufficient allowance'; - pub const TRANSFER_FROM_ZERO: felt252 = 'ERC6909: transfer from 0'; - pub const TRANSFER_TO_ZERO: felt252 = 'ERC6909: transfer to 0'; - pub const MINT_TO_ZERO: felt252 = 'ERC6909: mint to 0'; - pub const BURN_FROM_ZERO: felt252 = 'ERC6909: burn from 0'; - pub const APPROVE_FROM_ZERO: felt252 = 'ERC6909: approve from 0'; - pub const APPROVE_TO_ZERO: felt252 = 'ERC6909: approve to 0'; + pub const INVALID_APPROVER: felt252 = 'ERC6909: invalid approver'; + pub const INVALID_RECEIVER: felt252 = 'ERC6909: invalid receiver'; + pub const INVALID_SENDER: felt252 = 'ERC6909: invalid sender'; + pub const INVALID_SPENDER: felt252 = 'ERC6909: invalid spender'; } + // // Hooks // @@ -206,14 +205,14 @@ pub mod ERC6909Component { /// - `receiver` is not the zero address. /// /// Emits a `Transfer` event with `from` set to the zero address. - fn mint( + fn _mint( ref self: ComponentState, receiver: ContractAddress, id: u256, amount: u256, ) { - assert(!receiver.is_zero(), Errors::MINT_TO_ZERO); - self.update(Zero::zero(), receiver, id, amount); + assert(!receiver.is_zero(), Errors::INVALID_RECEIVER); + self._update(Zero::zero(), receiver, id, amount); } /// Destroys `amount` of tokens from `account`. @@ -224,14 +223,14 @@ pub mod ERC6909Component { /// - `account` must have at least a balance of `amount`. /// /// Emits a `Transfer` event with `to` set to the zero address. - fn burn( + fn _burn( ref self: ComponentState, account: ContractAddress, id: u256, amount: u256, ) { - assert(!account.is_zero(), Errors::BURN_FROM_ZERO); - self.update(account, Zero::zero(), id, amount); + assert(!account.is_zero(), Errors::INVALID_SENDER); + self._update(account, Zero::zero(), id, amount); } /// Transfers an `amount` of tokens from `sender` to `receiver`, or alternatively mints (or @@ -242,7 +241,7 @@ pub mod ERC6909Component { /// left to the extensions instead. /// /// Emits a `Transfer` event. - fn update( + fn _update( ref self: ComponentState, sender: ContractAddress, receiver: ContractAddress, @@ -316,8 +315,8 @@ pub mod ERC6909Component { id: u256, amount: u256, ) { - assert(owner.is_non_zero(), Errors::APPROVE_FROM_ZERO); - assert(spender.is_non_zero(), Errors::APPROVE_TO_ZERO); + assert(owner.is_non_zero(), Errors::INVALID_APPROVER); + assert(spender.is_non_zero(), Errors::INVALID_SPENDER); self.ERC6909_allowances.write((owner, spender, id), amount); self.emit(Approval { owner, spender, id, amount }); } @@ -338,9 +337,9 @@ pub mod ERC6909Component { id: u256, amount: u256, ) { - assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); - assert(!receiver.is_zero(), Errors::TRANSFER_TO_ZERO); - self.update(sender, receiver, id, amount); + assert(!sender.is_zero(), Errors::INVALID_SENDER); + assert(!receiver.is_zero(), Errors::INVALID_RECEIVER); + self._update(sender, receiver, id, amount); } } } From 34846617714df8fadcfb3d3436c27eb12bff6939 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 12 Nov 2025 16:01:42 +0100 Subject: [PATCH 37/44] feat(macros): add ERC6909 components to with_components --- .../attribute/with_components/components.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/macros/src/attribute/with_components/components.rs b/packages/macros/src/attribute/with_components/components.rs index 990d673b6..34f6c9148 100644 --- a/packages/macros/src/attribute/with_components/components.rs +++ b/packages/macros/src/attribute/with_components/components.rs @@ -22,6 +22,10 @@ pub enum AllowedComponents { ERC1155Receiver, ERC2981, ERC4626, + ERC6909, + ERC6909ContentURI, + ERC6909Metadata, + ERC6909TokenSupply, Upgradeable, Nonces, Multisig, @@ -60,6 +64,10 @@ impl AllowedComponents { "ERC1155Receiver" => Ok(AllowedComponents::ERC1155Receiver), "ERC2981" => Ok(AllowedComponents::ERC2981), "ERC4626" => Ok(AllowedComponents::ERC4626), + "ERC6909" => Ok(AllowedComponents::ERC6909), + "ERC6909ContentURI" => Ok(AllowedComponents::ERC6909ContentURI), + "ERC6909Metadata" => Ok(AllowedComponents::ERC6909Metadata), + "ERC6909TokenSupply" => Ok(AllowedComponents::ERC6909TokenSupply), "Upgradeable" => Ok(AllowedComponents::Upgradeable), "Nonces" => Ok(AllowedComponents::Nonces), "Multisig" => Ok(AllowedComponents::Multisig), @@ -249,6 +257,42 @@ impl AllowedComponents { has_immutable_config: true, internal_impls: vec!["InternalImpl"], }, + AllowedComponents::ERC6909 => ComponentInfo { + name: "ERC6909Component", + path: "openzeppelin_token::erc6909::ERC6909Component", + storage: "erc6909", + event: "ERC6909Event", + has_initializer: true, + has_immutable_config: false, + internal_impls: vec!["InternalImpl"], + }, + AllowedComponents::ERC6909ContentURI => ComponentInfo { + name: "ERC6909ContentURIComponent", + path: "openzeppelin_token::erc6909::extensions::ERC6909ContentURIComponent", + storage: "erc6909_content_uri", + event: "ERC6909ContentURIEvent", + has_initializer: true, + has_immutable_config: false, + internal_impls: vec!["InternalImpl"], + }, + AllowedComponents::ERC6909Metadata => ComponentInfo { + name: "ERC6909MetadataComponent", + path: "openzeppelin_token::erc6909::extensions::ERC6909MetadataComponent", + storage: "erc6909_metadata", + event: "ERC6909MetadataEvent", + has_initializer: true, + has_immutable_config: false, + internal_impls: vec!["InternalImpl"], + }, + AllowedComponents::ERC6909TokenSupply => ComponentInfo { + name: "ERC6909TokenSupplyComponent", + path: "openzeppelin_token::erc6909::extensions::ERC6909TokenSupplyComponent", + storage: "erc6909_token_supply", + event: "ERC6909TokenSupplyEvent", + has_initializer: true, + has_immutable_config: false, + internal_impls: vec!["InternalImpl"], + }, AllowedComponents::Upgradeable => ComponentInfo { name: "UpgradeableComponent", path: "openzeppelin_upgrades::UpgradeableComponent", From 587e72762284df586cc033e7aff34600a56e3dd2 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 12 Nov 2025 16:01:58 +0100 Subject: [PATCH 38/44] refactor(erc6909): make Storage structs public --- packages/token/src/erc6909/erc6909.cairo | 2 +- .../token/src/erc6909/extensions/erc6909_content_uri.cairo | 2 +- packages/token/src/erc6909/extensions/erc6909_metadata.cairo | 4 ++-- .../token/src/erc6909/extensions/erc6909_token_supply.cairo | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo index 63cd40c1c..ea4932c7e 100644 --- a/packages/token/src/erc6909/erc6909.cairo +++ b/packages/token/src/erc6909/erc6909.cairo @@ -16,7 +16,7 @@ pub mod ERC6909Component { use starknet::{ContractAddress, get_caller_address}; #[storage] - struct Storage { + pub struct Storage { ERC6909_balances: Map<(ContractAddress, u256), u256>, ERC6909_allowances: Map<(ContractAddress, ContractAddress, u256), u256>, ERC6909_operators: Map<(ContractAddress, ContractAddress), bool>, diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo index dac60cba1..f1ce1a172 100644 --- a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -14,7 +14,7 @@ pub mod ERC6909ContentURIComponent { use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; #[storage] - struct Storage { + pub struct Storage { ERC6909ContentURI_contract_uri: ByteArray, } diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo index af449ff4f..572139571 100644 --- a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -15,7 +15,7 @@ pub mod ERC6909MetadataComponent { use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; #[storage] - struct Storage { + pub struct Storage { ERC6909Metadata_name: Map, ERC6909Metadata_symbol: Map, ERC6909Metadata_decimals: Map, @@ -56,7 +56,7 @@ pub mod ERC6909MetadataComponent { > of InternalTrait { /// Initializes the contract by declaring support for the `IERC6909Metadata` /// interface id. - fn initialize(ref self: ComponentState) { + fn initializer(ref self: ComponentState) { let mut src5_component = get_dep_component_mut!(ref self, SRC5); src5_component.register_interface(interface::IERC6909_METADATA_ID); } diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo index 519bbc845..343b1a07c 100644 --- a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -15,7 +15,7 @@ pub mod ERC6909TokenSupplyComponent { use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; #[storage] - struct Storage { + pub struct Storage { ERC6909TokenSupply_total_supply: Map, } @@ -48,7 +48,7 @@ pub mod ERC6909TokenSupplyComponent { > of InternalTrait { /// Initializes the contract by declaring support for the `IERC6909TokenSupply` /// interface id. - fn initialize(ref self: ComponentState) { + fn initializer(ref self: ComponentState) { let mut src5_component = get_dep_component_mut!(ref self, SRC5); src5_component.register_interface(interface::IERC6909_TOKEN_SUPPLY_ID); } From 72dd5a1a2ea041eb838dde916ce557e1b94850dd Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 12 Nov 2025 16:02:36 +0100 Subject: [PATCH 39/44] refactor(test-common): replace ERC6909 mocks and helpers --- packages/test_common/src/erc6909.cairo | 92 ++++++ packages/test_common/src/lib.cairo | 1 + packages/test_common/src/mocks.cairo | 1 + packages/test_common/src/mocks/erc6909.cairo | 186 +++++++++++++ .../src/mocks/erc6909_content_uri_mocks.cairo | 50 ---- .../src/mocks/erc6909_metadata_mocks.cairo | 75 ----- .../test_common/src/mocks/erc6909_mocks.cairo | 263 ------------------ .../mocks/erc6909_token_supply_mocks.cairo | 76 ----- 8 files changed, 280 insertions(+), 464 deletions(-) create mode 100644 packages/test_common/src/erc6909.cairo create mode 100644 packages/test_common/src/mocks/erc6909.cairo delete mode 100644 packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo delete mode 100644 packages/test_common/src/mocks/erc6909_metadata_mocks.cairo delete mode 100644 packages/test_common/src/mocks/erc6909_mocks.cairo delete mode 100644 packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo diff --git a/packages/test_common/src/erc6909.cairo b/packages/test_common/src/erc6909.cairo new file mode 100644 index 000000000..1c7a71568 --- /dev/null +++ b/packages/test_common/src/erc6909.cairo @@ -0,0 +1,92 @@ +use openzeppelin_testing::{EventSpyExt, EventSpyQueue as EventSpy, ExpectedEvent}; +use starknet::ContractAddress; + +#[generate_trait] +pub impl ERC6909SpyHelpersImpl of ERC6909SpyHelpers { + fn assert_event_operator_set( + ref self: EventSpy, + contract: ContractAddress, + owner: ContractAddress, + spender: ContractAddress, + approved: bool, + ) { + let expected = ExpectedEvent::new() + .key(selector!("OperatorSet")) + .key(owner) + .key(spender) + .data(approved); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_operator_set( + ref self: EventSpy, + contract: ContractAddress, + owner: ContractAddress, + spender: ContractAddress, + approved: bool, + ) { + self.assert_event_operator_set(contract, owner, spender, approved); + self.assert_no_events_left_from(contract); + } + + fn assert_event_approval( + ref self: EventSpy, + contract: ContractAddress, + owner: ContractAddress, + spender: ContractAddress, + id: u256, + amount: u256, + ) { + let expected = ExpectedEvent::new() + .key(selector!("Approval")) + .key(owner) + .key(spender) + .key(id) + .data(amount); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_approval( + ref self: EventSpy, + contract: ContractAddress, + owner: ContractAddress, + spender: ContractAddress, + id: u256, + amount: u256, + ) { + self.assert_event_approval(contract, owner, spender, id, amount); + self.assert_no_events_left_from(contract); + } + + fn assert_event_transfer( + ref self: EventSpy, + contract: ContractAddress, + caller: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256, + ) { + let expected = ExpectedEvent::new() + .key(selector!("Transfer")) + .data(caller) + .key(sender) + .key(receiver) + .key(id) + .data(amount); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_transfer( + ref self: EventSpy, + contract: ContractAddress, + caller: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256, + ) { + self.assert_event_transfer(contract, caller, sender, receiver, id, amount); + self.assert_no_events_left_from(contract); + } +} diff --git a/packages/test_common/src/lib.cairo b/packages/test_common/src/lib.cairo index fc1d892b5..56d8dac92 100644 --- a/packages/test_common/src/lib.cairo +++ b/packages/test_common/src/lib.cairo @@ -2,6 +2,7 @@ pub mod account; pub mod common; pub mod erc1155; pub mod erc20; +pub mod erc6909; pub mod erc721; pub mod eth_account; pub mod math; diff --git a/packages/test_common/src/mocks.cairo b/packages/test_common/src/mocks.cairo index cccf9a26b..8df763166 100644 --- a/packages/test_common/src/mocks.cairo +++ b/packages/test_common/src/mocks.cairo @@ -6,6 +6,7 @@ pub mod erc1155; pub mod erc20; pub mod erc2981; pub mod erc4626; +pub mod erc6909; pub mod erc721; pub mod governor; pub mod multisig; diff --git a/packages/test_common/src/mocks/erc6909.cairo b/packages/test_common/src/mocks/erc6909.cairo new file mode 100644 index 000000000..224b1f8e2 --- /dev/null +++ b/packages/test_common/src/mocks/erc6909.cairo @@ -0,0 +1,186 @@ +#[starknet::contract] +#[with_components(ERC6909, SRC5)] +pub mod ERC6909Mock { + use openzeppelin_token::erc6909::ERC6909HooksEmptyImpl; + use starknet::ContractAddress; + + + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage {} + + #[constructor] + fn constructor(ref self: ContractState, recipient: ContractAddress, id: u256, amount: u256) { + self.erc6909.initializer(); + self.erc6909._mint(recipient, id, amount); + } +} + + +#[starknet::contract] +#[with_components(ERC6909, SRC5)] +pub mod ERC6909MockWithHooks { + use starknet::ContractAddress; + + + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + BeforeUpdate: BeforeUpdate, + AfterUpdate: AfterUpdate, + } + + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct BeforeUpdate { + pub from: ContractAddress, + pub recipient: ContractAddress, + pub id: u256, + pub amount: u256, + } + + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct AfterUpdate { + pub from: ContractAddress, + pub recipient: ContractAddress, + pub id: u256, + pub amount: u256, + } + + #[constructor] + fn constructor(ref self: ContractState, recipient: ContractAddress, id: u256, amount: u256) { + self.erc6909.initializer(); + self.erc6909._mint(recipient, id, amount); + } + + impl ERC6909HooksImpl of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.emit(BeforeUpdate { from, recipient, id, amount }); + } + + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.emit(AfterUpdate { from, recipient, id, amount }); + } + } +} + +#[starknet::contract] +#[with_components(ERC6909, ERC6909ContentURI, SRC5)] +pub mod ERC6909ContentURIMock { + use openzeppelin_token::erc6909::ERC6909HooksEmptyImpl; + use starknet::ContractAddress; + + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + #[abi(embed_v0)] + impl ERC6909ContentURIImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage {} + + #[constructor] + fn constructor( + ref self: ContractState, + contract_uri: ByteArray, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + self.erc6909.initializer(); + self.erc6909_content_uri.initializer(contract_uri); + self.erc6909._mint(recipient, id, amount); + } +} + +#[starknet::contract] +#[with_components(ERC6909, ERC6909Metadata, SRC5)] +pub mod ERC6909MetadataMock { + use openzeppelin_token::erc6909::ERC6909HooksEmptyImpl; + use starknet::ContractAddress; + + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + #[abi(embed_v0)] + impl ERC6909MetadataImpl = + ERC6909MetadataComponent::ERC6909MetadataImpl; + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage {} + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + decimals: u8, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + self.erc6909.initializer(); + self.erc6909_metadata.initializer(); + self.erc6909_metadata._set_token_metadata(id, name, symbol, decimals); + self.erc6909._mint(recipient, id, amount); + } +} + +#[starknet::contract] +#[with_components(ERC6909, ERC6909TokenSupply, SRC5)] +pub mod ERC6909TokenSupplyMock { + use openzeppelin_token::erc6909::ERC6909HooksEmptyImpl; + use starknet::ContractAddress; + + + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + #[abi(embed_v0)] + impl ERC6909TokenSupplyImpl = + ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage {} + + + #[constructor] + fn constructor(ref self: ContractState, recipient: ContractAddress, id: u256, amount: u256) { + self.erc6909.initializer(); + self.erc6909_token_supply.initializer(); + self.erc6909._mint(recipient, id, amount); + } +} diff --git a/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo b/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo deleted file mode 100644 index 452d2f72f..000000000 --- a/packages/test_common/src/mocks/erc6909_content_uri_mocks.cairo +++ /dev/null @@ -1,50 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC6909ContentURIMock { - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use starknet::ContractAddress; - - component!( - path: ERC6909ContentURIComponent, - storage: erc6909_content_uri, - event: ERC6909ContentURIEvent - ); - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - - // ERC6909ContentURI - #[abi(embed_v0)] - impl ERC6909ContentURIComponentImpl = - ERC6909ContentURIComponent::ERC6909ContentURIImpl; - - // ERC6909Mixin - #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - - impl ERC6909InternalImpl = ERC6909Component::InternalImpl; - impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc6909_content_uri: ERC6909ContentURIComponent::Storage, - #[substorage(v0)] - erc6909: ERC6909Component::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, - #[flat] - ERC6909Event: ERC6909Component::Event, - } - - #[constructor] - fn constructor( - ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray - ) { - self.erc6909.mint(receiver, id, amount); - self.erc6909_content_uri.initializer(uri); - } -} diff --git a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo b/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo deleted file mode 100644 index 3071e5ddf..000000000 --- a/packages/test_common/src/mocks/erc6909_metadata_mocks.cairo +++ /dev/null @@ -1,75 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC6909MetadataMock { - use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; - use starknet::ContractAddress; - - component!( - path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent - ); - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - - // ERC6909Metadata - #[abi(embed_v0)] - impl ERC6909MetadataComponentImpl = - ERC6909MetadataComponent::ERC6909MetadataImpl; - - // ERC6909Mixin - #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - - impl ERC6909InternalImpl = ERC6909Component::InternalImpl; - impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc6909_metadata: ERC6909MetadataComponent::Storage, - #[substorage(v0)] - erc6909: ERC6909Component::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909MetadataEvent: ERC6909MetadataComponent::Event, - #[flat] - ERC6909Event: ERC6909Component::Event, - } - - #[constructor] - fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { - self.erc6909.mint(receiver, id, amount); - } - - impl ERC6909MetadataHooksImpl< - TContractState, - impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, - impl HasComponent: ERC6909Component::HasComponent, - +Drop - > of ERC6909Component::ERC6909HooksTrait { - fn before_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) {} - - /// Update after any transfer - fn after_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) { - let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); - let name = "MyERC6909Token"; - let symbol = "MET"; - let decimals = 18; - erc6909_metadata_component._update_token_metadata(from, id, name, symbol, decimals); - } - } -} diff --git a/packages/test_common/src/mocks/erc6909_mocks.cairo b/packages/test_common/src/mocks/erc6909_mocks.cairo deleted file mode 100644 index 460a11502..000000000 --- a/packages/test_common/src/mocks/erc6909_mocks.cairo +++ /dev/null @@ -1,263 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC6909Mock { - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use starknet::ContractAddress; - - /// Component - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - - /// ABI of Components - #[abi(embed_v0)] - impl ERC6909Impl = ERC6909Component::ERC6909Impl; - #[abi(embed_v0)] - impl ERC6909CamelOnlyImpl = - ERC6909Component::ERC6909CamelOnlyImpl; - - /// Internal logic - impl InternalImpl = ERC6909Component::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { - self.erc6909.mint(receiver, id, amount); - } -} - -#[starknet::contract] -pub(crate) mod SnakeERC6909Mock { - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use starknet::ContractAddress; - - /// Component - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - - /// ABI of Components - #[abi(embed_v0)] - impl ERC6909Impl = ERC6909Component::ERC6909Impl; - - /// Internal logic - impl InternalImpl = ERC6909Component::InternalImpl; - - - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { - self.erc6909.mint(receiver, id, amount); - } -} - -#[starknet::contract] -pub(crate) mod CamelERC6909Mock { - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use starknet::ContractAddress; - - /// Component - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - - #[abi(embed_v0)] - impl ERC6909CamelOnlyImpl = - ERC6909Component::ERC6909CamelOnlyImpl; - - // `ERC6909Impl` is not embedded because it would defeat the purpose of the - // mock. The `ERC6909Impl` case-agnostic methods are manually exposed. - impl ERC6909Impl = ERC6909Component::ERC6909Impl; - impl InternalImpl = ERC6909Component::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { - self.erc6909.mint(receiver, id, amount); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, - ) -> u256 { - self.erc6909.allowance(owner, spender, id) - } - - #[external(v0)] - fn transfer( - ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool { - self.erc6909.transfer(receiver, id, amount) - } - - #[external(v0)] - fn approve( - ref self: ContractState, spender: ContractAddress, id: u256, amount: u256 - ) -> bool { - self.erc6909.approve(spender, id, amount) - } - } -} - -/// Although these modules are designed to panic, functions -/// still need a valid return value. We chose: -/// -/// 3 for felt252, u8, and u256 -/// zero for ContractAddress -/// false for bool -#[starknet::contract] -pub(crate) mod SnakeERC6909Panic { - use starknet::ContractAddress; - - #[storage] - struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn balance_of(self: @ContractState, account: ContractAddress, id: u256) -> u256 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, - ) -> u256 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn is_operator( - self: @ContractState, owner: ContractAddress, spender: ContractAddress, - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn transfer( - ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn transfer_from( - ref self: ContractState, - sender: ContractAddress, - receiver: ContractAddress, - id: u256, - amount: u256 - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn approve( - ref self: ContractState, spender: ContractAddress, id: u256, amount: u256 - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn set_operator(ref self: ContractState, spender: ContractAddress, approved: bool) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - panic!("Some error"); - false - } - } -} - -#[starknet::contract] -pub(crate) mod CamelERC6909Panic { - use starknet::ContractAddress; - - #[storage] - struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn balanceOf(self: @ContractState, account: ContractAddress, id: u256) -> u256 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn transferFrom( - ref self: ContractState, - sender: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn setOperator(ref self: ContractState, spender: ContractAddress, approved: bool) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn supportsInterface(self: @ContractState, interface_id: felt252) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn isOperator( - self: @ContractState, owner: ContractAddress, spender: ContractAddress, - ) -> bool { - panic!("Some error"); - false - } - } -} - diff --git a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo b/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo deleted file mode 100644 index e0f123575..000000000 --- a/packages/test_common/src/mocks/erc6909_token_supply_mocks.cairo +++ /dev/null @@ -1,76 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC6909TokenSupplyMock { - use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; - use starknet::ContractAddress; - - component!( - path: ERC6909TokenSupplyComponent, - storage: erc6909_token_supply, - event: ERC6909TokenSupplyEvent - ); - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - - // ERC6909TokenSupply - #[abi(embed_v0)] - impl ERC6909TokenSupplyComponentImpl = - ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; - - // ERC6909Mixin - #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - - impl ERC6909InternalImpl = ERC6909Component::InternalImpl; - impl ERC6909TokenSupplyInternalImpl = ERC6909TokenSupplyComponent::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc6909_token_supply: ERC6909TokenSupplyComponent::Storage, - #[substorage(v0)] - erc6909: ERC6909Component::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909TokenSupplyEvent: ERC6909TokenSupplyComponent::Event, - #[flat] - ERC6909Event: ERC6909Component::Event, - } - - #[constructor] - fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { - self.erc6909.mint(receiver, id, amount); - } - - impl ERC6909TokenSupplyHooksImpl< - TContractState, - impl ERC6909TokenSupply: ERC6909TokenSupplyComponent::HasComponent, - impl HasComponent: ERC6909Component::HasComponent, - +Drop - > of ERC6909Component::ERC6909HooksTrait { - fn before_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) {} - - /// Update after any transfer - fn after_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) { - let mut erc6909_token_supply_component = get_dep_component_mut!( - ref self, ERC6909TokenSupply - ); - erc6909_token_supply_component._update_token_supply(from, recipient, id, amount); - } - } -} From 767241be631421878913b712a4146da701a5a556 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 12 Nov 2025 16:03:04 +0100 Subject: [PATCH 40/44] test(erc6909): add core and extension tests --- packages/token/src/tests.cairo | 1 + packages/token/src/tests/erc6909.cairo | 7 +- .../src/tests/erc6909/test_erc6909.cairo | 628 +++++++----------- .../erc6909/test_erc6909_content_uri.cairo | 123 ++-- .../tests/erc6909/test_erc6909_metadata.cairo | 157 ++--- .../erc6909/test_erc6909_token_supply.cairo | 168 ++--- 6 files changed, 397 insertions(+), 687 deletions(-) diff --git a/packages/token/src/tests.cairo b/packages/token/src/tests.cairo index a9654141f..523aef8f8 100644 --- a/packages/token/src/tests.cairo +++ b/packages/token/src/tests.cairo @@ -2,4 +2,5 @@ pub mod erc1155; pub mod erc20; pub mod erc2981; pub mod erc4626; +pub mod erc6909; pub mod erc721; diff --git a/packages/token/src/tests/erc6909.cairo b/packages/token/src/tests/erc6909.cairo index 8d9685af3..ab1b5448a 100644 --- a/packages/token/src/tests/erc6909.cairo +++ b/packages/token/src/tests/erc6909.cairo @@ -1,7 +1,2 @@ -pub(crate) mod common; - -mod test_dual6909; mod test_erc6909; -mod test_erc6909_content_uri; -mod test_erc6909_metadata; -mod test_erc6909_token_supply; + diff --git a/packages/token/src/tests/erc6909/test_erc6909.cairo b/packages/token/src/tests/erc6909/test_erc6909.cairo index 98979e574..a4a068258 100644 --- a/packages/token/src/tests/erc6909/test_erc6909.cairo +++ b/packages/token/src/tests/erc6909/test_erc6909.cairo @@ -1,538 +1,366 @@ -use core::integer::BoundedInt; -use core::starknet::{ContractAddress, testing}; -use openzeppelin::introspection::interface::ISRC5_ID; -use openzeppelin::tests::mocks::erc6909_mocks::DualCaseERC6909Mock; -use openzeppelin::tests::utils::constants::{ - ZERO, OWNER, SPENDER, RECIPIENT, SUPPLY, VALUE, OPERATOR -}; -use openzeppelin::tests::utils; -use openzeppelin::token::erc6909::ERC6909Component::{ - InternalImpl, ERC6909Impl, ERC6909CamelOnlyImpl -}; -use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet}; -use openzeppelin::token::erc6909::ERC6909Component; -use super::common::{ - assert_event_approval, assert_only_event_approval, assert_only_event_transfer, - assert_only_event_operator_set, assert_event_operator_set -}; - -// -// Setup -// - -const TOKEN_ID: u256 = 420; - -type ComponentState = ERC6909Component::ComponentState; +use core::num::traits::Bounded; +use openzeppelin_interfaces::erc6909 as interface; +use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::erc6909::ERC6909SpyHelpers; +use openzeppelin_test_common::mocks::erc6909::{ERC6909Mock, ERC6909MockWithHooks}; +use openzeppelin_testing::constants::{OWNER, RECIPIENT, SPENDER, SUPPLY, TOKEN_ID, VALUE, ZERO}; +use openzeppelin_testing::{EventSpyExt, EventSpyQueue as EventSpy, ExpectedEvent, spy_events}; +use snforge_std::{start_cheat_caller_address, test_address}; +use starknet::ContractAddress; +use crate::erc6909::ERC6909Component; +use crate::erc6909::ERC6909Component::{ERC6909Impl, InternalImpl}; + +type ComponentState = ERC6909Component::ComponentState; +type ComponentStateWithHooks = + ERC6909Component::ComponentState; + + +fn CONTRACT_STATE() -> ERC6909Mock::ContractState { + ERC6909Mock::contract_state_for_testing() +} fn COMPONENT_STATE() -> ComponentState { ERC6909Component::component_state_for_testing() } +fn COMPONENT_STATE_WITH_HOOKS() -> ComponentStateWithHooks { + ERC6909Component::component_state_for_testing() +} + fn setup() -> ComponentState { let mut state = COMPONENT_STATE(); - state.mint(OWNER(), TOKEN_ID, SUPPLY); - utils::drop_event(ZERO()); + state.initializer(); + state._mint(OWNER, TOKEN_ID, SUPPLY); state } -// -// Getters -// - -#[test] -fn test_balance_of() { - let mut state = COMPONENT_STATE(); - state.mint(OWNER(), TOKEN_ID, SUPPLY); - assert_eq!(state.balance_of((OWNER()), TOKEN_ID), SUPPLY); +fn setup_with_hooks() -> ComponentStateWithHooks { + let mut state = COMPONENT_STATE_WITH_HOOKS(); + state.initializer(); + state._mint(OWNER, TOKEN_ID, SUPPLY); + state } + #[test] -fn test_balanceOf() { +fn test_initializer() { let mut state = COMPONENT_STATE(); - state.mint(OWNER(), TOKEN_ID, SUPPLY); - assert_eq!(state.balanceOf((OWNER()), TOKEN_ID), SUPPLY); -} + let mock_state = CONTRACT_STATE(); -#[test] -fn test_allowance() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, VALUE); - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(allowance, VALUE); -} + state.initializer(); -#[test] -fn test_set_supports_interface() { - let mut state = setup(); - // IERC6909_ID as defined in `interface.cairo` = - // 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee - assert!( - state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee) - ); - assert_eq!(state.supports_interface(0x32cb), false); - assert_eq!( - state.supports_interface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ef), - false - ); - - // id == ISRC5_ID || id == IERC6909_ID - assert!(state.supports_interface(ISRC5_ID)) -} + let supports_ierc6909 = mock_state.supports_interface(interface::IERC6909_ID); + assert!(supports_ierc6909); -#[test] -fn test_set_supportsInterface() { - let mut state = setup(); - // IERC6909_ID as defined in `interface.cairo` = - // 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee - assert!( - state.supportsInterface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee) - ); - assert_eq!(state.supportsInterface(0x32cb), false); - assert_eq!( - state.supportsInterface(0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ef), - false - ); - - // id == ISRC5_ID || id == IERC6909_ID - assert!(state.supportsInterface(ISRC5_ID)) + let supports_isrc5 = mock_state + .supports_interface(openzeppelin_interfaces::introspection::ISRC5_ID); + assert!(supports_isrc5); } -// -// approve & _approve -// - #[test] -fn test_approve() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - assert!(state.approve(SPENDER(), TOKEN_ID, VALUE)); - assert_only_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, VALUE); - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(allowance, VALUE); +fn test_balance_of() { + let state = setup(); + assert_eq!(state.balance_of(OWNER, TOKEN_ID), SUPPLY); } #[test] -#[should_panic(expected: ('ERC6909: approve from 0',))] -fn test_approve_from_zero() { +fn test_allowance() { let mut state = setup(); - state.approve(SPENDER(), TOKEN_ID, VALUE); + assert_eq!(state.allowance(OWNER, SPENDER, TOKEN_ID), 0); + state._approve(OWNER, SPENDER, TOKEN_ID, VALUE); + assert_eq!(state.allowance(OWNER, SPENDER, TOKEN_ID), VALUE); } #[test] -#[should_panic(expected: ('ERC6909: approve to 0',))] -fn test_approve_to_zero() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(ZERO(), TOKEN_ID, VALUE); -} +fn test_is_operator() { + let mut state = COMPONENT_STATE(); -#[test] -fn test__approve() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - state._approve(OWNER(), SPENDER(), TOKEN_ID, VALUE); - assert_only_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, VALUE); - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID,); - assert_eq!(allowance, VALUE); -} + let not_operator = !state.is_operator(OWNER, SPENDER); + assert!(not_operator); -#[test] -#[should_panic(expected: ('ERC6909: approve from 0',))] -fn test__approve_from_zero() { - let mut state = setup(); - state._approve(ZERO(), SPENDER(), TOKEN_ID, VALUE); -} + state._set_operator(OWNER, SPENDER, true); + let is_operator = state.is_operator(OWNER, SPENDER); + assert!(is_operator); -#[test] -#[should_panic(expected: ('ERC6909: approve to 0',))] -fn test__approve_to_zero() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - state._approve(OWNER(), ZERO(), TOKEN_ID, VALUE); + state._set_operator(OWNER, SPENDER, false); + let not_operator = !state.is_operator(OWNER, SPENDER); + assert!(not_operator); } -// -// transfer & _transfer -// #[test] -fn test_transfer() { +fn test_transfer_success() { let mut state = setup(); - testing::set_caller_address(OWNER()); - assert!(state.transfer(RECIPIENT(), TOKEN_ID, VALUE)); - - assert_only_event_transfer(ZERO(), OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); - assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); -} + let contract_address = test_address(); + let caller = OWNER; + let receiver = RECIPIENT; + let id = TOKEN_ID; + let amount = VALUE; -#[test] -#[should_panic(expected: ('ERC6909: insufficient balance',))] -fn test_transfer_not_enough_balance() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - let balance_plus_one = SUPPLY + 1; - state.transfer(RECIPIENT(), TOKEN_ID, balance_plus_one); -} + start_cheat_caller_address(contract_address, caller); -#[test] -#[should_panic(expected: ('ERC6909: transfer from 0',))] -fn test_transfer_from_zero() { - let mut state = setup(); - state.transfer(RECIPIENT(), TOKEN_ID, VALUE); -} + let mut spy = spy_events(); + assert_state_before_transfer(caller, receiver, id, amount, SUPPLY); -#[test] -#[should_panic(expected: ('ERC6909: transfer to 0',))] -fn test_transfer_to_zero() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - state.transfer(ZERO(), TOKEN_ID, VALUE); -} + let ok = state.transfer(receiver, id, amount); + assert!(ok); -#[test] -fn test__transfer() { - let mut state = setup(); - state._transfer(OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); - assert_only_event_transfer(ZERO(), OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); - assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + spy.assert_only_event_transfer(contract_address, caller, caller, receiver, id, amount); + assert_state_after_transfer(caller, receiver, id, amount, SUPPLY); } #[test] -#[should_panic(expected: ('ERC6909: insufficient balance',))] -fn test__transfer_not_enough_balance() { +#[should_panic(expected: 'ERC6909: invalid receiver')] +fn test_transfer_invalid_receiver_zero() { let mut state = setup(); - testing::set_caller_address(OWNER()); - let balance_plus_one = SUPPLY + 1; - state._transfer(OWNER(), OWNER(), RECIPIENT(), TOKEN_ID, balance_plus_one); + start_cheat_caller_address(test_address(), OWNER); + state.transfer(ZERO, TOKEN_ID, VALUE); } #[test] -#[should_panic(expected: ('ERC6909: transfer from 0',))] -fn test__transfer_from_zero() { +#[should_panic(expected: 'ERC6909: insufficient balance')] +fn test_transfer_insufficient_balance() { let mut state = setup(); - state._transfer(ZERO(), ZERO(), RECIPIENT(), TOKEN_ID, VALUE); + start_cheat_caller_address(test_address(), SPENDER); + state.transfer(RECIPIENT, TOKEN_ID, VALUE); } -#[test] -#[should_panic(expected: ('ERC6909: transfer to 0',))] -fn test__transfer_to_zero() { - let mut state = setup(); - state._transfer(OWNER(), OWNER(), ZERO(), TOKEN_ID, VALUE); -} #[test] -fn test_self_transfer() { +fn test_transfer_from_by_sender_itself() { let mut state = setup(); - testing::set_caller_address(OWNER()); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); - assert!(state.transfer(OWNER(), TOKEN_ID, 1)); - assert_only_event_transfer(ZERO(), OWNER(), OWNER(), OWNER(), TOKEN_ID, 1); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); -} + let contract_address = test_address(); + let sender = OWNER; + let receiver = RECIPIENT; + start_cheat_caller_address(contract_address, sender); + let mut spy = spy_events(); -// -// transfer_from & transferFrom -// + assert_state_before_transfer(sender, receiver, TOKEN_ID, VALUE, SUPPLY); + let ok = state.transfer_from(sender, receiver, TOKEN_ID, VALUE); + assert!(ok); + spy.assert_only_event_transfer(contract_address, sender, sender, receiver, TOKEN_ID, VALUE); + assert_state_after_transfer(sender, receiver, TOKEN_ID, VALUE, SUPPLY); +} #[test] -fn test_transfer_from() { +fn test_transfer_from_with_allowance() { let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, VALUE); - utils::drop_event(ZERO()); + let contract_address = test_address(); + + state._approve(OWNER, SPENDER, TOKEN_ID, VALUE); + assert_eq!(state.allowance(OWNER, SPENDER, TOKEN_ID), VALUE); - testing::set_caller_address(SPENDER()); - assert!(state.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE)); + start_cheat_caller_address(contract_address, SPENDER); + let mut spy = spy_events(); - assert_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, 0); - assert_only_event_transfer(ZERO(), SPENDER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + let ok = state.transfer_from(OWNER, RECIPIENT, TOKEN_ID, VALUE); + assert!(ok); - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(allowance, 0); + spy.assert_event_approval(contract_address, OWNER, SPENDER, TOKEN_ID, 0); + spy.assert_only_event_transfer(contract_address, SPENDER, OWNER, RECIPIENT, TOKEN_ID, VALUE); - assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.allowance(OWNER, SPENDER, TOKEN_ID), 0); + assert_eq!(state.balance_of(OWNER, TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.balance_of(RECIPIENT, TOKEN_ID), VALUE); } #[test] -fn test_transfer_from_doesnt_consume_infinite_allowance() { +fn test_transfer_from_with_infinite_allowance_does_not_decrease() { let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, BoundedInt::max()); + let contract_address = test_address(); - testing::set_caller_address(SPENDER()); - state.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + state._approve(OWNER, SPENDER, TOKEN_ID, Bounded::MAX); + assert_eq!(state.allowance(OWNER, SPENDER, TOKEN_ID), Bounded::MAX); - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(allowance, BoundedInt::max()); -} - -#[test] -#[should_panic(expected: ('ERC6909: insufficient allowance',))] -fn test_transfer_from_greater_than_allowance() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, VALUE); + start_cheat_caller_address(contract_address, SPENDER); + state.transfer_from(OWNER, RECIPIENT, TOKEN_ID, VALUE); - testing::set_caller_address(SPENDER()); - let allowance_plus_one = VALUE + 1; - state.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, allowance_plus_one); + assert_eq!(state.allowance(OWNER, SPENDER, TOKEN_ID), Bounded::MAX); + assert_eq!(state.balance_of(OWNER, TOKEN_ID), SUPPLY - VALUE); + assert_eq!(state.balance_of(RECIPIENT, TOKEN_ID), VALUE); } #[test] -#[should_panic(expected: ('ERC6909: transfer to 0',))] -fn test_transfer_from_to_zero_address() { +fn test_transfer_from_operator_bypass_allowance() { let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, VALUE); + let contract_address = test_address(); - testing::set_caller_address(SPENDER()); - state.transfer_from(OWNER(), ZERO(), TOKEN_ID, VALUE); -} + state._set_operator(OWNER, SPENDER, true); -// This does not check `_spend_allowance` since the owner (the zero address) -// is the sender, see `_spend_allowance` in erc6909.cairo -#[test] -#[should_panic(expected: ('ERC6909: transfer from 0',))] -fn test_transfer_from_from_zero_address() { - let mut state = setup(); - state.transfer_from(ZERO(), RECIPIENT(), TOKEN_ID, VALUE); -} + start_cheat_caller_address(contract_address, SPENDER); + let mut spy = spy_events(); -#[test] -#[should_panic(expected: ('ERC6909: insufficient allowance',))] -fn test_transfer_no_allowance() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, VALUE); + let ok = state.transfer_from(OWNER, RECIPIENT, TOKEN_ID, VALUE); + assert!(ok); - testing::set_caller_address(RECIPIENT()); - state.transfer_from(OWNER(), ZERO(), TOKEN_ID, VALUE); + spy.assert_only_event_transfer(contract_address, SPENDER, OWNER, RECIPIENT, TOKEN_ID, VALUE); + assert_eq!(state.allowance(OWNER, SPENDER, TOKEN_ID), 0); } #[test] -fn test_transferFrom() { +#[should_panic(expected: 'ERC6909: insufficient allowance')] +fn test_transfer_from_insufficient_allowance() { let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, VALUE); - utils::drop_event(ZERO()); + let lesser = VALUE - 1; + state._approve(OWNER, SPENDER, TOKEN_ID, lesser); - testing::set_caller_address(SPENDER()); - assert!(state.transferFrom(OWNER(), RECIPIENT(), TOKEN_ID, VALUE)); - - assert_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, 0); - assert_only_event_transfer(ZERO(), SPENDER(), OWNER(), RECIPIENT(), TOKEN_ID, VALUE); - - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(allowance, 0); - - assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), VALUE); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); - assert_eq!(allowance, 0); + start_cheat_caller_address(test_address(), SPENDER); + state.transfer_from(OWNER, RECIPIENT, TOKEN_ID, VALUE); } + #[test] -fn test_transferFrom_doesnt_consume_infinite_allowance() { +fn test_approve_external() { let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, BoundedInt::max()); + let contract_address = test_address(); - testing::set_caller_address(SPENDER()); - state.transferFrom(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + start_cheat_caller_address(contract_address, OWNER); + let mut spy = spy_events(); - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(allowance, BoundedInt::max()); -} + let ok = state.approve(SPENDER, TOKEN_ID, VALUE); + assert!(ok); -#[test] -#[should_panic(expected: ('ERC6909: insufficient allowance',))] -fn test_transferFrom_greater_than_allowance() { - let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, VALUE); - - testing::set_caller_address(SPENDER()); - let allowance_plus_one = VALUE + 1; - state.transferFrom(OWNER(), RECIPIENT(), TOKEN_ID, allowance_plus_one); + spy.assert_only_event_approval(contract_address, OWNER, SPENDER, TOKEN_ID, VALUE); + assert_eq!(state.allowance(OWNER, SPENDER, TOKEN_ID), VALUE); } #[test] -#[should_panic(expected: ('ERC6909: transfer to 0',))] -fn test_transferFrom_to_zero_address() { +#[should_panic(expected: 'ERC6909: invalid approver')] +fn test__approve_invalid_owner_zero() { let mut state = setup(); - testing::set_caller_address(OWNER()); - state.approve(SPENDER(), TOKEN_ID, VALUE); - - testing::set_caller_address(SPENDER()); - state.transferFrom(OWNER(), ZERO(), TOKEN_ID, VALUE); + state._approve(ZERO, SPENDER, TOKEN_ID, VALUE); } #[test] -fn test_self_transfer_from() { +#[should_panic(expected: 'ERC6909: invalid spender')] +fn test__approve_invalid_spender_zero() { let mut state = setup(); - testing::set_caller_address(OWNER()); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); - assert!(state.transfer_from(OWNER(), OWNER(), TOKEN_ID, 1)); - assert_only_event_transfer(ZERO(), OWNER(), OWNER(), OWNER(), TOKEN_ID, 1); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); + state._approve(OWNER, ZERO, TOKEN_ID, VALUE); } -// -// _spend_allowance -// - #[test] -fn test__spend_allowance_not_unlimited() { +fn test_set_operator_external() { let mut state = setup(); + let contract_address = test_address(); - state._approve(OWNER(), SPENDER(), TOKEN_ID, SUPPLY); - utils::drop_event(ZERO()); + start_cheat_caller_address(contract_address, OWNER); + let mut spy = spy_events(); - state._spend_allowance(OWNER(), SPENDER(), TOKEN_ID, VALUE); + let ok = state.set_operator(SPENDER, true); + assert!(ok); + spy.assert_only_event_operator_set(contract_address, OWNER, SPENDER, true); + assert!(state.is_operator(OWNER, SPENDER)); - assert_only_event_approval(ZERO(), OWNER(), SPENDER(), TOKEN_ID, SUPPLY - VALUE); - - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(allowance, SUPPLY - VALUE); + let ok2 = state.set_operator(SPENDER, false); + assert!(ok2); + spy.assert_only_event_operator_set(contract_address, OWNER, SPENDER, false); + assert!(!state.is_operator(OWNER, SPENDER)); } + #[test] -fn test__spend_allowance_unlimited() { +fn test__burn_reduces_balance_and_emits() { let mut state = setup(); - state._approve(OWNER(), SPENDER(), TOKEN_ID, BoundedInt::max()); + let contract_address = test_address(); - let max_minus_one: u256 = BoundedInt::max() - 1; - state._spend_allowance(OWNER(), SPENDER(), TOKEN_ID, max_minus_one); + start_cheat_caller_address(contract_address, OWNER); + let mut spy = spy_events(); - let allowance = state.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(allowance, BoundedInt::max()); -} + assert_eq!(state.balance_of(OWNER, TOKEN_ID), SUPPLY); + state._burn(OWNER, TOKEN_ID, VALUE); -// -// _mint -// + spy.assert_only_event_transfer(contract_address, OWNER, OWNER, ZERO, TOKEN_ID, VALUE); -#[test] -fn test__mint() { - let mut state = COMPONENT_STATE(); - state.mint(OWNER(), TOKEN_ID, VALUE); - - assert_only_event_transfer(ZERO(), ZERO(), ZERO(), OWNER(), TOKEN_ID, VALUE); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), VALUE); + assert_eq!(state.balance_of(OWNER, TOKEN_ID), SUPPLY - VALUE); } #[test] -#[should_panic(expected: ('ERC6909: mint to 0',))] -fn test__mint_to_zero() { - let mut state = COMPONENT_STATE(); - state.mint(ZERO(), TOKEN_ID, VALUE); -} - -// -// _burn -// - -#[test] -fn test__burn() { +#[should_panic(expected: 'ERC6909: invalid sender')] +fn test__burn_invalid_sender_zero() { let mut state = setup(); - state.burn(OWNER(), TOKEN_ID, VALUE); - - assert_only_event_transfer(ZERO(), ZERO(), OWNER(), ZERO(), TOKEN_ID, VALUE); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); + state._burn(ZERO, TOKEN_ID, VALUE); } #[test] -#[should_panic(expected: ('ERC6909: burn from 0',))] -fn test__burn_from_zero() { +#[should_panic(expected: 'ERC6909: insufficient balance')] +fn test__burn_insufficient_balance() { let mut state = setup(); - state.burn(ZERO(), TOKEN_ID, VALUE); + state._burn(OWNER, TOKEN_ID, SUPPLY + 1); } -// -// is_operator & set_operator -// #[test] -fn test_transfer_from_caller_is_operator() { - let mut state = setup(); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY); - assert_eq!(state.balance_of(RECIPIENT(), TOKEN_ID), 0); - assert_eq!(state.is_operator(OWNER(), OPERATOR()), false); +fn test_update_calls_before_and_after_update_hooks_on_transfer() { + let mut state = setup_with_hooks(); + let contract_address = test_address(); - testing::set_caller_address(OWNER()); - state.set_operator(OPERATOR(), true); - - assert_only_event_operator_set(ZERO(), OWNER(), OPERATOR(), true); - - testing::set_caller_address(OPERATOR()); - assert!(state.transfer_from(OWNER(), OPERATOR(), TOKEN_ID, VALUE)); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - VALUE); - assert_eq!(state.balance_of(OPERATOR(), TOKEN_ID), VALUE); - assert!(state.is_operator(OWNER(), OPERATOR())); -} + start_cheat_caller_address(contract_address, OWNER); + let mut spy = spy_events(); -#[test] -fn test_set_operator() { - let mut state = setup(); - assert_eq!(state.is_operator(OWNER(), OPERATOR()), false); + let amount = VALUE; + let id = TOKEN_ID; - testing::set_caller_address(OWNER()); - state.set_operator(OPERATOR(), true); + state.transfer(RECIPIENT, id, amount); - assert_only_event_operator_set(ZERO(), OWNER(), OPERATOR(), true); - assert!(state.is_operator(OWNER(), OPERATOR())); + spy.assert_event_before_update(contract_address, OWNER, RECIPIENT, id, amount); + spy.assert_event_after_update(contract_address, OWNER, RECIPIENT, id, amount); } -#[test] -fn test_set_operator_false() { - let mut state = setup(); - assert_eq!(state.is_operator(OWNER(), OPERATOR()), false); - testing::set_caller_address(OWNER()); - state.set_operator(OPERATOR(), true); - assert_only_event_operator_set(ZERO(), OWNER(), OPERATOR(), true); - assert!(state.is_operator(OWNER(), OPERATOR())); - - testing::set_caller_address(OWNER()); - state.set_operator(OPERATOR(), false); - assert_only_event_operator_set(ZERO(), OWNER(), OPERATOR(), false); - assert_eq!(state.is_operator(OWNER(), OPERATOR()), false); +fn assert_state_before_transfer( + sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256, total: u256, +) { + let state = COMPONENT_STATE(); + assert_eq!(state.balance_of(sender, id), total); + assert_eq!(state.balance_of(receiver, id), 0); + assert!(amount <= total); } -#[test] -fn test_operator_does_not_deduct_allowance() { - let mut state = setup(); - - testing::set_caller_address(OWNER()); - state.approve(OPERATOR(), TOKEN_ID, 1); - assert_eq!(state.allowance(OWNER(), OPERATOR(), TOKEN_ID), 1); - assert_event_approval(ZERO(), OWNER(), OPERATOR(), TOKEN_ID, 1); - - testing::set_caller_address(OWNER()); - state.set_operator(OPERATOR(), true); - assert!(state.is_operator(OWNER(), OPERATOR())); - assert_event_operator_set(ZERO(), OWNER(), OPERATOR(), true); +fn assert_state_after_transfer( + sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256, total_before: u256, +) { + let state = COMPONENT_STATE(); + assert_eq!(state.balance_of(sender, id), total_before - amount); + assert_eq!(state.balance_of(receiver, id), amount); +} - testing::set_caller_address(OPERATOR()); - assert!(state.transfer_from(OWNER(), OPERATOR(), TOKEN_ID, 1)); - assert_only_event_transfer(ZERO(), OPERATOR(), OWNER(), OPERATOR(), TOKEN_ID, 1); +#[generate_trait] +impl ERC6909HooksSpyHelpersImpl of ERC6909HooksSpyHelpers { + fn assert_event_before_update( + ref self: EventSpy, + contract: ContractAddress, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + let expected = ExpectedEvent::new() + .key(selector!("BeforeUpdate")) + .data(from) + .data(recipient) + .data(id) + .data(amount); + self.assert_emitted_single(contract, expected); + } - assert_eq!(state.allowance(OWNER(), OPERATOR(), TOKEN_ID), 1); - assert_eq!(state.balance_of(OWNER(), TOKEN_ID), SUPPLY - 1); - assert_eq!(state.balance_of(OPERATOR(), TOKEN_ID), 1); + fn assert_event_after_update( + ref self: EventSpy, + contract: ContractAddress, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + let expected = ExpectedEvent::new() + .key(selector!("AfterUpdate")) + .data(from) + .data(recipient) + .data(id) + .data(amount); + self.assert_emitted_single(contract, expected); + } } -#[test] -fn test_self_set_operator() { - let mut state = setup(); - assert_eq!(state.is_operator(OWNER(), OWNER()), false); - testing::set_caller_address(OWNER()); - state.set_operator(OWNER(), true); - assert!(state.is_operator(OWNER(), OWNER())); -} diff --git a/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo b/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo index 770e84cdd..157c88e0f 100644 --- a/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo +++ b/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo @@ -1,105 +1,80 @@ -use core::integer::BoundedInt; -use core::num::traits::Zero; -use openzeppelin::tests::mocks::erc6909_content_uri_mocks::DualCaseERC6909ContentURIMock; -use openzeppelin::tests::utils::constants::{ - OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO, BASE_URI, BASE_URI_2 -}; -use openzeppelin::tests::utils; -use openzeppelin::token::erc6909::ERC6909Component::InternalImpl as InternalERC6909Impl; -use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent::{ +use openzeppelin_interfaces::erc6909::IERC6909_CONTENT_URI_ID; +use openzeppelin_interfaces::introspection::ISRC5_ID; +use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::erc6909::ERC6909ContentURIMock; +use crate::erc6909::extensions::erc6909_content_uri::ERC6909ContentURIComponent; +use crate::erc6909::extensions::erc6909_content_uri::ERC6909ContentURIComponent::{ ERC6909ContentURIImpl, InternalImpl, }; -use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; -use openzeppelin::utils::serde::SerializedAppend; -use starknet::ContractAddress; -use starknet::contract_address_const; -use starknet::storage::{StorageMapMemberAccessTrait, StorageMemberAccessTrait}; -use starknet::testing; - -use super::common::{ - assert_event_approval, assert_only_event_approval, assert_only_event_transfer, - assert_only_event_operator_set, assert_event_operator_set -}; -// -// Setup -// -const TOKEN_ID: u256 = 420; +fn CONTRACT_URI() -> ByteArray { + "ipfs://erc6909/" +} + +const SAMPLE_ID: u256 = 1234; + type ComponentState = - ERC6909ContentURIComponent::ComponentState; + ERC6909ContentURIComponent::ComponentState; -fn CONTRACT_STATE() -> DualCaseERC6909ContentURIMock::ContractState { - DualCaseERC6909ContentURIMock::contract_state_for_testing() +fn CONTRACT_STATE() -> ERC6909ContentURIMock::ContractState { + ERC6909ContentURIMock::contract_state_for_testing() } fn COMPONENT_STATE() -> ComponentState { ERC6909ContentURIComponent::component_state_for_testing() } -fn setup() -> (ComponentState, DualCaseERC6909ContentURIMock::ContractState) { + +#[test] +fn test_initializer_registers_interface_and_sets_uri() { let mut state = COMPONENT_STATE(); let mut mock_state = CONTRACT_STATE(); - mock_state.erc6909.mint(OWNER(), TOKEN_ID, SUPPLY); - utils::drop_event(ZERO()); - (state, mock_state) + + let uri = CONTRACT_URI(); + state.initializer(uri); + + let supports_content_uri = mock_state.supports_interface(IERC6909_CONTENT_URI_ID); + assert!(supports_content_uri); + + let supports_isrc5 = mock_state.supports_interface(ISRC5_ID); + assert!(supports_isrc5); + + assert_eq!(state.contract_uri(), uri); } -// -// Getters -// #[test] -fn test_unset_content_uri() { - let (mut state, _) = setup(); - let mut uri = state.contract_uri(); - assert_eq!(uri, ""); +fn test_contract_uri_default_is_empty() { + let state = COMPONENT_STATE(); + let empty: ByteArray = ""; + assert_eq!(state.contract_uri(), empty); } #[test] -fn test_unset_token_uri() { - let (mut state, _) = setup(); - let uri = state.token_uri(TOKEN_ID); - assert_eq!(uri, ""); +fn test_contract_uri_after_initializer_returns_set_value() { + let mut state = COMPONENT_STATE(); + let uri = CONTRACT_URI(); + state.initializer(uri); + + assert_eq!(state.contract_uri(), uri); } -// -// internal setters -// #[test] -fn test_set_contract_uri() { - let (mut state, _) = setup(); - testing::set_caller_address(OWNER()); - state.initializer(BASE_URI()); - let uri = state.contract_uri(); - assert_eq!(uri, BASE_URI()); -} +fn test_token_uri_concatenates_contract_uri_and_id() { + let mut state = COMPONENT_STATE(); + let uri = CONTRACT_URI(); + state.initializer(uri); -#[test] -fn test_set_token_uri() { - let (mut state, _) = setup(); - testing::set_caller_address(OWNER()); - state.initializer(BASE_URI()); - let uri = state.token_uri(TOKEN_ID); - let expected = format!("{}{}", BASE_URI(), TOKEN_ID); - assert_eq!(uri, expected); + let expected = format!("{}{}", uri, SAMPLE_ID); + assert_eq!(state.token_uri(SAMPLE_ID), expected); } -// Updates the URI once set #[test] -fn test_update_token_uri() { - let (mut state, _) = setup(); - testing::set_caller_address(OWNER()); - state.initializer(BASE_URI()); - let mut uri = state.token_uri(TOKEN_ID); - let mut expected = format!("{}{}", BASE_URI(), TOKEN_ID); - assert_eq!(uri, expected); - - testing::set_caller_address(OWNER()); - state.initializer(BASE_URI_2()); - let mut uri = state.token_uri(TOKEN_ID); - let expected = format!("{}{}", BASE_URI_2(), TOKEN_ID); - assert_eq!(uri, expected); +fn test_token_uri_when_contract_uri_not_set_is_empty() { + let state = COMPONENT_STATE(); + let empty: ByteArray = ""; + assert_eq!(state.token_uri(SAMPLE_ID), empty); } diff --git a/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo b/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo index 5bbcc101e..541c5442b 100644 --- a/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo +++ b/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo @@ -1,108 +1,109 @@ -use core::integer::BoundedInt; -use core::num::traits::Zero; -use openzeppelin::tests::mocks::erc6909_metadata_mocks::DualCaseERC6909MetadataMock; -use openzeppelin::tests::utils::constants::{OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO}; -use openzeppelin::tests::utils; -use openzeppelin::token::erc6909::ERC6909Component::InternalImpl as InternalERC6909Impl; -use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent::{ - ERC6909MetadataImpl, InternalImpl, -}; -use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; -use openzeppelin::utils::serde::SerializedAppend; +use openzeppelin_interfaces::erc6909::IERC6909_METADATA_ID; +use openzeppelin_interfaces::introspection::ISRC5_ID; +use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::erc6909::ERC6909MetadataMock; +use openzeppelin_testing::constants::{DECIMALS, NAME, OWNER, SYMBOL, TOKEN_ID, ZERO}; use starknet::ContractAddress; -use starknet::contract_address_const; -use starknet::storage::{StorageMapMemberAccessTrait, StorageMemberAccessTrait}; -use starknet::testing; - -use super::common::{ - assert_event_approval, assert_only_event_approval, assert_only_event_transfer, - assert_only_event_operator_set, assert_event_operator_set +use crate::erc6909::extensions::erc6909_metadata::ERC6909MetadataComponent; +use crate::erc6909::extensions::erc6909_metadata::ERC6909MetadataComponent::{ + ERC6909MetadataImpl, InternalImpl, }; -// -// Setup -// +type ComponentState = ERC6909MetadataComponent::ComponentState; -const TOKEN_ID: u256 = 420; - -type ComponentState = - ERC6909MetadataComponent::ComponentState; - -fn CONTRACT_STATE() -> DualCaseERC6909MetadataMock::ContractState { - DualCaseERC6909MetadataMock::contract_state_for_testing() +fn CONTRACT_STATE() -> ERC6909MetadataMock::ContractState { + ERC6909MetadataMock::contract_state_for_testing() } fn COMPONENT_STATE() -> ComponentState { ERC6909MetadataComponent::component_state_for_testing() } -fn setup() -> (ComponentState, DualCaseERC6909MetadataMock::ContractState) { - let mut state = COMPONENT_STATE(); - let mut mock_state = CONTRACT_STATE(); - mock_state.erc6909.mint(OWNER(), TOKEN_ID, SUPPLY); - utils::drop_event(ZERO()); - (state, mock_state) +fn NAME_2() -> ByteArray { + "ALT_NAME" } +fn SYMBOL_2() -> ByteArray { + "ALT_SYMBOL" +} +const DECIMALS_2: u8 = 6; -// Getters - -// The mocks use this metadata -// Check that minting a token updates the metadata using the ERC6909Hooks #[test] -fn test_name() { - let (mut state, _) = setup(); - let mut name = state.ERC6909Metadata_name.read(TOKEN_ID); - assert_eq!(name, "MyERC6909Token"); +fn test_initializer_registers_interface() { + let mut state = COMPONENT_STATE(); + let mock_state = CONTRACT_STATE(); + + state.initializer(); + + assert!(mock_state.supports_interface(IERC6909_METADATA_ID)); + assert!(mock_state.supports_interface(ISRC5_ID)); } #[test] -fn test_symbol() { - let (mut state, _) = setup(); - let mut symbol = state.ERC6909Metadata_symbol.read(TOKEN_ID); - assert_eq!(symbol, "MET"); +fn test_default_getters_are_empty_or_zero() { + let state = COMPONENT_STATE(); + + let empty: ByteArray = ""; + assert_eq!(state.name(TOKEN_ID), empty); + assert_eq!(state.symbol(TOKEN_ID), empty); + assert_eq!(state.decimals(TOKEN_ID), 0); } #[test] -fn test_decimals() { - let (mut state, _) = setup(); - let mut decimals = state.ERC6909Metadata_decimals.read(TOKEN_ID); - assert_eq!(decimals, 18); +fn test__set_token_metadata_sets_values() { + let mut state = COMPONENT_STATE(); + + state._set_token_metadata(TOKEN_ID, NAME(), SYMBOL(), DECIMALS); + + assert_eq!(state.name(TOKEN_ID), NAME()); + assert_eq!(state.symbol(TOKEN_ID), SYMBOL()); + assert_eq!(state.decimals(TOKEN_ID), DECIMALS); } -// internal setters +#[test] +fn test__update_token_metadata_on_mint_sets_when_absent() { + let mut state = COMPONENT_STATE(); + + state._update_token_metadata(ZERO, TOKEN_ID, NAME(), SYMBOL(), DECIMALS); + + assert_eq!(state.name(TOKEN_ID), NAME()); + assert_eq!(state.symbol(TOKEN_ID), SYMBOL()); + assert_eq!(state.decimals(TOKEN_ID), DECIMALS); +} #[test] -fn test_set_name() { - let (_, mut mock_state) = setup(); - testing::set_caller_address(OWNER()); - mock_state.erc6909_metadata._set_token_name(TOKEN_ID, "some token"); - let mut name = mock_state.name(TOKEN_ID); - assert_eq!(name, "some token"); - - let mut name = mock_state.name(TOKEN_ID + 69); - assert_eq!(name, ""); +fn test__update_token_metadata_on_mint_does_not_overwrite_if_exists() { + let mut state = COMPONENT_STATE(); + + state._set_token_metadata(TOKEN_ID, NAME(), SYMBOL(), DECIMALS); + state._update_token_metadata(ZERO, TOKEN_ID, NAME_2(), SYMBOL_2(), DECIMALS_2); + + assert_eq!(state.name(TOKEN_ID), NAME()); + assert_eq!(state.symbol(TOKEN_ID), SYMBOL()); + assert_eq!(state.decimals(TOKEN_ID), DECIMALS); } #[test] -fn test_set_symbol() { - let (_, mut mock_state) = setup(); - testing::set_caller_address(OWNER()); - mock_state.erc6909_metadata._set_token_symbol(TOKEN_ID, "some symbol"); - let mut symbol = mock_state.symbol(TOKEN_ID); - assert_eq!(symbol, "some symbol"); - - let mut symbol = mock_state.symbol(TOKEN_ID + 69); - assert_eq!(symbol, ""); +fn test__update_token_metadata_on_transfer_does_nothing_when_absent() { + let mut state = COMPONENT_STATE(); + let sender: ContractAddress = OWNER; + + state._update_token_metadata(sender, TOKEN_ID, NAME(), SYMBOL(), DECIMALS); + + let empty: ByteArray = ""; + assert_eq!(state.name(TOKEN_ID), empty); + assert_eq!(state.symbol(TOKEN_ID), empty); + assert_eq!(state.decimals(TOKEN_ID), 0); } #[test] -fn test_set_decimals() { - let (_, mut mock_state) = setup(); - testing::set_caller_address(OWNER()); - mock_state.erc6909_metadata._set_token_decimals(TOKEN_ID, 18); - let mut decimals = mock_state.decimals(TOKEN_ID); - assert_eq!(decimals, 18); - - let mut decimals = mock_state.decimals(TOKEN_ID + 69); - assert_eq!(decimals, 0); +fn test__update_token_metadata_on_transfer_does_not_overwrite_existing() { + let mut state = COMPONENT_STATE(); + let sender: ContractAddress = OWNER; + + state._set_token_metadata(TOKEN_ID, NAME(), SYMBOL(), DECIMALS); + state._update_token_metadata(sender, TOKEN_ID, NAME_2(), SYMBOL_2(), DECIMALS_2); + + assert_eq!(state.name(TOKEN_ID), NAME()); + assert_eq!(state.symbol(TOKEN_ID), SYMBOL()); + assert_eq!(state.decimals(TOKEN_ID), DECIMALS); } diff --git a/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo b/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo index 4bdf62dcf..6743254eb 100644 --- a/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo +++ b/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo @@ -1,164 +1,74 @@ -use core::integer::BoundedInt; -use core::num::traits::Zero; -use openzeppelin::tests::mocks::erc6909_token_supply_mocks::DualCaseERC6909TokenSupplyMock; -use openzeppelin::tests::utils::constants::{OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO}; -use openzeppelin::tests::utils; -use openzeppelin::token::erc6909::ERC6909Component::{ - InternalImpl as InternalERC6909Impl, ERC6909Impl -}; -use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent::{ - ERC6909TokenSupplyImpl, InternalImpl, -}; -use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; -use openzeppelin::utils::serde::SerializedAppend; +use openzeppelin_interfaces::erc6909::IERC6909_TOKEN_SUPPLY_ID; +use openzeppelin_interfaces::introspection::ISRC5_ID; +use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::erc6909::ERC6909TokenSupplyMock; +use openzeppelin_testing::constants::{OWNER, RECIPIENT, TOKEN_ID, VALUE, ZERO}; use starknet::ContractAddress; -use starknet::contract_address_const; -use starknet::storage::{StorageMapMemberAccessTrait, StorageMemberAccessTrait}; -use starknet::testing; - -use super::common::{ - assert_event_approval, assert_only_event_approval, assert_only_event_transfer, - assert_only_event_operator_set, assert_event_operator_set +use crate::erc6909::extensions::erc6909_token_supply::ERC6909TokenSupplyComponent; +use crate::erc6909::extensions::erc6909_token_supply::ERC6909TokenSupplyComponent::{ + ERC6909TokenSupplyImpl, InternalImpl, }; -// -// Setup -// - -const TOKEN_ID: u256 = 420; - type ComponentState = - ERC6909TokenSupplyComponent::ComponentState; + ERC6909TokenSupplyComponent::ComponentState; -fn CONTRACT_STATE() -> DualCaseERC6909TokenSupplyMock::ContractState { - DualCaseERC6909TokenSupplyMock::contract_state_for_testing() +fn CONTRACT_STATE() -> ERC6909TokenSupplyMock::ContractState { + ERC6909TokenSupplyMock::contract_state_for_testing() } fn COMPONENT_STATE() -> ComponentState { ERC6909TokenSupplyComponent::component_state_for_testing() } -fn setup() -> (ComponentState, DualCaseERC6909TokenSupplyMock::ContractState) { - let mut state = COMPONENT_STATE(); - let mut mock_state = CONTRACT_STATE(); - mock_state.erc6909.mint(OWNER(), TOKEN_ID, SUPPLY); - utils::drop_event(ZERO()); - (state, mock_state) -} - -// -// Getters -// - #[test] -fn test__state_total_supply() { - let (mut state, _) = setup(); - let mut id_supply = state.ERC6909TokenSupply_total_supply.read(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); -} - -#[test] -fn test__state_no_total_supply() { - let (mut state, _) = setup(); - let mut id_supply = state.ERC6909TokenSupply_total_supply.read(TOKEN_ID + 69); - assert_eq!(id_supply, 0); -} - +fn test_initializer_registers_interface() { + let mut state = COMPONENT_STATE(); + let mock_state = CONTRACT_STATE(); -#[test] -fn test_total_supply() { - let (mut state, _) = setup(); - let mut id_supply = state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); -} + state.initializer(); -#[test] -fn test_no_total_supply() { - let (mut state, _) = setup(); - let mut id_supply = state.total_supply(TOKEN_ID + 69); - assert_eq!(id_supply, 0); + assert!(mock_state.supports_interface(IERC6909_TOKEN_SUPPLY_ID)); + assert!(mock_state.supports_interface(ISRC5_ID)); } #[test] -fn test_total_supply_contract() { - let (_, mut mock_state) = setup(); - let mut id_supply = mock_state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); +fn test_total_supply_default_zero() { + let state = COMPONENT_STATE(); + assert_eq!(state.total_supply(TOKEN_ID), 0); } -// -// mint & burn -// #[test] -fn test_mint_increase_supply() { - let (_, mut mock_state) = setup(); - let mut id_supply = mock_state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); - - let new_token_id = TOKEN_ID + 69; +fn test__update_token_supply_increments_on_mint() { + let mut state = COMPONENT_STATE(); + let before = state.total_supply(TOKEN_ID); - testing::set_caller_address(OWNER()); - mock_state.erc6909.mint(OWNER(), new_token_id, SUPPLY * 2); + state._update_token_supply(ZERO, RECIPIENT, TOKEN_ID, VALUE); - let mut old_token_id_supply = mock_state.total_supply(TOKEN_ID); - let mut new_token_id_supply = mock_state.total_supply(new_token_id); - assert_eq!(old_token_id_supply, SUPPLY); - assert_eq!(new_token_id_supply, SUPPLY * 2); + assert_eq!(state.total_supply(TOKEN_ID), before + VALUE); } #[test] -fn test_burn_decrease_supply() { - let (_, mut mock_state) = setup(); - let mut id_supply = mock_state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); - - let new_token_id = TOKEN_ID + 69; - - testing::set_caller_address(OWNER()); - mock_state.erc6909.mint(OWNER(), new_token_id, SUPPLY * 2); +fn test__update_token_supply_decrements_on_burn() { + let mut state = COMPONENT_STATE(); - let mut new_token_id_supply = mock_state.total_supply(new_token_id); - assert_eq!(new_token_id_supply, SUPPLY * 2); + state._update_token_supply(ZERO, RECIPIENT, TOKEN_ID, VALUE + 1); + let mid = state.total_supply(TOKEN_ID); - testing::set_caller_address(OWNER()); - mock_state.erc6909.burn(OWNER(), new_token_id, SUPPLY * 2); + state._update_token_supply(OWNER, ZERO, TOKEN_ID, 1); - let mut new_token_id_supply = mock_state.total_supply(new_token_id); - assert_eq!(new_token_id_supply, 0); + assert_eq!(state.total_supply(TOKEN_ID), mid - 1); } -// transfer & transferFrom #[test] -fn test_transfers_dont_change_supply() { - let (_, mut mock_state) = setup(); - let mut id_supply = mock_state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); - - testing::set_caller_address(OWNER()); - mock_state.transfer(RECIPIENT(), TOKEN_ID, SUPPLY); - - let mut id_supply = mock_state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); +fn test__update_token_supply_no_change_on_transfer() { + let mut state = COMPONENT_STATE(); - testing::set_caller_address(RECIPIENT()); - mock_state.transfer(OWNER(), TOKEN_ID, SUPPLY / 2); + state._update_token_supply(ZERO, RECIPIENT, TOKEN_ID, VALUE); + let before = state.total_supply(TOKEN_ID); - let mut id_supply = mock_state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); -} + let sender: ContractAddress = OWNER; + let receiver: ContractAddress = RECIPIENT; + state._update_token_supply(sender, receiver, TOKEN_ID, VALUE); -// transfer & transferFrom -#[test] -fn test_transfer_from_doesnt_change_supply() { - let (_, mut mock_state) = setup(); - let mut id_supply = mock_state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); - - testing::set_caller_address(OWNER()); - mock_state.approve(SPENDER(), TOKEN_ID, SUPPLY); - testing::set_caller_address(SPENDER()); - mock_state.transfer_from(OWNER(), SPENDER(), TOKEN_ID, SUPPLY); - - let mut id_supply = mock_state.total_supply(TOKEN_ID); - assert_eq!(id_supply, SUPPLY); + assert_eq!(state.total_supply(TOKEN_ID), before); } From 15709fc9f6cd4601f65166796649dd068d12dba0 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Wed, 12 Nov 2025 16:03:27 +0100 Subject: [PATCH 41/44] chore(tests): remove older ERC6909 test files --- packages/token/src/tests/erc6909/common.cairo | 90 ------ .../src/tests/erc6909/test_dual6909.cairo | 260 ------------------ 2 files changed, 350 deletions(-) delete mode 100644 packages/token/src/tests/erc6909/common.cairo delete mode 100644 packages/token/src/tests/erc6909/test_dual6909.cairo diff --git a/packages/token/src/tests/erc6909/common.cairo b/packages/token/src/tests/erc6909/common.cairo deleted file mode 100644 index 0c8fdba98..000000000 --- a/packages/token/src/tests/erc6909/common.cairo +++ /dev/null @@ -1,90 +0,0 @@ -use openzeppelin::tests::utils; -use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet, InternalImpl}; -use openzeppelin::token::erc6909::ERC6909Component; -use openzeppelin::utils::serde::SerializedAppend; -use starknet::ContractAddress; - -// Approval -pub(crate) fn assert_event_approval( - contract: ContractAddress, - owner: ContractAddress, - spender: ContractAddress, - id: u256, - amount: u256 -) { - let event = utils::pop_log::(contract).unwrap(); - let expected = ERC6909Component::Event::Approval(Approval { owner, spender, id, amount }); - assert!(event == expected); - let mut indexed_keys = array![]; - indexed_keys.append_serde(selector!("Approval")); - indexed_keys.append_serde(owner); - indexed_keys.append_serde(spender); - indexed_keys.append_serde(id); - utils::assert_indexed_keys(event, indexed_keys.span()) -} - -pub(crate) fn assert_only_event_approval( - contract: ContractAddress, - owner: ContractAddress, - spender: ContractAddress, - id: u256, - amount: u256 -) { - assert_event_approval(contract, owner, spender, id, amount); - utils::assert_no_events_left(contract); -} - -// Transfer -pub(crate) fn assert_event_transfer( - contract: ContractAddress, - caller: ContractAddress, - sender: ContractAddress, - receiver: ContractAddress, - id: u256, - amount: u256 -) { - let event = utils::pop_log::(contract).unwrap(); - let expected = ERC6909Component::Event::Transfer( - Transfer { caller, sender, receiver, id, amount } - ); - assert!(event == expected); - let mut indexed_keys = array![]; - indexed_keys.append_serde(selector!("Transfer")); - indexed_keys.append_serde(sender); - indexed_keys.append_serde(receiver); - indexed_keys.append_serde(id); - utils::assert_indexed_keys(event, indexed_keys.span()); -} - -pub(crate) fn assert_only_event_transfer( - contract: ContractAddress, - caller: ContractAddress, - sender: ContractAddress, - receiver: ContractAddress, - id: u256, - amount: u256 -) { - assert_event_transfer(contract, caller, sender, receiver, id, amount); - utils::assert_no_events_left(contract); -} - -// OperatorSet -pub(crate) fn assert_only_event_operator_set( - contract: ContractAddress, owner: ContractAddress, spender: ContractAddress, approved: bool, -) { - assert_event_operator_set(contract, owner, spender, approved); - utils::assert_no_events_left(contract); -} - -pub(crate) fn assert_event_operator_set( - contract: ContractAddress, owner: ContractAddress, spender: ContractAddress, approved: bool -) { - let event = utils::pop_log::(contract).unwrap(); - let expected = ERC6909Component::Event::OperatorSet(OperatorSet { owner, spender, approved }); - assert!(event == expected); - let mut indexed_keys = array![]; - indexed_keys.append_serde(selector!("OperatorSet")); - indexed_keys.append_serde(owner); - indexed_keys.append_serde(spender); - utils::assert_indexed_keys(event, indexed_keys.span()) -} diff --git a/packages/token/src/tests/erc6909/test_dual6909.cairo b/packages/token/src/tests/erc6909/test_dual6909.cairo deleted file mode 100644 index 82b4d38cc..000000000 --- a/packages/token/src/tests/erc6909/test_dual6909.cairo +++ /dev/null @@ -1,260 +0,0 @@ -use openzeppelin::tests::mocks::erc6909_mocks::{CamelERC6909Mock, SnakeERC6909Mock}; -use openzeppelin::tests::mocks::erc6909_mocks::{CamelERC6909Panic, SnakeERC6909Panic}; -use openzeppelin::tests::mocks::non_implementing_mock::NonImplementingMock; -use openzeppelin::tests::utils::constants::{ - OWNER, RECIPIENT, SPENDER, OPERATOR, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE -}; -use openzeppelin::tests::utils; -use openzeppelin::token::erc6909::dual6909::{DualCaseERC6909, DualCaseERC6909Trait}; -use openzeppelin::token::erc6909::interface::{ - IERC6909CamelDispatcher, IERC6909CamelDispatcherTrait -}; -use openzeppelin::token::erc6909::interface::{IERC6909Dispatcher, IERC6909DispatcherTrait}; -use openzeppelin::utils::serde::SerializedAppend; -use starknet::testing::set_contract_address; - -// -// Setup -// - -pub const TOKEN_ID: u256 = 420; - -fn setup_snake() -> (DualCaseERC6909, IERC6909Dispatcher) { - let mut calldata = array![]; - calldata.append_serde(OWNER()); - calldata.append_serde(TOKEN_ID); - calldata.append_serde(SUPPLY); - let target = utils::deploy(SnakeERC6909Mock::TEST_CLASS_HASH, calldata); - (DualCaseERC6909 { contract_address: target }, IERC6909Dispatcher { contract_address: target }) -} - -fn setup_camel() -> (DualCaseERC6909, IERC6909CamelDispatcher) { - let mut calldata = array![]; - calldata.append_serde(OWNER()); - calldata.append_serde(TOKEN_ID); - calldata.append_serde(SUPPLY); - let target = utils::deploy(CamelERC6909Mock::TEST_CLASS_HASH, calldata); - ( - DualCaseERC6909 { contract_address: target }, - IERC6909CamelDispatcher { contract_address: target } - ) -} - -fn setup_non_erc6909() -> DualCaseERC6909 { - let calldata = array![]; - let target = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, calldata); - DualCaseERC6909 { contract_address: target } -} - -fn setup_erc6909_panic() -> (DualCaseERC6909, DualCaseERC6909) { - let snake_target = utils::deploy(SnakeERC6909Panic::TEST_CLASS_HASH, array![]); - let camel_target = utils::deploy(CamelERC6909Panic::TEST_CLASS_HASH, array![]); - ( - DualCaseERC6909 { contract_address: snake_target }, - DualCaseERC6909 { contract_address: camel_target } - ) -} - -// -// Case agnostic methods -// - -#[test] -fn test_dual_transfer() { - let (snake_dispatcher, snake_target) = setup_snake(); - set_contract_address(OWNER()); - assert!(snake_dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE)); - assert_eq!(snake_target.balance_of(RECIPIENT(), TOKEN_ID), VALUE); - - let (camel_dispatcher, camel_target) = setup_camel(); - set_contract_address(OWNER()); - assert!(camel_dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE)); - assert_eq!(camel_target.balanceOf(RECIPIENT(), TOKEN_ID), VALUE); -} - -#[test] -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_transfer() { - let dispatcher = setup_non_erc6909(); - dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_transfer_exists_and_panics() { - let (dispatcher, _) = setup_erc6909_panic(); - dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE); -} - - -#[test] -fn test_dual_approve() { - let (snake_dispatcher, snake_target) = setup_snake(); - set_contract_address(OWNER()); - assert!(snake_dispatcher.approve(SPENDER(), TOKEN_ID, VALUE)); - - let snake_allowance = snake_target.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(snake_allowance, VALUE); - - let (camel_dispatcher, camel_target) = setup_camel(); - set_contract_address(OWNER()); - assert!(camel_dispatcher.approve(SPENDER(), TOKEN_ID, VALUE)); - - let camel_allowance = camel_target.allowance(OWNER(), SPENDER(), TOKEN_ID); - assert_eq!(camel_allowance, VALUE); -} - -#[test] -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_approve() { - let dispatcher = setup_non_erc6909(); - dispatcher.approve(SPENDER(), TOKEN_ID, VALUE); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_approve_exists_and_panics() { - let (dispatcher, _) = setup_erc6909_panic(); - dispatcher.approve(SPENDER(), TOKEN_ID, VALUE); -} - -// -// snake_case target -// - -#[test] -fn test_dual_balance_of() { - let (dispatcher, _) = setup_snake(); - assert_eq!(dispatcher.balance_of(OWNER(), TOKEN_ID), SUPPLY); -} - -#[test] -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_balance_of() { - let dispatcher = setup_non_erc6909(); - dispatcher.balance_of(OWNER(), TOKEN_ID); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_balance_of_exists_and_panics() { - let (dispatcher, _) = setup_erc6909_panic(); - dispatcher.balance_of(OWNER(), TOKEN_ID); -} - -#[test] -fn test_dual_transfer_from() { - let (dispatcher, target) = setup_snake(); - set_contract_address(OWNER()); - target.approve(OPERATOR(), TOKEN_ID, VALUE); - - set_contract_address(OPERATOR()); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); - assert_eq!(target.balance_of(RECIPIENT(), TOKEN_ID), VALUE); -} - -#[test] -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_transfer_from() { - let dispatcher = setup_non_erc6909(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_transfer_from_exists_and_panics() { - let (dispatcher, _) = setup_erc6909_panic(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); -} - -// set_operator -#[test] -fn test_dual_set_operator() { - let (dispatcher, target) = setup_snake(); - set_contract_address(OWNER()); - target.set_operator(OPERATOR(), true); - - set_contract_address(OPERATOR()); - assert!(dispatcher.is_operator(OWNER(), OPERATOR())); -} - -#[test] -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_set_operator() { - let dispatcher = setup_non_erc6909(); - dispatcher.set_operator(OPERATOR(), true); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_set_operator_exists_and_panics() { - let (dispatcher, _) = setup_erc6909_panic(); - dispatcher.set_operator(OPERATOR(), true); -} - -// is_operator -#[test] -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_is_operator() { - let dispatcher = setup_non_erc6909(); - dispatcher.is_operator(OWNER(), OPERATOR()); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_is_operator_exists_and_panics() { - let (dispatcher, _) = setup_erc6909_panic(); - dispatcher.is_operator(OWNER(), OPERATOR()); -} - -// -// camelCase target -// - -#[test] -fn test_dual_balanceOf() { - let (dispatcher, _) = setup_camel(); - assert_eq!(dispatcher.balance_of(OWNER(), TOKEN_ID), SUPPLY); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_balanceOf_exists_and_panics() { - let (_, dispatcher) = setup_erc6909_panic(); - dispatcher.balance_of(OWNER(), TOKEN_ID); -} - -#[test] -fn test_dual_transferFrom() { - let (dispatcher, target) = setup_camel(); - set_contract_address(OWNER()); - target.approve(OPERATOR(), TOKEN_ID, VALUE); - - set_contract_address(OPERATOR()); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); - assert_eq!(target.balanceOf(RECIPIENT(), TOKEN_ID), VALUE); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_transferFrom_exists_and_panics() { - let (_, dispatcher) = setup_erc6909_panic(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); -} - -#[test] -fn test_dual_setOperator() { - let (dispatcher, target) = setup_camel(); - set_contract_address(OWNER()); - target.setOperator(OPERATOR(), true); - - set_contract_address(OPERATOR()); - assert!(dispatcher.is_operator(OWNER(), OPERATOR())); -} - -#[test] -#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] -fn test_dual_setOperator_exists_and_panics() { - let (_, dispatcher) = setup_erc6909_panic(); - dispatcher.set_operator(OPERATOR(), true); -} From 3d700597984583c16c2a7fa3dfbf243616da78d5 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 17 Nov 2025 14:05:28 +0100 Subject: [PATCH 42/44] feat(macros): add warning when missing ERC6909HooksTrait impl is missing --- .../src/attribute/with_components/diagnostics.rs | 11 +++++++++++ .../macros/src/attribute/with_components/parser.rs | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/macros/src/attribute/with_components/diagnostics.rs b/packages/macros/src/attribute/with_components/diagnostics.rs index 81ffb3797..fa50f1dca 100644 --- a/packages/macros/src/attribute/with_components/diagnostics.rs +++ b/packages/macros/src/attribute/with_components/diagnostics.rs @@ -110,6 +110,17 @@ pub mod warnings { " }; + /// Warning when the ERC6909 component is missing an implementation of the ERC6909HooksTrait + pub const ERC6909_HOOKS_IMPL_MISSING: &str = indoc! { + "The ERC6909 component requires an implementation of the ERC6909HooksTrait in scope and + it looks like it is missing. + + You can use the ERC6909HooksEmptyImpl implementation by importing it: + + `use openzeppelin_token::erc6909::ERC6909HooksEmptyImpl;` + " + }; + /// Warning when the Upgradeable component is not used. pub const UPGRADEABLE_NOT_USED: &str = indoc! { "It looks like the `self.upgradeable.upgrade(new_class_hash)` function is not used in the contract. If diff --git a/packages/macros/src/attribute/with_components/parser.rs b/packages/macros/src/attribute/with_components/parser.rs index bb5206136..381239542 100644 --- a/packages/macros/src/attribute/with_components/parser.rs +++ b/packages/macros/src/attribute/with_components/parser.rs @@ -308,6 +308,15 @@ fn add_per_component_warnings(code: &str, component_info: &ComponentInfo) -> Vec warnings.push(warning); } } + AllowedComponents::ERC6909 => { + // Check that the ERC6909HooksTrait is implemented + let hooks_trait_used = code.contains("ERC6909HooksTrait"); + let hooks_empty_impl_used = code.contains("ERC6909HooksEmptyImpl"); + if !hooks_trait_used && !hooks_empty_impl_used { + let warning = Diagnostic::warn(warnings::ERC6909_HOOKS_IMPL_MISSING); + warnings.push(warning); + } + } AllowedComponents::Upgradeable => { // Check that the upgrade function is called let upgrade_function_called = code.contains("self.upgradeable.upgrade"); From de7f28f9ab03ce3337d186c5a37fcd1e7527063b Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 17 Nov 2025 14:23:11 +0100 Subject: [PATCH 43/44] docs: update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91abcd432..1031cc2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added -- ERC-6909: interfaces, components, tests, and docs. (initial contribution by @swan-of-bodom; rebased to current main) + +- ERC-6909 standard implementation and extensions (#1594) + - Added `ERC6909Component`, `ERC6909ContentURIComponent`, `ERC6909MetadataComponent`, and `ERC6909TokenSupplyComponent`. + - Added interfaces and ABI traits: `IERC6909`, `ERC6909ABI`, `IERC6909Metadata`, `ERC6909MetadataABI`, `IERC6909TokenSupply`, `ERC6909TokenSupplyABI`, `IERC6909ContentUri`, and `IERC6909ContentUriABI`. + - Added mocks: `ERC6909Mock`, `ERC6909MockWithHooks`, `ERC6909ContentURIMock`, `ERC6909MetadataMock`, `ERC6909TokenSupplyMock`. + - Added tests for components. + +- Integration of ERC-6909 components into the `with_components` macro (#1594) + - Added `ComponentInfo` implementations for ERC-6909 and its extensions. + - Added new diagnostic `ERC6909_HOOKS_IMPL_MISSING`. + - Added parser validation for missing hooks implementations. ## 3.0.0-alpha.3 (2025-10-9) @@ -502,4 +512,4 @@ with new external functions (#1173) - Account events indexed keys (#853) - Support higher tx versions in Account (#858) - Bump scarb to v2.4.1 (#858) -- Add security section to Upgrades docs (#861) \ No newline at end of file +- Add security section to Upgrades docs (#861) From 64a8e2f21b3f3b3848e4dc4ff2db30742f5ed8d7 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 18 Nov 2025 16:33:17 +0100 Subject: [PATCH 44/44] docs: update ERC6909 documentation --- docs/antora.yml | 2 +- docs/modules/ROOT/nav.adoc | 1 - docs/modules/ROOT/pages/api/erc6909.adoc | 625 ++++++++++++------ docs/modules/ROOT/pages/erc6909.adoc | 358 +++++++--- .../ROOT/pages/guides/erc6909-extensions.adoc | 351 ---------- 5 files changed, 722 insertions(+), 615 deletions(-) delete mode 100644 docs/modules/ROOT/pages/guides/erc6909-extensions.adoc diff --git a/docs/antora.yml b/docs/antora.yml index 43f89d8df..9902025a3 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -5,4 +5,4 @@ nav: - modules/ROOT/nav.adoc asciidoc: attributes: - page-sidebar-collapse-default: 'Access,Accounts,Finance,Governance,Introspection,Security,ERC20,ERC721,ERC1155,ERC4626,Upgrades,Universal Deployer Contract' + page-sidebar-collapse-default: 'Access,Accounts,Finance,Governance,Introspection,Security,ERC20,ERC721,ERC1155,ERC4626,ERC6909,Upgrades,Universal Deployer Contract' diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 70666c3a2..d236bfbab 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -54,7 +54,6 @@ *** xref:erc4626.adoc[ERC4626] **** xref:/api/erc20.adoc#ERC4626Component[API Reference] *** xref:erc6909.adoc[ERC6909] -**** xref:/guides/erc6909-extensions.adoc[Extensions] **** xref:/api/erc6909.adoc[API Reference] *** xref:/api/token_common.adoc[Common] diff --git a/docs/modules/ROOT/pages/api/erc6909.adoc b/docs/modules/ROOT/pages/api/erc6909.adoc index ac5c54111..34e758395 100644 --- a/docs/modules/ROOT/pages/api/erc6909.adoc +++ b/docs/modules/ROOT/pages/api/erc6909.adoc @@ -1,28 +1,36 @@ :github-icon: pass:[] -:eip6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] -:erc6909-guide: xref:erc6909.adoc[ERC6909 guide] -:casing-discussion: https://github.com/OpenZeppelin/cairo-contracts/discussions/34[here] +:eip6909: https://eips.ethereum.org/EIPS/eip-6909[EIP6909] +:inner-src5: xref:api/introspection.adoc#ISRC5[SRC5 ID] = ERC6909 include::../utils/_common.adoc[] -Reference of interfaces and utilities related to ERC6909 contracts. +This module provides interfaces, presets, and utilities related to ERC6909 contracts. -TIP: For an overview of ERC6909, read our {erc6909-guide}. +TIP: For an overview of ERC6909, read our xref:erc6909.adoc[ERC6909 guide]. -== Core +== Interfaces + +NOTE: Starting from version `3.x.x`, the interfaces are no longer part of the `openzeppelin_token` package. The references +documented here are contained in the `openzeppelin_interfaces` package version `v{current_openzeppelin_interfaces_version}`. [.contract] [[IERC6909]] -=== `++IERC6909++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/interface.cairo[{github-icon},role=heading-link] +=== `++IERC6909++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v3.0.0-alpha.3/packages/interfaces/src/token/erc6909.cairo[{github-icon},role=heading-link] [.hljs-theme-dark] ```cairo -use openzeppelin::token::erc6909::interface::IERC6909; +use openzeppelin_interfaces::erc6909::IERC6909; ``` -Interface of the IERC6909 standard as defined in {eip6909}. +Interface of the IERC6909 minimal multi-token standard as defined in {eip6909}. + +[.contract-index] +.{inner-src5} +-- +0xd5aa138060489fd9c4592f77a16011cc5615ce4d292ee1f7873ae65c43b6bb +-- [.contract-index] .Functions @@ -34,7 +42,6 @@ Interface of the IERC6909 standard as defined in {eip6909}. * xref:#IERC6909-transfer_from[`++transfer_from(sender, receiver, id, amount)++`] * xref:#IERC6909-approve[`++approve(spender, id, amount)++`] * xref:#IERC6909-set_operator[`++set_operator(spender, approved)++`] -* xref:#IERC6909-supports_interface[`++supports_interface(interface_id)++`] -- [.contract-index] @@ -45,35 +52,32 @@ Interface of the IERC6909 standard as defined in {eip6909}. * xref:#IERC6909-OperatorSet[`++OperatorSet(owner, spender, approved)++`] -- -[#IERC6909-Functions] ==== Functions [.contract-item] [[IERC6909-balance_of]] ==== `[.contract-item-name]#++balance_of++#++(owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# -Returns the amount owned by `owner` of `id`. +Returns the amount of `id` tokens owned by `owner`. [.contract-item] [[IERC6909-allowance]] ==== `[.contract-item-name]#++allowance++#++(owner: ContractAddress, spender: ContractAddress, id: u256) → u256++` [.item-kind]#external# -Returns the remaining number of `id` tokens that `spender` is allowed to spend on behalf of `owner` through <>. This is zero by default. - -This value changes when <> or <> are called, unless called by an operator. +Returns the remaining number of `id` tokens that `spender` is allowed to spend on behalf of `owner` +through <>. [.contract-item] [[IERC6909-is_operator]] ==== `[.contract-item-name]#++is_operator++#++(owner: ContractAddress, spender: ContractAddress) → bool++` [.item-kind]#external# -Checks if a `spender` is approved by an `owner` as an operator. Operators are not subject to allowance restrictions. +Returns `true` if `spender` is approved by `owner` as an operator over all token IDs. [.contract-item] [[IERC6909-transfer]] ==== `[.contract-item-name]#++transfer++#++(receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# -Moves `amount` of an `id` from the caller's token balance to `receiver`. -Returns `true` on success, reverts otherwise. +Transfers `amount` of token `id` from the caller to `receiver`. Emits a <> event. @@ -81,9 +85,7 @@ Emits a <> event. [[IERC6909-transfer_from]] ==== `[.contract-item-name]#++transfer_from++#++(sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# -Moves `amount` of an `id` from `sender` to `receiver` using the allowance mechanism. -`amount` is then deducted from the caller's allowance, unless called by an operator. -Returns `true` on success, reverts otherwise. +Transfers `amount` of token `id` from `sender` to `receiver`, deducting from the caller's allowance if required. Emits a <> event. @@ -91,60 +93,169 @@ Emits a <> event. [[IERC6909-approve]] ==== `[.contract-item-name]#++approve++#++(spender: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# -Sets `amount` as the allowance of `spender` over the caller's `id`. -Returns `true` on success, reverts otherwise. +Sets `amount` as the allowance of `spender` over the caller's tokens of ID `id`. -Emits an <> event. +Emits an <> event. [.contract-item] [[IERC6909-set_operator]] ==== `[.contract-item-name]#++set_operator++#++(spender: ContractAddress, approved: bool) → bool++` [.item-kind]#external# -Sets or unsets `spender` as an operator for the caller. +Sets or unsets `spender` as an operator for the caller, granting or revoking permission to transfer +any of the caller's token IDs. -Emits an <> event. +Emits an <> event. -[.contract-item] -[[IERC6909-supports_interface]] -==== `[.contract-item-name]#++supports_interface++#++(interface_id: felt252) → bool++` [.item-kind]#external# - -Checks if a contract implements `interface_id`. - -[#IERC6909-Events] ==== Events [.contract-item] [[IERC6909-Transfer]] ==== `[.contract-item-name]#++Transfer++#++(caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# -Emitted when `amount` of `id` are moved from `sender` to `receiver`. - -Note that `amount` may be zero. +Emitted when `amount` tokens of ID `id` are moved from `sender` to `receiver` by `caller`. [.contract-item] [[IERC6909-Approval]] ==== `[.contract-item-name]#++Approval++#++(owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# -Emitted when the allowance of a `spender` for an `owner` is set over a token `id`. -`amount` is the new allowance. +Emitted when the allowance of `spender` for `owner` is set to `amount` for token `id`. [.contract-item] [[IERC6909-OperatorSet]] ==== `[.contract-item-name]#++OperatorSet++#++(owner: ContractAddress, spender: ContractAddress, approved: bool)++` [.item-kind]#event# -Emitted when an operator (`spender`) is set or unset for `owner`. `approved` is the new status of the operator. +Emitted when `owner` enables or disables `spender` as an operator over all token IDs. + +[.contract] +[[IERC6909ContentUri]] +=== `++IERC6909ContentUri++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v3.0.0-alpha.3/packages/interfaces/src/token/erc6909.cairo[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```cairo +use openzeppelin_interfaces::erc6909::IERC6909ContentUri; +``` + +Interface for contract-level and token-level URIs in {eip6909}. + +[.contract-index] +.{inner-src5} +-- +0x356efd8b40a01c1525c7d0ecafbe3b82a47df564fdd496727effe6336526f05 +-- + +[.contract-index] +.Functions +-- +* xref:#IERC6909ContentUri-contract_uri[`++contract_uri()++`] +* xref:#IERC6909ContentUri-token_uri[`++token_uri(id)++`] +-- + +==== Functions + +[.contract-item] +[[IERC6909ContentUri-contract_uri]] +==== `[.contract-item-name]#++contract_uri++#++() → ByteArray++` [.item-kind]#external# + +Returns the contract-level URI. + +[.contract-item] +[[IERC6909ContentUri-token_uri]] +==== `[.contract-item-name]#++token_uri++#++(id: u256) → ByteArray++` [.item-kind]#external# + +Returns the token-level URI for token `id`. + +[.contract] +[[IERC6909Metadata]] +=== `++IERC6909Metadata++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v3.0.0-alpha.3/packages/interfaces/src/token/erc6909.cairo[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```cairo +use openzeppelin_interfaces::erc6909::IERC6909Metadata; +``` + +Interface for per-token metadata in {eip6909}. + +[.contract-index] +.{inner-src5} +-- +0x19aa0b778d120d5294054319458ee8886514766411c50dceddd9463712d6011 +-- + +[.contract-index] +.Functions +-- +* xref:#IERC6909Metadata-name[`++name(id)++`] +* xref:#IERC6909Metadata-symbol[`++symbol(id)++`] +* xref:#IERC6909Metadata-decimals[`++decimals(id)++`] +-- + +==== Functions + +[.contract-item] +[[IERC6909Metadata-name]] +==== `[.contract-item-name]#++name++#++(id: u256) → ByteArray++` [.item-kind]#external# + +Returns the name of token `id`. + +[.contract-item] +[[IERC6909Metadata-symbol]] +==== `[.contract-item-name]#++symbol++#++(id: u256) → ByteArray++` [.item-kind]#external# + +Returns the symbol of token `id`. + +[.contract-item] +[[IERC6909Metadata-decimals]] +==== `[.contract-item-name]#++decimals++#++(id: u256) → u8++` [.item-kind]#external# + +Returns the number of decimals of token `id`. + +[.contract] +[[IERC6909TokenSupply]] +=== `++IERC6909TokenSupply++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v3.0.0-alpha.3/packages/interfaces/src/token/erc6909.cairo[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```cairo +use openzeppelin_interfaces::erc6909::IERC6909TokenSupply; +``` + +Interface for querying per-token total supply in {eip6909}. + +[.contract-index] +.{inner-src5} +-- +0x3a632c15cb93b574eb9166de70521abbeab5c2eb4fdab9930729bba8658c41 +-- + +[.contract-index] +.Functions +-- +* xref:#IERC6909TokenSupply-total_supply[`++total_supply(id)++`] +-- + +==== Functions + +[.contract-item] +[[IERC6909TokenSupply-total_supply]] +==== `[.contract-item-name]#++total_supply++#++(id: u256) → u256++` [.item-kind]#external# + +Returns the total supply of token `id`. + +== Core [.contract] [[ERC6909Component]] -=== `++ERC6909Component++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/erc6909.cairo[{github-icon},role=heading-link] +=== `++ERC6909Component++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v3.0.0-alpha.3/packages/token/src/erc6909/erc6909.cairo[{github-icon},role=heading-link] [.hljs-theme-dark] ```cairo -use openzeppelin::token::erc6909::ERC6909Component; +use openzeppelin_token::erc6909::ERC6909Component; ``` -ERC6909 component extending <>. -NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how are hooks used. +ERC6909 component implementing <>. + +NOTE: {src5-component-required-note} + +NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how hooks are used. [.contract-index] .Hooks @@ -158,9 +269,9 @@ NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how are hooks used. [.contract-index#ERC6909Component-Embeddable-Mixin-Impl] .{mixin-impls} -- -.ERC6909MixinImpl +.ERC6909Impl * xref:#ERC6909Component-Embeddable-Impls-ERC6909Impl[`++ERC6909Impl++`] -* xref:#ERC6909Component-Embeddable-Impls-ERC6909CamelOnlyImpl[`++ERC6909CamelOnlyImpl++`] +* xref:api/introspection.adoc#SRC5Component-Embeddable-Impls-SRC5Impl[`++SRC5Impl++`] -- [.contract-index#ERC6909Component-Embeddable-Impls] @@ -175,33 +286,26 @@ NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how are hooks used. * xref:#ERC6909Component-transfer_from[`++transfer_from(self, sender, receiver, id, amount)++`] * xref:#ERC6909Component-approve[`++approve(self, spender, id, amount)++`] * xref:#ERC6909Component-set_operator[`++set_operator(self, spender, approved)++`] -* xref:#ERC6909Component-supports_interface[`++supports_interface(self, interface_id)++`] - -[.sub-index#ERC6909Component-Embeddable-Impls-ERC6909CamelOnlyImpl] -.ERC6909CamelOnlyImpl -* xref:#ERC6909Component-balanceOf[`++balanceOf(self, owner, id)++`] -* xref:#ERC6909Component-isOperator[`++isOperator(self, owner, spender)++`] -* xref:#ERC6909Component-transferFrom[`++transferFrom(self, sender, receiver, id, amount)++`] -* xref:#ERC6909Component-setOperator[`++setOperator(self, spender, approved)++`] -* xref:#ERC6909Component-supportsInterface[`++supportsInterface(self, interface_id)++`] -- [.contract-index] -.Internal implementations +.Internal functions -- .InternalImpl -* xref:#ERC6909Component-mint[`++mint(self, receiver, id, amount)++`] -* xref:#ERC6909Component-burn[`++burn(self, account, id, amount)++`] -* xref:#ERC6909Component-update[`++update(self, caller, sender, receiver, id, amount)++`] +* xref:#ERC6909Component-initializer[`++initializer(self)++`] +* xref:#ERC6909Component-_mint[`++_mint(self, receiver, id, amount)++`] +* xref:#ERC6909Component-_burn[`++_burn(self, account, id, amount)++`] +* xref:#ERC6909Component-_update[`++_update(self, sender, receiver, id, amount)++`] * xref:#ERC6909Component-_set_operator[`++_set_operator(self, owner, spender, approved)++`] -* xref:#ERC6909Component-_spend_allowance[`++_spend_allowance(self, sender, spender, id, amount)++`] +* xref:#ERC6909Component-_spend_allowance[`++_spend_allowance(self, owner, spender, id, amount)++`] * xref:#ERC6909Component-_approve[`++_approve(self, owner, spender, id, amount)++`] -* xref:#ERC6909Component-_transfer[`++_approve(self, caller, sender, receiver, id, amount)++`] +* xref:#ERC6909Component-_transfer[`++_transfer(self, sender, receiver, id, amount)++`] -- [.contract-index] .Events -- +.IERC6909 * xref:#ERC6909Component-Transfer[`++Transfer(caller, sender, receiver, id, amount)++`] * xref:#ERC6909Component-Approval[`++Approval(owner, spender, id, amount)++`] * xref:#ERC6909Component-OperatorSet[`++OperatorSet(owner, spender, approved)++`] @@ -210,213 +314,196 @@ NOTE: See xref:#ERC6909Component-Hooks[Hooks] to understand how are hooks used. [#ERC6909Component-Hooks] ==== Hooks -Hooks are functions which implementations can extend the functionality of the component source code. Every contract -using ERC6909Component is expected to provide an implementation of the ERC6909HooksTrait. For basic token contracts, an -empty implementation with no logic must be provided. +Hooks are functions which implementations can use to extend the functionality of the component source code. +Every contract using `ERC6909Component` is expected to provide an implementation of the `ERC6909HooksTrait`. +For basic token contracts, an empty implementation with no logic must be provided. -TIP: You can use `openzeppelin::token::erc6909::ERC6909HooksEmptyImpl` which is already available as part of the library -for this purpose. +TIP: You can use `openzeppelin_token::erc6909::ERC6909HooksEmptyImpl` which is already available as part of +the library for this purpose. [.contract-item] [[ERC6909Component-before_update]] -==== `[.contract-item-name]#++before_update++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#hook# +==== `[.contract-item-name]#++before_update++#++(ref self: ContractState, from: ContractAddress, recipient: ContractAddress, id: u256, amount: u256)++` [.item-kind]#hook# -Function executed at the beginning of the xref:#ERC6909Component-update[update] function prior to any other logic. +Function executed at the beginning of the xref:#ERC6909Component-_update[_update] function prior to any other logic. [.contract-item] [[ERC6909Component-after_update]] -==== `[.contract-item-name]#++after_update++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#hook# +==== `[.contract-item-name]#++after_update++#++(ref self: ContractState, from: ContractAddress, recipient: ContractAddress, id: u256, amount: u256)++` [.item-kind]#hook# -Function executed at the end of the xref:#ERC6909Component-update[update] function. +Function executed at the end of the xref:#ERC6909Component-_update[_update] function. -[#ERC6909Component-Embeddable-functions] ==== Embeddable functions [.contract-item] [[ERC6909Component-balance_of]] -==== `[.contract-item-name]#++balance_of++#++(@self: ContractState, owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# +==== `[.contract-item-name]#++balance_of++#++(self: @ContractState, owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# -See <>. +Returns the amount of `id` tokens owned by `owner`. [.contract-item] [[ERC6909Component-allowance]] -==== `[.contract-item-name]#++allowance++#++(@self: ContractState, owner: ContractAddress, spender: ContractAddress, id: u256) → u256++` [.item-kind]#external# +==== `[.contract-item-name]#++allowance++#++(self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256) → u256++` [.item-kind]#external# -See <>. +Returns the remaining number of `id` tokens that `spender` is allowed to spend on behalf of `owner` +through <>. [.contract-item] [[ERC6909Component-is_operator]] -==== `[.contract-item-name]#++is_operator++#++(@self: ContractState, owner: ContractAddress, spender: ContractAddress) → bool++` [.item-kind]#external# +==== `[.contract-item-name]#++is_operator++#++(self: @ContractState, owner: ContractAddress, spender: ContractAddress) → bool++` [.item-kind]#external# -See <>. +Returns `true` if `spender` is approved by `owner` as an operator over all token IDs. [.contract-item] [[ERC6909Component-transfer]] ==== `[.contract-item-name]#++transfer++#++(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# -See <>. +Transfers `amount` of token `id` from the caller to `receiver`. Requirements: -- `receiver` cannot be the zero address. -- The caller must have a balance of at least `amount`. +- `receiver` is not the zero address. + +Emits a <> event. [.contract-item] [[ERC6909Component-transfer_from]] ==== `[.contract-item-name]#++transfer_from++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# -See <>. +Transfers `amount` of token `id` from `sender` to `receiver`, deducting from the caller's allowance +for `sender` and `id` if applicable. Requirements: -- `sender` cannot be the zero address. -- `sender` must have a balance of at least `amount`. -- `receiver` cannot be the zero address. -- The caller must have allowance for ``sender``'s tokens of at least `amount`. +- Caller is either `sender`, an operator for `sender`, or has sufficient allowance. +- `sender` is not the zero address. +- `receiver` is not the zero address. +- `sender` must have at least a balance of `amount` for token `id`. + +Emits a <> event. [.contract-item] [[ERC6909Component-approve]] ==== `[.contract-item-name]#++approve++#++(ref self: ContractState, spender: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# -See <>. +Sets `amount` as the allowance of `spender` over the caller's tokens of token `id`. Requirements: -- `spender` cannot be the zero address. +- `spender` is not the zero address. + +Emits an <> event. [.contract-item] [[ERC6909Component-set_operator]] ==== `[.contract-item-name]#++set_operator++#++(ref self: ContractState, spender: ContractAddress, approved: bool) → bool++` [.item-kind]#external# -See <>. - -[.contract-item] -[[ERC6909Component-supports_interface]] -==== `[.contract-item-name]#++supports_interface++#++(self: @ContractState, interface_id: felt252) → bool++` [.item-kind]#external# +Sets or unsets `spender` as an operator for the caller, granting or revoking permission to transfer +any of the caller's token IDs. -See <>. - -[.contract-item] -[[ERC6909Component-balanceOf]] -==== `[.contract-item-name]#++balanceOf++#++(@self: ContractState, owner: ContractAddress, id: u256) → u256++` [.item-kind]#external# - -See <>. - -Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. +Requirements: -[.contract-item] -[[ERC6909Component-isOperator]] -==== `[.contract-item-name]#++isOperator++#++(@self: ContractState, owner: ContractAddress, spender: ContractAddress) → bool++` [.item-kind]#external# +- `spender` is not the zero address. -See <>. +Emits an <> event. -Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. +==== Internal functions [.contract-item] -[[ERC6909Component-transferFrom]] -==== `[.contract-item-name]#++transferFrom++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256) → bool++` [.item-kind]#external# +[[ERC6909Component-initializer]] +==== `[.contract-item-name]#++initializer++#++(ref self: ContractState)++` [.item-kind]#internal# -See <>. +Initializes the contract by registering the IERC6909 interface ID as supported through introspection. -Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. +This should only be used inside the contract's constructor. [.contract-item] -[[ERC6909Component-setOperator]] -==== `[.contract-item-name]#++setOperator++#++(ref self: ContractState, operator: ContractAddress, approved: bool) → bool++` [.item-kind]#external# +[[ERC6909Component-_mint]] +==== `[.contract-item-name]#++_mint++#++(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# -See <>. +Mints `amount` tokens of token ID `id` to `receiver`. -Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. - -[.contract-item] -[[ERC6909Component-supportsInterface]] -==== `[.contract-item-name]#++supportsInterface++#++(ref self: ContractState, interface_id: felt252) → bool++` [.item-kind]#external# - -See <>. +Requirements: -Supports the Cairo v0 convention of writing external methods in camelCase as discussed {casing-discussion}. +- `receiver` is not the zero address. -[#ERC6909Component-Internal-functions] -==== Internal functions +Emits a <> event with `sender` set to the zero address. [.contract-item] -[[ERC6909Component-mint]] -==== `[.contract-item-name]#++mint++#++(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# - -Creates an `amount` number of `id` tokens and assigns them to `receiver`. +[[ERC6909Component-_burn]] +==== `[.contract-item-name]#++_burn++#++(ref self: ContractState, account: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# -Emits a <> event with `from` being the zero address. +Destroys `amount` tokens of ID `id` from `account`. Requirements: -- `receiver` cannot be the zero address. +- `account` is not the zero address. +- `account` must have at least a balance of `amount` for token `id`. + +Emits a <> event with `receiver` set to the zero address. [.contract-item] -[[ERC6909Component-burn]] -==== `[.contract-item-name]#++burn++#++(ref self: ContractState, account: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# +[[ERC6909Component-_update]] +==== `[.contract-item-name]#++_update++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# -Destroys `amount` number of `id` tokens from `account`. +Transfers `amount` tokens of ID `id` from `sender` to `receiver`, or alternatively mints (or burns) +if `sender` (or `receiver`) is the zero address. -Emits a <> event with `to` set to the zero address. +This function can be extended using the xref:#ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait], to add +functionality before and/or after the transfer, mint, or burn. -Requirements: +The implementation does not track per-token total supply; this logic is delegated to extensions. -- `account` cannot be the zero address. +Emits a <> event. [.contract-item] -[[ERC6909Component-update]] -==== `[.contract-item-name]#++update++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# +[[ERC6909Component-_set_operator]] +==== `[.contract-item-name]#++_set_operator++#++(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, approved: bool)++` [.item-kind]#internal# -Transfers an `amount` of `id` tokens from `from` to `to`, or alternatively mints (or burns) if `from` (or `to`) is -the zero address. +Sets or unsets `spender` as an operator for `owner`. -NOTE: This function can be extended using the xref:ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait], to add -functionality before and/or after the transfer, mint, or burn. - -Emits a <> event. +Emits an <> event. [.contract-item] -[[ERC6909Component-_transfer]] -==== `[.contract-item-name]#++_transfer++#++(ref self: ContractState, caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# +[[ERC6909Component-_spend_allowance]] +==== `[.contract-item-name]#++_spend_allowance++#++(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# -Moves `amount` of `id` tokens from `from` to `to`. +Updates `owner`'s allowance for `spender` and token `id` based on `amount` spent. -This internal function does not check for access permissions but can be useful as a building block, for example to implement automatic token fees, slashing mechanisms, etc. - -Emits a <> event. +Spenders that have been set as operators for `owner` are not subject to allowance restrictions. +If the current allowance is set to the maximum value, it is treated as infinite and is not reduced. Requirements: -- `from` cannot be the zero address. -- `to` cannot be the zero address. -- `from` must have a balance of `id` tokens of at least `amount`. +- If not infinite and not an operator, the allowance must be at least `amount`. [.contract-item] [[ERC6909Component-_approve]] ==== `[.contract-item-name]#++_approve++#++(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# -Sets `amount` as the allowance of `spender` over ``owner``'s `id` tokens. - -This internal function does not check for access permissions but can be useful as a building block, for example to implement automatic allowances on behalf of other addresses. - -Emits an <> event. +Internal method that sets `amount` as the allowance of `spender` over `owner`'s tokens of token `id`. Requirements: -- `owner` cannot be the zero address. -- `spender` cannot be the zero address. +- `owner` is not the zero address. +- `spender` is not the zero address. + +Emits an <> event. [.contract-item] -[[ERC6909Component-_spend_allowance]] -==== `[.contract-item-name]#++_spend_allowance++#++(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# +[[ERC6909Component-_transfer]] +==== `[.contract-item-name]#++_transfer++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +Internal method that moves `amount` tokens of ID `id` from `sender` to `receiver`. -Updates ``owner``'s allowance for `spender` based on spent `amount` for `id` tokens. +Requirements: -This internal function does not update the allowance value in the case of infinite allowance or if spender is operator. +- `sender` is not the zero address. +- `receiver` is not the zero address. +- `sender` must have at least a balance of `amount` for token `id`. -Possibly emits an <> event. +Emits a <> event. -[#ERC6909Component-Events] ==== Events [.contract-item] @@ -427,7 +514,7 @@ See <>. [.contract-item] [[ERC6909Component-Approval]] -==== `[.contract-item-name]#++Approval++#++(owner: ContractAddress, spender: ContractAddress, value: u256)++` [.item-kind]#event# +==== `[.contract-item-name]#++Approval++#++(owner: ContractAddress, spender: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# See <>. @@ -441,18 +528,17 @@ See <>. [.contract] [[ERC6909ContentURIComponent]] -=== `++ERC6909ContentURIComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/extensions/erc6909_content_uri.cairo[{github-icon},role=heading-link] +=== `++ERC6909ContentURIComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v3.0.0-alpha.3/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo[{github-icon},role=heading-link] +[.hljs-theme-dark] ```cairo -use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; +use openzeppelin_token::erc6909::extensions::ERC6909ContentURIComponent; ``` -Extension of ERC6909 to support contract and token URIs. +ERC6909ContentURI component implementing <>. NOTE: Implementing xref:#ERC6909Component[ERC6909Component] is a requirement for this component to be implemented. -This extension allows to set the contract URI (ideally) in the constructor via `initializer(uri: ByteArray)`. - [.contract-index#ERC6909ContentURIComponent-Embeddable-Impls] .Embeddable Implementations -- @@ -463,49 +549,218 @@ This extension allows to set the contract URI (ideally) in the constructor via ` -- [.contract-index] -.Internal implementations +.Internal functions -- .InternalImpl * xref:#ERC6909ContentURIComponent-initializer[`++initializer(self, contract_uri)++`] -- -[#ERC6909ContentURI-Embeddable-functions] ==== Embeddable functions [.contract-item] -[[ERC6909ContentURI-contract_uri]] +[[ERC6909ContentURIComponent-contract_uri]] ==== `[.contract-item-name]#++contract_uri++#++(self: @ContractState) → ByteArray++` [.item-kind]#external# -Returns the contract URI. +Returns the contract-level URI. [.contract-item] -[[ERC6909ContentURI-token_uri]] +[[ERC6909ContentURIComponent-token_uri]] ==== `[.contract-item-name]#++token_uri++#++(self: @ContractState, id: u256) → ByteArray++` [.item-kind]#external# -Returns the token URI for `id` token +Returns the token-level URI for token `id`. + +If a contract URI is set, the resulting URI for each token will be the concatenation of the contract URI +and the token ID. For example, the contract URI pass:[https://token-cdn-domain/] would be +returned as pass:[https://token-cdn-domain/123] for token ID 123. + +If the contract URI is not set, the return value will be an empty ByteArray. -[#ERC6909ContentURI-Internal-functions] ==== Internal functions [.contract-item] -[[ERC6909ContentURI-initializer]] +[[ERC6909ContentURIComponent-initializer]] ==== `[.contract-item-name]#++initializer++#++(ref self: ContractState, contract_uri: ByteArray)++` [.item-kind]#internal# -Initializes the contract URI. -This should be used inside of the contract's constructor. +Initializes the component by setting the contract-level URI to `contract_uri` and registering the +IERC6909ContentUri interface ID as supported through introspection. + +This should be used inside the contract's constructor. [.contract] [[ERC6909MetadataComponent]] -=== `++ERC6909MetadataComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.14.0/src/token/erc6909/extensions/erc6909_metadata.cairo[{github-icon},role=heading-link] +=== `++ERC6909MetadataComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v3.0.0-alpha.3/packages/token/src/erc6909/extensions/erc6909_metadata.cairo[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```cairo +use openzeppelin_token::erc6909::extensions::ERC6909MetadataComponent; +``` + +ERC6909Metadata component implementing <>. + +This extension allows contracts to associate name, symbol, and decimals metadata with each token ID. + +NOTE: Implementing xref:#ERC6909Component[ERC6909Component] is a requirement for this component to be implemented. +To properly initialize metadata, this extension expects its internal helper +xref:#ERC6909MetadataComponent-_update_token_metadata[_update_token_metadata] to be used from the ERC6909 hooks +during mints. + +[.contract-index#ERC6909MetadataComponent-Embeddable-Impls] +.Embeddable Implementations +-- +[.sub-index#ERC6909MetadataComponent-Embeddable-Impls-ERC6909MetadataImpl] +.ERC6909MetadataImpl +* xref:#ERC6909MetadataComponent-name[`++name(self, id)++`] +* xref:#ERC6909MetadataComponent-symbol[`++symbol(self, id)++`] +* xref:#ERC6909MetadataComponent-decimals[`++decimals(self, id)++`] +-- + +[.contract-index] +.Internal functions +-- +.InternalImpl +* xref:#ERC6909MetadataComponent-initializer[`++initializer(self)++`] +* xref:#ERC6909MetadataComponent-_update_token_metadata[`++_update_token_metadata(self, sender, id, name, symbol, decimals)++`] +* xref:#ERC6909MetadataComponent-_token_metadata_exists[`++_token_metadata_exists(self, id)++`] +* xref:#ERC6909MetadataComponent-_set_token_metadata[`++_set_token_metadata(self, id, name, symbol, decimals)++`] +* xref:#ERC6909MetadataComponent-_set_token_name[`++_set_token_name(self, id, name)++`] +* xref:#ERC6909MetadataComponent-_set_token_symbol[`++_set_token_symbol(self, id, symbol)++`] +* xref:#ERC6909MetadataComponent-_set_token_decimals[`++_set_token_decimals(self, id, decimals)++`] +-- + +==== Embeddable functions + +[.contract-item] +[[ERC6909MetadataComponent-name]] +==== `[.contract-item-name]#++name++#++(self: @ContractState, id: u256) → ByteArray++` [.item-kind]#external# + +Returns the name of token `id`. + +[.contract-item] +[[ERC6909MetadataComponent-symbol]] +==== `[.contract-item-name]#++symbol++#++(self: @ContractState, id: u256) → ByteArray++` [.item-kind]#external# + +Returns the symbol of token `id`. + +[.contract-item] +[[ERC6909MetadataComponent-decimals]] +==== `[.contract-item-name]#++decimals++#++(self: @ContractState, id: u256) → u8++` [.item-kind]#external# + +Returns the number of decimals of token `id`. + +==== Internal functions + +[.contract-item] +[[ERC6909MetadataComponent-initializer]] +==== `[.contract-item-name]#++initializer++#++(ref self: ContractState)++` [.item-kind]#internal# + +Initializes the component by registering the IERC6909Metadata interface ID as supported +through introspection. + +This should be used inside the contract's constructor. + +[.contract-item] +[[ERC6909MetadataComponent-_update_token_metadata]] +==== `[.contract-item-name]#++_update_token_metadata++#++(ref self: ContractState, sender: ContractAddress, id: u256, name: ByteArray, symbol: ByteArray, decimals: u8)++` [.item-kind]#internal# + +Updates the metadata of token `id` when it is first minted. + +If `sender` is the zero address (i.e. this call happens during a mint) and `id` does not yet +have metadata, this function stores the provided `name`, `symbol`, and `decimals`. + +This helper is intended to be called from the ERC6909 hooks during mint operations. + +[.contract-item] +[[ERC6909MetadataComponent-_token_metadata_exists]] +==== `[.contract-item-name]#++_token_metadata_exists++#++(self: @ContractState, id: u256) → bool++` [.item-kind]#internal# + +Returns `true` if token `id` has metadata associated with it at the time of minting. + +[.contract-item] +[[ERC6909MetadataComponent-_set_token_metadata]] +==== `[.contract-item-name]#++_set_token_metadata++#++(ref self: ContractState, id: u256, name: ByteArray, symbol: ByteArray, decimals: u8)++` [.item-kind]#internal# + +Updates the stored metadata for token `id` with the provided `name`, `symbol`, and `decimals`. + +[.contract-item] +[[ERC6909MetadataComponent-_set_token_name]] +==== `[.contract-item-name]#++_set_token_name++#++(ref self: ContractState, id: u256, name: ByteArray)++` [.item-kind]#internal# + +Sets the name for token `id`. + +[.contract-item] +[[ERC6909MetadataComponent-_set_token_symbol]] +==== `[.contract-item-name]#++_set_token_symbol++#++(ref self: ContractState, id: u256, symbol: ByteArray)++` [.item-kind]#internal# + +Sets the symbol for token `id`. + +[.contract-item] +[[ERC6909MetadataComponent-_set_token_decimals]] +==== `[.contract-item-name]#++_set_token_decimals++#++(ref self: ContractState, id: u256, decimals: u8)++` [.item-kind]#internal# + +Sets the decimals for token `id`. +[.contract] +[[ERC6909TokenSupplyComponent]] +=== `++ERC6909TokenSupplyComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v3.0.0-alpha.3/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo[{github-icon},role=heading-link] + +[.hljs-theme-dark] ```cairo -use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; +use openzeppelin_token::erc6909::extensions::ERC6909TokenSupplyComponent; ``` -Extension of ERC6909 to support contract metadata. +ERC6909TokenSupply component implementing <>. + +This extension allows contracts to track the total supply for each token ID. NOTE: Implementing xref:#ERC6909Component[ERC6909Component] is a requirement for this component to be implemented. +To properly track total supply, this extension expects its helper +xref:#ERC6909TokenSupplyComponent-_update_token_supply[_update_token_supply] to be used from the ERC6909 hooks +during mints and burns. + +[.contract-index#ERC6909TokenSupplyComponent-Embeddable-Impls] +.Embeddable Implementations +-- +[.sub-index#ERC6909TokenSupplyComponent-Embeddable-Impls-ERC6909TokenSupplyImpl] +.ERC6909TokenSupplyImpl +* xref:#ERC6909TokenSupplyComponent-total_supply[`++total_supply(self, id)++`] +-- + +[.contract-index] +.Internal functions +-- +.InternalImpl +* xref:#ERC6909TokenSupplyComponent-initializer[`++initializer(self)++`] +* xref:#ERC6909TokenSupplyComponent-_update_token_supply[`++_update_token_supply(self, sender, receiver, id, amount)++`] +-- + +[#ERC6909TokenSupplyComponent-Embeddable-functions] +==== Embeddable functions + +[.contract-item] +[[ERC6909TokenSupplyComponent-total_supply]] +==== `[.contract-item-name]#++total_supply++#++(self: @ContractState, id: u256) → u256++` [.item-kind]#external# + +Returns the total supply of token `id`. + +[#ERC6909TokenSupplyComponent-Internal-functions] +==== Internal functions + +[.contract-item] +[[ERC6909TokenSupplyComponent-initializer]] +==== `[.contract-item-name]#++initializer++#++(ref self: ContractState)++` [.item-kind]#internal# + +Initializes the component by registering the IERC6909TokenSupply interface ID as supported +through introspection. + +This should be used inside the contract's constructor. + +[.contract-item] +[[ERC6909TokenSupplyComponent-_update_token_supply]] +==== `[.contract-item-name]#++_update_token_supply++#++(ref self: ContractState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +Updates the total supply of token `id`. -WARNING: To individual token metadata, this extension requires that the -xref:#ERC6909MetadataComponent-_update_token_metadata[_update_token_metadata] function is called after every mint. For this, the xref:ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait] must be used. +When `sender` is the zero address, `amount` is added to the total supply (mint). +When `receiver` is the zero address, `amount` is subtracted from the total supply (burn). +This helper is intended to be called from the ERC6909 hooks during mint and burn operations. diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index 2fcb36625..0a302f788 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -1,133 +1,337 @@ -= ERC6909 - +:eip6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] :fungibility-agnostic: https://docs.openzeppelin.com/contracts/5.x/tokens#different-kinds-of-tokens[fungibility-agnostic] -:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] -The ERC6909 minimal multi token standard is a specification for {fungibility-agnostic} token contracts. -`token::erc6909::ERC6909Component` provides an approximation of {eip-6909} in Cairo for StarkNet. - -== Minimal Multi Token Standard += ERC6909 -Similar to ERC1155, it uses a single smart contract to represent multiple tokens via unique IDs. The main difference is -that in ERC6909 "the callbacks and batching have been removed from the interface and the permission system is a hybrid operator-approval -scheme for granular and scalable permissions. Functionally, the interface has been reduced to the bare minimum -required to manage multiple tokens under the same contract." {eip-6909} +The ERC6909 minimal multi-token standard is a specification for {fungibility-agnostic} token contracts that manage multiple token IDs in a single contract. Compared to ERC1155, it removes batch operations and transfer callbacks, and introduces a hybrid allowance–operator approval model for more granular control over approvals. +The ERC6909 library provides an approximation of {eip6909} in Cairo for Starknet. == Usage -:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] -:erc6909-extensions: xref:/guides/erc6909-extensions.adoc[ERC6909 Extensions] +:mint-api: xref:api/erc6909.adoc#ERC6909Component-_mint[_mint] +:burn-api: xref:api/erc6909.adoc#ERC6909Component-_burn[_burn] +:transfer-api: xref:api/erc6909.adoc#IERC6909-transfer[transfer] +:transfer-from-api: xref:api/erc6909.adoc#IERC6909-transfer_from[transfer_from] +:set-operator-api: xref:api/erc6909.adoc#IERC6909-set_operator[set_operator] -The ERC6909 minimal multi token standard is a specification for {fungibility-agnostic} token contracts. - -Using Contracts for Cairo, constructing an ERC6909 contract requires integrating the `ERC6909Component`. -Here's an example of a basic ERC6909 contract: +Using Contracts for Cairo, constructing an ERC6909 contract requires integrating both `ERC6909Component` and `SRC5Component`. +The contract should also set up the constructor to register interface support and optionally mint initial balances. +Here's an example of a basic contract: [,cairo] ---- #[starknet::contract] -mod MyERC6909Token { - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; +mod MyERC6909 { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; use starknet::ContractAddress; component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); - // ERC6909 Mixin #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + impl ERC6909Impl = ERC6909Component::ERC6909Impl; impl ERC6909InternalImpl = ERC6909Component::InternalImpl; #[storage] struct Storage { #[substorage(v0)] - erc6909: ERC6909Component::Storage + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage } #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] - ERC6909Event: ERC6909Component::Event + ERC6909Event: ERC6909Component::Event, + #[flat] + SRC5Event: SRC5Component::Event } #[constructor] fn constructor( ref self: ContractState, - recipient: ContractAddress, - token_id: u256, - initial_supply: u256, - contract_uri: ByteArray + recipient: ContractAddress + id: u256, + amount: u256, ) { - self.erc6909.mint(recipient, token_id, initial_supply); + self.erc6909.initializer(); + self.erc6909._mint(recipient, id, amount); + } } ---- -`MyERC6909Token` integrates the `ERC6909Impl` with the embed directive which marks the implementation as external in the contract -by importing the `ERC6909Mixin` which has both camel and snake-case functions. - -The above example also includes the `ERC6909InternalImpl` instance to access internal functions (such as `mint`) - -There are 3 optional extensions which can also be imported into `MyERC6909Token`: - -* `ERC6909ContentURI` - Allows to set the base contract URI and thus show individual token URIs. -* `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. -* `ERC6909TokenSupply` - Allows to keep track of individual token supplies upon mints and burns. - -TIP: For a more complete guide on using these extensions, see {erc6909-extensions}. +Once deployed, this contract exposes the full {transfer-api}, {transfer-from-api}, and {set-operator-api} entrypoints defined in {eip6909}, supporting both token ID allowances and global operators for an account. == Interface -:dual-interfaces: xref:/interfaces.adoc#dual_interfaces[Dual interfaces] +:compatibility: xref:/erc6909.adoc#erc6909_compatibility[ERC6909 Compatibility] +:isrc5-interface: xref:/api/introspection.adoc#ISRC5[ISRC5] :ierc6909-interface: xref:/api/erc6909.adoc#IERC6909[IERC6909] -:ierc6909_metadata-interface: xref:/api/erc6909.adoc#IERC6909Metadata[IERC6909Metadata] -:ierc6909_tokensupply-interface: xref:/api/erc6909.adoc#IERC6909TokenSupply[IERC6909TokenSupply] -:ierc6909_contenturi-interface: xref:/api/erc6909.adoc#IERC6909ContentURI[IERC6909ContentURI] +:ierc6909-metadata-interface: xref:/api/erc6909.adoc#IERC6909Metadata[IERC6909Metadata] +:ierc6909-content-uri-interface: xref:/api/erc6909.adoc#IERC6909ContentUri[IERC6909ContentUri] +:ierc6909-token-supply-interface: xref:/api/erc6909.adoc#IERC6909TokenSupply[IERC6909TokenSupply] :erc6909-component: xref:/api/erc6909.adoc#ERC6909Component[ERC6909Component] -The following interface represents the full ABI of the Contracts for Cairo {erc6909-component}. -The interface includes the {ierc6909-interface} standard interface. - -To support older token deployments, as mentioned in {dual-interfaces}, the component also includes an implementation of the interface written in camelCase. +The following interface represents a convenient ABI that mirrors the full surface of the Contracts for Cairo +{erc6909-component} together with its optional extensions. It includes the {ierc6909-interface} minimal multi-token +interface, the optional {ierc6909-content-uri-interface}, {ierc6909-metadata-interface}, and +{ierc6909-token-supply-interface} interfaces, as well as {isrc5-interface} for introspection. [,cairo] ---- #[starknet::interface] -pub trait ERC6909ABI { - /// @notice IERC6909 standard interface - fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; - fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; +pub trait ERC6909ABI { + // IERC6909 + fn balance_of(owner: ContractAddress, id: u256) -> u256; + fn allowance(owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + fn is_operator(owner: ContractAddress, spender: ContractAddress) -> bool; + fn transfer(receiver: ContractAddress, id: u256, amount: u256) -> bool; fn transfer_from( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 ) -> bool; - fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; - fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - fn supports_interface(self: @TState, interface_id: felt252) -> bool; - - /// @notice IERC6909Camel - fn balanceOf(self: @TState, owner: ContractAddress, id: u256) -> u256; - fn isOperator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; - fn transferFrom( - ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 - ) -> bool; - fn setOperator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; - fn supportsInterface(self: @TState, interfaceId: felt252) -> bool; + fn approve(spender: ContractAddress, id: u256, amount: u256) -> bool; + fn set_operator(spender: ContractAddress, approved: bool) -> bool; + + // IERC6909ContentUri + fn contract_uri() -> ByteArray; + fn token_uri(id: u256) -> ByteArray; + + // IERC6909Metadata + fn name(id: u256) -> ByteArray; + fn symbol(id: u256) -> ByteArray; + fn decimals(id: u256) -> u8; + + // IERC6909TokenSupply + fn total_supply(id: u256) -> u256; + + // ISRC5 + fn supports_interface(interface_id: felt252) -> bool; } ---- +NOTE: This ABI is provided as a convenience example that groups the standard and optional interfaces implemented by the +components. Your contracts are free to expose a subset of these functions depending on which extensions you integrate. + +[#erc6909_compatibility] == ERC6909 compatibility -:cairo-selectors: https://github.com/starkware-libs/cairo/blob/7dd34f6c57b7baf5cd5a30c15e00af39cb26f7e1/crates/cairo-lang-starknet/src/contract.rs#L39-L48[Cairo] -:solidity-selectors: https://solidity-by-example.org/function-selector/[Solidity] -:dual-interface: xref:/interfaces.adoc#dual_interfaces[dual interface] -:interface-id: https://community.starknet.io/t/starknet-standard-interface-detection/92664/23[interface ID] +:src5-api: xref:introspection.adoc#src5[SRC5] +:introspection: xref:introspection.adoc[Introspection] +:eip6909-spec: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] + +Although Starknet is not EVM compatible, this implementation aims to be as close as possible to the ERC6909 standard. +Some notable differences are: + +* Interface detection is implemented through the {src5-api} introspection standard on Starknet. + Components hardcode their interface IDs according to Cairo selector calculations and register them via SRC5. + See the {introspection} docs for details. +* Strings and URIs are represented as `ByteArray` instead of Solidity's `string`. +* Infinite allowances follow the {eip6909-spec} convention by assigning the maximum possible value of the u256 type +* Spenders marked as operators for an owner are not subject to allowance restrictions. + In that case, `transfer_from` will bypass allowance accounting and only enforce balance constraints, in line with + the {eip6909-spec} semantics. + +== Hooks and extensions + +:hooks-api: xref:api/erc6909.adoc#ERC6909Component-ERC6909HooksTrait[ERC6909HooksTrait] +:before-update-api: xref:api/erc6909.adoc#ERC6909Component-before_update[before_update] +:after-update-api: xref:api/erc6909.adoc#ERC6909Component-after_update[after_update] + +The {erc6909-component} exposes a hook trait, {hooks-api}, that allows extending its core behavior without modifying +the component code itself. Every contract using `ERC6909Component` is expected to provide an implementation of this +trait, even if empty, typically using `ERC6909HooksEmptyImpl` for basic presets. + +Hooks are invoked by the internal `_update` helper whenever tokens are transferred, minted, or burned: + +* {before-update-api} is executed before balances are modified. +* {after-update-api} is executed after balances are modified and events have been emitted. + +TIP: Extensions such as `ERC6909TokenSupplyComponent` and `ERC6909MetadataComponent` are designed to be called from +these hooks. + +=== ERC6909ContentURI + +:erc6909-contenturi-component: xref:/api/erc6909.adoc#ERC6909ContentURIComponent[ERC6909ContentURIComponent] +:contract-uri-api: xref:/api/erc6909.adoc#IERC6909ContentUri-contract_uri[contract_uri] +:token-uri-api: xref:/api/erc6909.adoc#IERC6909ContentUri-token_uri[token_uri] + +The {erc6909-contenturi-component} implements {ierc6909-content-uri-interface} and provides contract-level and +token-level URIs. + +* {contract-uri-api} returns the base contract URI. +* {token-uri-api} returns the concatenation of the contract URI and the token ID, or an empty `ByteArray` if no + contract URI is set. + +A typical integration calls the component's `initializer` in the constructor: + +[,cairo] +---- +#[starknet::contract] +mod MyERC6909WithURI { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc6909::{ + ERC6909Component, ERC6909ContentURIComponent, ERC6909HooksEmptyImpl + }; + use starknet::ContractAddress; + + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!(path: ERC6909ContentURIComponent, storage: erc6909_content_uri, event: ERC6909ContentURIEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + #[abi(embed_v0)] + impl ERC6909ContentURIImpl = ERC6909ContentURIComponent::ERC6909ContentURIImpl; + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event, + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + contract_uri: ByteArray, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + self.erc6909.initializer(); + self.erc6909_content_uri.initializer(contract_uri); + self.erc6909._mint(recipient, id, amount); + } +} +---- + +=== ERC6909Metadata + +:erc6909-metadata-component: xref:/api/erc6909.adoc#ERC6909MetadataComponent[ERC6909MetadataComponent] +:metadata-name-api: xref:/api/erc6909.adoc#IERC6909Metadata-name[name] +:metadata-symbol-api: xref:/api/erc6909.adoc#IERC6909Metadata-symbol[symbol] +:metadata-decimals-api: xref:/api/erc6909.adoc#IERC6909Metadata-decimals[decimals] +:update-token-metadata-api: xref:/api/erc6909.adoc#ERC6909MetadataComponent-_update_token_metadata[_update_token_metadata] + +The {erc6909-metadata-component} implements {ierc6909-metadata-interface} and allows associating metadata with each +token ID: + +* {metadata-name-api} returns the token name. +* {metadata-symbol-api} returns the token symbol. +* {metadata-decimals-api} returns the number of decimals for that token ID. + +The extension expects its helper {update-token-metadata-api} to be called from the ERC6909 hooks. +A simple pattern is to call it from `before_update`, passing the `from` address. The helper will detect mints +(when `from` is the zero address) and only set metadata for token IDs that do not already have it. + +[,cairo] +---- +use openzeppelin_token::erc6909::{ERC6909Component, ERC6909MetadataComponent}; +use starknet::ContractAddress; + +pub impl ERC6909HooksImpl of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ContractState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + // Example: assign default metadata on first mint of each ID + let name = "MyERC6909Token"; + let symbol = "M6909"; + let decimals: u8 = 18_u8; + + self + .erc6909_metadata + ._update_token_metadata(from, id, name, symbol, decimals); + } + + fn after_update( + ref self: ContractState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256, + ) {} +} +---- + +Don't forget to: + +* Add `ERC6909MetadataComponent` as a component in your contract. +* Call its `initializer` in the constructor so that the `IERC6909Metadata` interface ID is registered via SRC5. + +=== ERC6909TokenSupply + +:erc6909-supply-component: xref:/api/erc6909.adoc#ERC6909TokenSupplyComponent[ERC6909TokenSupplyComponent] +:total-supply-api: xref:/api/erc6909.adoc#IERC6909TokenSupply-total_supply[total_supply] +:update-token-supply-api: xref:/api/erc6909.adoc#ERC6909TokenSupplyComponent-_update_token_supply[_update_token_supply] + +The {erc6909-supply-component} implements {ierc6909-token-supply-interface} and tracks the total supply per token ID. + +* {total-supply-api} returns the total supply of a given token ID. + +The helper {update-token-supply-api} is designed to be called from ERC6909 hooks during mints and burns: + +* When `sender` is the zero address, `amount` is added to the total supply. +* When `receiver` is the zero address, `amount` is subtracted from the total supply. + +[,cairo] +---- +use openzeppelin_token::erc6909::{ERC6909Component, ERC6909TokenSupplyComponent}; +use starknet::ContractAddress; + +pub impl ERC6909HooksImpl of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ContractState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256, + ) { + self + .erc6909_token_supply + ._update_token_supply(from, recipient, id, amount); + } + + fn after_update( + ref self: ContractState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256, + ) {} +} +---- + +As with other extensions: -Although Starknet is not EVM compatible, this component aims to be as close as possible to the ERC6909 token standard. -Some notable differences, however, can still be found, such as: +* Add `ERC6909TokenSupplyComponent` as a component in your contract. +* Call its `initializer` during construction to register `IERC6909TokenSupply` support via SRC5. -* The `ByteArray` type is used to represent strings in Cairo in the Metadata extension. -* The `felt252` type is used to represent the `byte4` interface ID. The {interface-id} is also calculated different in Cairo. -* The component offers a {dual-interface} which supports both snake_case and camelCase methods, as opposed to just camelCase in Solidity. -* `transfer`, `transfer_from` and `approve` will never return anything different from `true` because they will revert on any error. +By composing `ERC6909Component` with these extensions and your own hook logic, you can build rich multi-token systems +with per-ID metadata, URIs, and supply tracking while keeping the core transfer logic minimal and reusable. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc deleted file mode 100644 index 31cb1cb69..000000000 --- a/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc +++ /dev/null @@ -1,351 +0,0 @@ -= ERC6909 Extensions - -:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] - -{eip-6909} is a fungible-agnostic multi-token standard, but does not define -certain characteristics typically found across fungible tokens: Such as metadata and -token supplies. - -This is why there are 3 optional extensions which can also be imported into `MyERC6909Token` out of the box to be more accessible: - -* `ERC6909ContentURI` - Allows to set the base contract URI and thus show individual token URIs. -* `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. -* `ERC6909TokenSupply` - Allows to keep track of individual token supplies upon mints and burns. - -The `ERC6909Component` always requires for hooks to be implemented. In the case of the first extension -(Content URI) simply importing the `HooksEmptyImpl` is enough. The other extensions make use of hooks -so we must implement these. - -This guide will go over these extensions and how to integrate them into your `ERC6909` contracts, with an example -for each component integration. - - -== ERC6909 Content URI - -Let's say we want to create a ERC6909 token named `MyERC6909TokenWithURI` with a contract URI. As explained the -contract URI is not part of the {eip-6909} but rather an optional extension. Therefore to achieve -this we can make use of the `ERC6909ContentURI` extension. - -[,cairo] ----- -#[starknet::contract] -pub mod MyERC6909ContentURI { - // 1. Import the Content URI Component - use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; - use starknet::ContractAddress; - - // 2. Declare the component to access its storage and events - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - component!( - path: ERC6909ContentURIComponent, - storage: erc6909_content_uri, - event: ERC6909ContentURIEvent - ); - - // 3. Embed ABI to access external functions - #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - #[abi(embed_v0)] - impl ERC6909ContentURIComponentImpl = - ERC6909ContentURIComponent::ERC6909ContentURIImpl; - - // 4. Implement internal implementations to access internal functions - impl ERC6909InternalImpl = ERC6909Component::InternalImpl; - impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; - - // 5. Include component storage and events - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage, - #[substorage(v0)] - erc6909_content_uri: ERC6909ContentURIComponent::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event, - #[flat] - ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, - } - - // 6. Initialize contract URI in the constructor via the component's internal `initializer` function - #[constructor] - fn constructor( - ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray - ) { - self.erc6909.mint(receiver, id, amount); - self.erc6909_content_uri.initializer(uri); - } -} ----- - -There's a few things happening in our contract so let's go from the beginning. - -To include the URI extension we must import the `ERC6909ContentURI` component (along with the `ERC6909` base component). - -The `ERC6909Component` always requires us to implement the hooks, we are simply importing the `ERC6909HooksEmptyImpl` as we do not -require any hooks for our token, so importing empty hooks suffices in this case. - -Once imported, we declare both components with `component!(path, storage, events)`. -This tells the compiler to generate an implementation for `HasComponent`, constructing the component state from the associated storage and event types (step 5). - -We then embed the ABI for both components so each function in the implementation is now accessible externally, and the impl/interface are reflected in the ABI. -Notice that we are also implementing the `ERC6909InternalImpl` and `ERC6909COntentURIInternalImpl` to access the internal functions of each component (such as `mint` or `initializer`). - -Finally, in the constructor we mint an initial token supply to `receiver` and set the contract URI via `ERC6909ContentURIComponent` initializer. Notice that the `initializer` -function is called in the constructor in this case, but since it is an internal function it can be called anytime, however it is usually recommended to set it once in the -constructor to not be accessible again. - -== ERC6909 Metadata - -Now let's say we want to add Metadata to our token. To do this we can import the `ERC6909MetadataComponent`. Since ERC6909 is a multi-token standard, -each token ID can have different metadata associated with it! - -To set the individual token IDs metadata we have two options: - -* Set the metadata during mints via hooks -* Set the metadata for each token manually - -The easiest way to set the metadata is via hooks. To do so, we import the `ERC6909MetadataComponent` and follow the same steps as above, with one small -exception: We do not import the `ERC6909EmptyHooksImpl` and instead we define the logic ourselves. Here's what it would look like: - -[,cairo] ----- -#[starknet::contract] -pub mod MyERC6909TokenMetadata { - // 1. Import the Metadata Component - use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; - use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; - use starknet::ContractAddress; - - // 2. Declare the component to access its storage and events - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - component!( - path: ERC6909ContentURIComponent, - storage: erc6909_content_uri, - event: ERC6909ContentURIEvent - ); - component!( - path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent - ); - - // 3. Embed ABI to access external functions - #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - #[abi(embed_v0)] - impl ERC6909ContentURIComponentImpl = - ERC6909ContentURIComponent::ERC6909ContentURIImpl; - #[abi(embed_v0)] - impl ERC6909MetadataComponentImpl = - ERC6909MetadataComponent::ERC6909MetadataImpl; - - // 4. Implement internal implementations to access internal functions - impl ERC6909InternalImpl = ERC6909Component::InternalImpl; - impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; - impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; - - // 5. Include component storage and events - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage, - #[substorage(v0)] - erc6909_content_uri: ERC6909ContentURIComponent::Storage, - #[substorage(v0)] - erc6909_metadata: ERC6909MetadataComponent::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event, - #[flat] - ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, - #[flat] - ERC6909MetadataEvent: ERC6909MetadataComponent::Event, - } - - #[constructor] - fn constructor( - ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray - ) { - self.erc6909.mint(receiver, id, amount); - self.erc6909_content_uri.initializer(uri); - } - - // 6. Implement the hook to set update metadata upon mints - impl ERC6909HooksImpl< - TContractState, - impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, - impl HasComponent: ERC6909Component::HasComponent, - +Drop - > of ERC6909Component::ERC6909HooksTrait { - fn before_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) {} - - fn after_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) { - let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); - - let name = "MyERC6909Token"; - let symbol = "MET"; - let decimals = 18; - - erc6909_metadata_component._update_token_metadata(from, id, name, symbol, decimals); - } - } -} ----- - -The `ERC6909Metadata` component has a function to check and update metadata if it hasn't been set yet. The `_update_token_metadata` -updates token metadata only upon mints, not transfers or burns. Thus while minting a new token ID, if it has not metadata associated with it -we can make use of the `after_update` hook to set the new metadata. - -In this case we used a fixed name and symbol, but during the hook you could define your own logic. For example, if the underlying deposit -is something like an LP Token, you could get the symbol of each token in the LP and use both as symbol, etc. - -The rest of the contract is identical to the `ContentURI` implementation shown above. - -== ERC6909 Token Supply - -Keeping track of each token ID supply in our ERC6909 contract is also possible by importing the `ERC6909TokenSupplyComponent` extension . The mechanism is the same as -the `ERC6909Metadata` implementation. - -The `ERC6909TokenSupplyComponent` implementation has a function to be used in the ERC6909 hooks to update supply upon mints and burns. - -Here is an example of how to implement it: - -[,cairo] ----- -#[starknet::contract] -pub mod MyERC6909TokenTotalSupply { - // 1. Import the Metadata Component - use openzeppelin::token::erc6909::ERC6909Component; - use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; - use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; - use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; - use starknet::ContractAddress; - - // 2. Declare the component to access its storage and events - component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); - component!( - path: ERC6909ContentURIComponent, - storage: erc6909_content_uri, - event: ERC6909ContentURIEvent - ); - component!( - path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent - ); - component!( - path: ERC6909TokenSupplyComponent, - storage: erc6909_token_supply, - event: ERC6909TokenSupplyEvent - ); - - // 3. Embed ABI to access external functions - #[abi(embed_v0)] - impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; - #[abi(embed_v0)] - impl ERC6909ContentURIComponentImpl = - ERC6909ContentURIComponent::ERC6909ContentURIImpl; - #[abi(embed_v0)] - impl ERC6909MetadataComponentImpl = - ERC6909MetadataComponent::ERC6909MetadataImpl; - #[abi(embed_v0)] - impl ERC6909TokenSupplyComponentImpl = - ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; - - // 4. Implement internal implementations to access internal functions - impl ERC6909InternalImpl = ERC6909Component::InternalImpl; - impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; - impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; - impl ERC6909TokenSuppplyInternalImpl = ERC6909TokenSupplyComponent::InternalImpl; - - // 5. Include component storage and events - #[storage] - struct Storage { - #[substorage(v0)] - erc6909: ERC6909Component::Storage, - #[substorage(v0)] - erc6909_content_uri: ERC6909ContentURIComponent::Storage, - #[substorage(v0)] - erc6909_metadata: ERC6909MetadataComponent::Storage, - #[substorage(v0)] - erc6909_token_supply: ERC6909TokenSupplyComponent::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC6909Event: ERC6909Component::Event, - #[flat] - ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, - #[flat] - ERC6909MetadataEvent: ERC6909MetadataComponent::Event, - #[flat] - ERC6909TokenSupplyEvent: ERC6909TokenSupplyComponent::Event, - } - - #[constructor] - fn constructor( - ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray - ) { - self.erc6909.mint(receiver, id, amount); - self.erc6909_content_uri.initializer(uri); - } - - // 6. Implement the hook to update total supply upon mints and burns - impl ERC6909HooksImpl< - TContractState, - impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, - impl ERC6909TokenSupply: ERC6909TokenSupplyComponent::HasComponent, - impl HasComponent: ERC6909Component::HasComponent, - +Drop - > of ERC6909Component::ERC6909HooksTrait { - fn before_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) {} - - fn after_update( - ref self: ERC6909Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - id: u256, - amount: u256 - ) { - let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); - erc6909_metadata_component - ._update_token_metadata(from, id, "MyERC6909Token", "MET", 18); - - let mut erc6909_token_supply_component = get_dep_component_mut!( - ref self, ERC6909TokenSupply - ); - erc6909_token_supply_component._update_token_supply(from, recipient, id, amount); - } - } -} ----- - -The logic is the exact same as when implementing the Metadata component. The `ERC6909TokenSupplyComponent` has an internal -function (`_update_token_supply`) which updates the supply of a token ID only upon mints and/or burns.