From f9735692b077c13202034ff2aec3ba4709b3afb7 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/32] erc6909 cairo contracts and tests --- src/tests/mocks.cairo | 1 + src/tests/mocks/erc6909_mocks.cairo | 284 +++++++++ src/tests/token.cairo | 1 + src/tests/token/erc6909.cairo | 4 + src/tests/token/erc6909/common.cairo | 90 +++ src/tests/token/erc6909/test_dual6909.cairo | 260 ++++++++ src/tests/token/erc6909/test_erc6909.cairo | 541 ++++++++++++++++ src/token.cairo | 1 + src/token/erc6909.cairo | 8 + src/token/erc6909/dual6909.cairo | 137 ++++ src/token/erc6909/erc6909.cairo | 662 ++++++++++++++++++++ src/token/erc6909/interface.cairo | 238 +++++++ src/utils/selectors.cairo | 14 + 13 files changed, 2241 insertions(+) create mode 100644 src/tests/mocks/erc6909_mocks.cairo create mode 100644 src/tests/token/erc6909.cairo create mode 100644 src/tests/token/erc6909/common.cairo create mode 100644 src/tests/token/erc6909/test_dual6909.cairo create mode 100644 src/tests/token/erc6909/test_erc6909.cairo create mode 100644 src/token/erc6909.cairo create mode 100644 src/token/erc6909/dual6909.cairo create mode 100644 src/token/erc6909/erc6909.cairo create mode 100644 src/token/erc6909/interface.cairo diff --git a/src/tests/mocks.cairo b/src/tests/mocks.cairo index 6e6e38156..a425440a9 100644 --- a/src/tests/mocks.cairo +++ b/src/tests/mocks.cairo @@ -4,6 +4,7 @@ pub(crate) mod erc1155_mocks; pub(crate) mod erc1155_receiver_mocks; pub(crate) mod erc20_mocks; pub(crate) mod erc20_votes_mocks; +pub(crate) mod erc6909_mocks; pub(crate) mod erc721_mocks; pub(crate) mod erc721_receiver_mocks; pub(crate) mod eth_account_mocks; diff --git a/src/tests/mocks/erc6909_mocks.cairo b/src/tests/mocks/erc6909_mocks.cairo new file mode 100644 index 000000000..d600ce077 --- /dev/null +++ b/src/tests/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/src/tests/token.cairo b/src/tests/token.cairo index 04f631ea8..feee6ff4b 100644 --- a/src/tests/token.cairo +++ b/src/tests/token.cairo @@ -1,3 +1,4 @@ pub(crate) mod erc1155; pub(crate) mod erc20; +pub(crate) mod erc6909; pub(crate) mod erc721; diff --git a/src/tests/token/erc6909.cairo b/src/tests/token/erc6909.cairo new file mode 100644 index 000000000..1bc5001cc --- /dev/null +++ b/src/tests/token/erc6909.cairo @@ -0,0 +1,4 @@ +pub(crate) mod common; + +mod test_dual6909; +mod test_erc6909; diff --git a/src/tests/token/erc6909/common.cairo b/src/tests/token/erc6909/common.cairo new file mode 100644 index 000000000..0c8fdba98 --- /dev/null +++ b/src/tests/token/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/src/tests/token/erc6909/test_dual6909.cairo b/src/tests/token/erc6909/test_dual6909.cairo new file mode 100644 index 000000000..82b4d38cc --- /dev/null +++ b/src/tests/token/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/src/tests/token/erc6909/test_erc6909.cairo b/src/tests/token/erc6909/test_erc6909.cairo new file mode 100644 index 000000000..0bfae9eeb --- /dev/null +++ b/src/tests/token/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())); +} diff --git a/src/token.cairo b/src/token.cairo index afe47f7ac..d3fe12656 100644 --- a/src/token.cairo +++ b/src/token.cairo @@ -1,3 +1,4 @@ pub mod erc1155; pub mod erc20; +pub mod erc6909; pub mod erc721; diff --git a/src/token/erc6909.cairo b/src/token/erc6909.cairo new file mode 100644 index 000000000..c5fe1ce7a --- /dev/null +++ b/src/token/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/src/token/erc6909/dual6909.cairo b/src/token/erc6909/dual6909.cairo new file mode 100644 index 000000000..470051f52 --- /dev/null +++ b/src/token/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/src/token/erc6909/erc6909.cairo b/src/token/erc6909/erc6909.cairo new file mode 100644 index 000000000..acea3ae52 --- /dev/null +++ b/src/token/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/src/token/erc6909/interface.cairo b/src/token/erc6909/interface.cairo new file mode 100644 index 000000000..6fd6685dc --- /dev/null +++ b/src/token/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/src/utils/selectors.cairo b/src/utils/selectors.cairo index 61e9758e0..dcb49b7f9 100644 --- a/src/utils/selectors.cairo +++ b/src/utils/selectors.cairo @@ -102,3 +102,17 @@ pub const getPublicKey: felt252 = selector!("getPublicKey"); pub const is_valid_signature: felt252 = selector!("is_valid_signature"); pub const isValidSignature: felt252 = selector!("isValidSignature"); pub const supports_interface: felt252 = selector!("supports_interface"); + +// +// ERC6909 +// + +// The following ERC20 selectors are already defined above: +// name, symbol, balance_of, balanceOf, transfer_from, transferFrom, approve, +// totalSupply, total_supply, allowance, transfer, supports_interface + +pub const is_operator: felt252 = selector!("is_operator"); +pub const isOperator: felt252 = selector!("isOperator"); +pub const set_operator: felt252 = selector!("set_operator"); +pub const setOperator: felt252 = selector!("setOperator"); +pub const supportsInterface: felt252 = selector!("supportsInterface"); From 771219963349f6e07033e1feaa72675c883ac2ac 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/32] add docs and changelog --- CHANGELOG.md | 8 + docs/modules/ROOT/nav.adoc | 2 + docs/modules/ROOT/pages/api/erc6909.adoc | 748 +++++++++++++++++++++++ docs/modules/ROOT/pages/erc6909.adoc | 232 +++++++ src/token/erc6909/erc6909.cairo | 18 +- 5 files changed, 1007 insertions(+), 1 deletion(-) 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 562be9606..91b1eef43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added (2024-06-21) + +- ERC6909 token standard (2024-06-21) in `/src/token/` +- ERC6909 mocks in `/src/tests/mocks/` +- ERC6909 tests in `/src/tests/token/erc6909/` +- New selectors for the ERC6909 standard on `src/utils/selectors.cairo` +- Docs page for ERC6909 in `/docs/` + ## 0.14.0 (2024-06-14) ### Changed (Breaking) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 692ec52c5..270b72e91 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -34,6 +34,8 @@ **** xref:/api/erc721.adoc[API Reference] *** xref:erc1155.adoc[ERC1155] **** xref:/api/erc1155.adoc[API Reference] +*** xref:erc6909.adoc[ERC6909] +**** xref:/api/erc6909.adoc[API Reference] ** xref:udc.adoc[Universal Deployer Contract] *** xref:/api/udc.adoc[API Reference] 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/src/token/erc6909/erc6909.cairo b/src/token/erc6909/erc6909.cairo index acea3ae52..d2b3d3f71 100644 --- a/src/token/erc6909/erc6909.cairo +++ b/src/token/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 bd82b63ba8598bc2edfc6a3f8dc7272bb90c2780 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/32] edit `/docs/` --- docs/modules/ROOT/pages/erc6909.adoc | 25 ++++++++++++++----------- src/token/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/src/token/erc6909/erc6909.cairo b/src/token/erc6909/erc6909.cairo index d2b3d3f71..cbf565c7f 100644 --- a/src/token/erc6909/erc6909.cairo +++ b/src/token/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 0a3b145285055e3276ba7f5db3e3382dc21ec8d2 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/32] 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 cea45a900bce768a4ce640e146350e71bd57c38e 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/32] 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 --- src/tests/mocks/erc6909_mocks.cairo | 30 +--- src/token/erc6909.cairo | 1 + src/token/erc6909/erc6909.cairo | 150 ++---------------- src/token/erc6909/extensions.cairo | 7 + .../extensions/erc6909_content_uri.cairo | 60 +++++++ .../erc6909/extensions/erc6909_metadata.cairo | 76 +++++++++ .../extensions/erc6909_token_supply.cairo | 71 +++++++++ src/token/erc6909/interface.cairo | 76 ++++----- 8 files changed, 267 insertions(+), 204 deletions(-) create mode 100644 src/token/erc6909/extensions.cairo create mode 100644 src/token/erc6909/extensions/erc6909_content_uri.cairo create mode 100644 src/token/erc6909/extensions/erc6909_metadata.cairo create mode 100644 src/token/erc6909/extensions/erc6909_token_supply.cairo diff --git a/src/tests/mocks/erc6909_mocks.cairo b/src/tests/mocks/erc6909_mocks.cairo index d600ce077..3091f38a9 100644 --- a/src/tests/mocks/erc6909_mocks.cairo +++ b/src/tests/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/src/token/erc6909.cairo b/src/token/erc6909.cairo index c5fe1ce7a..00d355fe5 100644 --- a/src/token/erc6909.cairo +++ b/src/token/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/src/token/erc6909/erc6909.cairo b/src/token/erc6909/erc6909.cairo index cbf565c7f..563209c5f 100644 --- a/src/token/erc6909/erc6909.cairo +++ b/src/token/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/src/token/erc6909/extensions.cairo b/src/token/erc6909/extensions.cairo new file mode 100644 index 000000000..edbe7bead --- /dev/null +++ b/src/token/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/src/token/erc6909/extensions/erc6909_content_uri.cairo b/src/token/erc6909/extensions/erc6909_content_uri.cairo new file mode 100644 index 000000000..09e724e59 --- /dev/null +++ b/src/token/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/src/token/erc6909/extensions/erc6909_metadata.cairo b/src/token/erc6909/extensions/erc6909_metadata.cairo new file mode 100644 index 000000000..68d9345d7 --- /dev/null +++ b/src/token/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/src/token/erc6909/extensions/erc6909_token_supply.cairo b/src/token/erc6909/extensions/erc6909_token_supply.cairo new file mode 100644 index 000000000..352265ef6 --- /dev/null +++ b/src/token/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/src/token/erc6909/interface.cairo b/src/token/erc6909/interface.cairo index 6fd6685dc..0b0c44485 100644 --- a/src/token/erc6909/interface.cairo +++ b/src/token/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 49d436995fd770a36a4d977ee234973b0848ceeb 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/32] 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 79363c7c459f71a9280f17d71e3a6afd1db5176f 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/32] update token --- src/token/erc6909/erc6909.cairo | 18 ++++++++++++------ .../extensions/erc6909_token_supply.cairo | 1 - 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/token/erc6909/erc6909.cairo b/src/token/erc6909/erc6909.cairo index 563209c5f..5e4e8c745 100644 --- a/src/token/erc6909/erc6909.cairo +++ b/src/token/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/src/token/erc6909/extensions/erc6909_token_supply.cairo b/src/token/erc6909/extensions/erc6909_token_supply.cairo index 352265ef6..c2713997c 100644 --- a/src/token/erc6909/extensions/erc6909_token_supply.cairo +++ b/src/token/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 704748622372cae25195c20d7d4c8309aae857a1 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/32] update token tests --- src/tests/token/erc6909.cairo | 2 +- src/tests/token/erc6909/test_erc6909.cairo | 77 ++++++++++------------ 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/src/tests/token/erc6909.cairo b/src/tests/token/erc6909.cairo index 1bc5001cc..8a82f2b2a 100644 --- a/src/tests/token/erc6909.cairo +++ b/src/tests/token/erc6909.cairo @@ -1,4 +1,4 @@ pub(crate) mod common; -mod test_dual6909; +// mod test_dual6909; mod test_erc6909; diff --git a/src/tests/token/erc6909/test_erc6909.cairo b/src/tests/token/erc6909/test_erc6909.cairo index 0bfae9eeb..86084145d 100644 --- a/src/tests/token/erc6909/test_erc6909.cairo +++ b/src/tests/token/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 3b8b627dd504fe04431f4cc664a30408f1b66b4c 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/32] add `token_supply` mocoks --- src/tests/mocks/erc6909_mocks.cairo | 94 +++++-------------- .../mocks/erc6909_token_supply_mocks.cairo | 83 ++++++++++++++++ 2 files changed, 107 insertions(+), 70 deletions(-) create mode 100644 src/tests/mocks/erc6909_token_supply_mocks.cairo diff --git a/src/tests/mocks/erc6909_mocks.cairo b/src/tests/mocks/erc6909_mocks.cairo index 3091f38a9..0e62ac6f7 100644 --- a/src/tests/mocks/erc6909_mocks.cairo +++ b/src/tests/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/src/tests/mocks/erc6909_token_supply_mocks.cairo b/src/tests/mocks/erc6909_token_supply_mocks.cairo new file mode 100644 index 000000000..cfa52733e --- /dev/null +++ b/src/tests/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 f5fa462d2e0b23ae33128d0fe36346e9455e02a3 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/32] refactor dual mocks and tests --- src/tests/mocks/erc6909_mocks.cairo | 42 ++++++++++++++++++++++++++--- src/tests/token/erc6909.cairo | 2 +- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/tests/mocks/erc6909_mocks.cairo b/src/tests/mocks/erc6909_mocks.cairo index 0e62ac6f7..fbc7283bc 100644 --- a/src/tests/mocks/erc6909_mocks.cairo +++ b/src/tests/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/src/tests/token/erc6909.cairo b/src/tests/token/erc6909.cairo index 8a82f2b2a..1bc5001cc 100644 --- a/src/tests/token/erc6909.cairo +++ b/src/tests/token/erc6909.cairo @@ -1,4 +1,4 @@ pub(crate) mod common; -// mod test_dual6909; +mod test_dual6909; mod test_erc6909; From 641f33ff4eaaf5ad64e42e52455f49cdec26b249 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/32] add token supply tests --- src/tests.cairo | 28 +-- src/tests/mocks.cairo | 33 ++-- src/tests/mocks/erc6909_mocks.cairo | 11 +- .../mocks/erc6909_token_supply_mocks.cairo | 29 ++-- src/tests/token.cairo | 8 +- src/tests/token/erc6909.cairo | 1 + .../erc6909/test_erc6909_token_supply.cairo | 164 ++++++++++++++++++ .../extensions/erc6909_token_supply.cairo | 9 +- 8 files changed, 234 insertions(+), 49 deletions(-) create mode 100644 src/tests/token/erc6909/test_erc6909_token_supply.cairo diff --git a/src/tests.cairo b/src/tests.cairo index 1f53e79de..997149540 100644 --- a/src/tests.cairo +++ b/src/tests.cairo @@ -1,20 +1,20 @@ -#[cfg(test)] -mod access; -#[cfg(test)] -mod account; -#[cfg(test)] -mod cryptography; -#[cfg(test)] -mod introspection; +// #[cfg(test)] +// mod access; +// #[cfg(test)] +// mod account; +// #[cfg(test)] +// mod cryptography; +// #[cfg(test)] +// mod introspection; #[cfg(test)] mod mocks; -#[cfg(test)] -mod presets; -#[cfg(test)] -mod security; +// #[cfg(test)] +// mod presets; +// #[cfg(test)] +// mod security; #[cfg(test)] mod token; -#[cfg(test)] -mod upgrades; +// #[cfg(test)] +// mod upgrades; pub mod utils; diff --git a/src/tests/mocks.cairo b/src/tests/mocks.cairo index a425440a9..87f3574fc 100644 --- a/src/tests/mocks.cairo +++ b/src/tests/mocks.cairo @@ -1,18 +1,19 @@ -pub(crate) mod accesscontrol_mocks; -pub(crate) mod account_mocks; -pub(crate) mod erc1155_mocks; -pub(crate) mod erc1155_receiver_mocks; -pub(crate) mod erc20_mocks; -pub(crate) mod erc20_votes_mocks; +// pub(crate) mod nonces_mocks; +// pub(crate) mod ownable_mocks; +// pub(crate) mod pausable_mocks; +// pub(crate) mod reentrancy_mocks; +// pub(crate) mod src5_mocks; +// pub(crate) mod upgrades_mocks; pub(crate) mod erc6909_mocks; -pub(crate) mod erc721_mocks; -pub(crate) mod erc721_receiver_mocks; -pub(crate) mod eth_account_mocks; -pub(crate) mod initializable_mocks; +pub(crate) mod erc6909_token_supply_mocks; +// pub(crate) mod accesscontrol_mocks; +// pub(crate) mod account_mocks; +// pub(crate) mod erc1155_mocks; +// pub(crate) mod erc1155_receiver_mocks; +// pub(crate) mod erc20_mocks; +// pub(crate) mod erc20_votes_mocks; +// pub(crate) mod erc721_mocks; +// pub(crate) mod erc721_receiver_mocks; +// pub(crate) mod eth_account_mocks; +// pub(crate) mod initializable_mocks; pub(crate) mod non_implementing_mock; -pub(crate) mod nonces_mocks; -pub(crate) mod ownable_mocks; -pub(crate) mod pausable_mocks; -pub(crate) mod reentrancy_mocks; -pub(crate) mod src5_mocks; -pub(crate) mod upgrades_mocks; diff --git a/src/tests/mocks/erc6909_mocks.cairo b/src/tests/mocks/erc6909_mocks.cairo index fbc7283bc..460a11502 100644 --- a/src/tests/mocks/erc6909_mocks.cairo +++ b/src/tests/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/src/tests/mocks/erc6909_token_supply_mocks.cairo b/src/tests/mocks/erc6909_token_supply_mocks.cairo index cfa52733e..18bc7cd76 100644 --- a/src/tests/mocks/erc6909_token_supply_mocks.cairo +++ b/src/tests/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/src/tests/token.cairo b/src/tests/token.cairo index feee6ff4b..fd35f4364 100644 --- a/src/tests/token.cairo +++ b/src/tests/token.cairo @@ -1,4 +1,6 @@ -pub(crate) mod erc1155; -pub(crate) mod erc20; +// pub(crate) mod erc1155; +// pub(crate) mod erc20; pub(crate) mod erc6909; -pub(crate) mod erc721; +// pub(crate) mod erc721; + + diff --git a/src/tests/token/erc6909.cairo b/src/tests/token/erc6909.cairo index 1bc5001cc..9a12c6deb 100644 --- a/src/tests/token/erc6909.cairo +++ b/src/tests/token/erc6909.cairo @@ -2,3 +2,4 @@ pub(crate) mod common; mod test_dual6909; mod test_erc6909; +mod test_erc6909_token_supply; diff --git a/src/tests/token/erc6909/test_erc6909_token_supply.cairo b/src/tests/token/erc6909/test_erc6909_token_supply.cairo new file mode 100644 index 000000000..2910faa38 --- /dev/null +++ b/src/tests/token/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); +} diff --git a/src/token/erc6909/extensions/erc6909_token_supply.cairo b/src/token/erc6909/extensions/erc6909_token_supply.cairo index c2713997c..b8eb0e17c 100644 --- a/src/token/erc6909/extensions/erc6909_token_supply.cairo +++ b/src/token/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, From fa621c0b677095954f67bf6d6723e4a09ea39bbe 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/32] add token supply and content uri tests --- src/tests/mocks.cairo | 2 + .../mocks/erc6909_content_uri_mocks.cairo | 49 ++++++++ src/tests/mocks/erc6909_metadata_mocks.cairo | 43 +++++++ .../mocks/erc6909_token_supply_mocks.cairo | 15 --- src/tests/token/erc6909.cairo | 2 + .../erc6909/test_erc6909_content_uri.cairo | 105 +++++++++++++++++ .../token/erc6909/test_erc6909_metadata.cairo | 110 ++++++++++++++++++ .../erc6909/test_erc6909_token_supply.cairo | 12 +- .../extensions/erc6909_content_uri.cairo | 4 +- 9 files changed, 319 insertions(+), 23 deletions(-) create mode 100644 src/tests/mocks/erc6909_content_uri_mocks.cairo create mode 100644 src/tests/mocks/erc6909_metadata_mocks.cairo create mode 100644 src/tests/token/erc6909/test_erc6909_content_uri.cairo create mode 100644 src/tests/token/erc6909/test_erc6909_metadata.cairo diff --git a/src/tests/mocks.cairo b/src/tests/mocks.cairo index 87f3574fc..c70407e79 100644 --- a/src/tests/mocks.cairo +++ b/src/tests/mocks.cairo @@ -1,3 +1,5 @@ +pub(crate) mod erc6909_content_uri_mocks; +pub(crate) mod erc6909_metadata_mocks; // pub(crate) mod nonces_mocks; // pub(crate) mod ownable_mocks; // pub(crate) mod pausable_mocks; diff --git a/src/tests/mocks/erc6909_content_uri_mocks.cairo b/src/tests/mocks/erc6909_content_uri_mocks.cairo new file mode 100644 index 000000000..2b0760bbd --- /dev/null +++ b/src/tests/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/src/tests/mocks/erc6909_metadata_mocks.cairo b/src/tests/mocks/erc6909_metadata_mocks.cairo new file mode 100644 index 000000000..e48503224 --- /dev/null +++ b/src/tests/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/src/tests/mocks/erc6909_token_supply_mocks.cairo b/src/tests/mocks/erc6909_token_supply_mocks.cairo index 18bc7cd76..e15b5eb54 100644 --- a/src/tests/mocks/erc6909_token_supply_mocks.cairo +++ b/src/tests/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/src/tests/token/erc6909.cairo b/src/tests/token/erc6909.cairo index 9a12c6deb..8d9685af3 100644 --- a/src/tests/token/erc6909.cairo +++ b/src/tests/token/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/src/tests/token/erc6909/test_erc6909_content_uri.cairo b/src/tests/token/erc6909/test_erc6909_content_uri.cairo new file mode 100644 index 000000000..770e84cdd --- /dev/null +++ b/src/tests/token/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/src/tests/token/erc6909/test_erc6909_metadata.cairo b/src/tests/token/erc6909/test_erc6909_metadata.cairo new file mode 100644 index 000000000..a7390310d --- /dev/null +++ b/src/tests/token/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/src/tests/token/erc6909/test_erc6909_token_supply.cairo b/src/tests/token/erc6909/test_erc6909_token_supply.cairo index 2910faa38..4bdf62dcf 100644 --- a/src/tests/token/erc6909/test_erc6909_token_supply.cairo +++ b/src/tests/token/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); diff --git a/src/token/erc6909/extensions/erc6909_content_uri.cairo b/src/token/erc6909/extensions/erc6909_content_uri.cairo index 09e724e59..32ab38bd4 100644 --- a/src/token/erc6909/extensions/erc6909_content_uri.cairo +++ b/src/token/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); } } From 7802429b80ca6a97fb5f08f7ba2e3d3542eb2702 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/32] 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/src/tests/mocks/erc6909_token_supply_mocks.cairo b/src/tests/mocks/erc6909_token_supply_mocks.cairo index e15b5eb54..23ef59db6 100644 --- a/src/tests/mocks/erc6909_token_supply_mocks.cairo +++ b/src/tests/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 b2fecad4bed9011558ff6b603069170dcd8d96f0 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/32] 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/src/token/erc6909/extensions/erc6909_content_uri.cairo b/src/token/erc6909/extensions/erc6909_content_uri.cairo index 32ab38bd4..2a0948c52 100644 --- a/src/token/erc6909/extensions/erc6909_content_uri.cairo +++ b/src/token/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 e30797a970b6985020b4e7ad04181ae764a13c55 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/32] add metadata hook --- src/tests/mocks/erc6909_metadata_mocks.cairo | 35 ++++++++++++- .../token/erc6909/test_erc6909_metadata.cairo | 12 ++--- .../erc6909/extensions/erc6909_metadata.cairo | 51 +++++++++++++++++++ 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/tests/mocks/erc6909_metadata_mocks.cairo b/src/tests/mocks/erc6909_metadata_mocks.cairo index e48503224..b0eb84306 100644 --- a/src/tests/mocks/erc6909_metadata_mocks.cairo +++ b/src/tests/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/src/tests/token/erc6909/test_erc6909_metadata.cairo b/src/tests/token/erc6909/test_erc6909_metadata.cairo index a7390310d..5bbcc101e 100644 --- a/src/tests/token/erc6909/test_erc6909_metadata.cairo +++ b/src/tests/token/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() { diff --git a/src/token/erc6909/extensions/erc6909_metadata.cairo b/src/token/erc6909/extensions/erc6909_metadata.cairo index 68d9345d7..3acd98bab 100644 --- a/src/token/erc6909/extensions/erc6909_metadata.cairo +++ b/src/token/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); From 5094c6698bb06adde43e8149e83a15fc2c24a5a0 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/32] 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/src/token/erc6909/extensions/erc6909_content_uri.cairo b/src/token/erc6909/extensions/erc6909_content_uri.cairo index 2a0948c52..f8261b14c 100644 --- a/src/token/erc6909/extensions/erc6909_content_uri.cairo +++ b/src/token/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/src/token/erc6909/extensions/erc6909_metadata.cairo b/src/token/erc6909/extensions/erc6909_metadata.cairo index 3acd98bab..0ec1ed461 100644 --- a/src/token/erc6909/extensions/erc6909_metadata.cairo +++ b/src/token/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/src/token/erc6909/extensions/erc6909_token_supply.cairo b/src/token/erc6909/extensions/erc6909_token_supply.cairo index b8eb0e17c..48ed8986c 100644 --- a/src/token/erc6909/extensions/erc6909_token_supply.cairo +++ b/src/token/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 b3a9b62aec529ccfb92753bb7aa3c186879f893e 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/32] 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 d653416bc4172307a65f0556a2a8d339891831bc 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/32] 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 d2658753e56d9702c4229a4d8a2a4362b97fa42b 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/32] make internalimpl mocks consistent --- src/tests.cairo | 28 ++++++++-------- src/tests/mocks.cairo | 32 +++++++++---------- .../mocks/erc6909_content_uri_mocks.cairo | 5 +-- src/tests/mocks/erc6909_metadata_mocks.cairo | 8 ++--- .../mocks/erc6909_token_supply_mocks.cairo | 5 +-- src/tests/token.cairo | 6 ++-- 6 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/tests.cairo b/src/tests.cairo index 997149540..1f53e79de 100644 --- a/src/tests.cairo +++ b/src/tests.cairo @@ -1,20 +1,20 @@ -// #[cfg(test)] -// mod access; -// #[cfg(test)] -// mod account; -// #[cfg(test)] -// mod cryptography; -// #[cfg(test)] -// mod introspection; +#[cfg(test)] +mod access; +#[cfg(test)] +mod account; +#[cfg(test)] +mod cryptography; +#[cfg(test)] +mod introspection; #[cfg(test)] mod mocks; -// #[cfg(test)] -// mod presets; -// #[cfg(test)] -// mod security; +#[cfg(test)] +mod presets; +#[cfg(test)] +mod security; #[cfg(test)] mod token; -// #[cfg(test)] -// mod upgrades; +#[cfg(test)] +mod upgrades; pub mod utils; diff --git a/src/tests/mocks.cairo b/src/tests/mocks.cairo index c70407e79..7c79e0cd3 100644 --- a/src/tests/mocks.cairo +++ b/src/tests/mocks.cairo @@ -1,21 +1,21 @@ pub(crate) mod erc6909_content_uri_mocks; pub(crate) mod erc6909_metadata_mocks; -// pub(crate) mod nonces_mocks; -// pub(crate) mod ownable_mocks; -// pub(crate) mod pausable_mocks; -// pub(crate) mod reentrancy_mocks; -// pub(crate) mod src5_mocks; -// pub(crate) mod upgrades_mocks; +pub(crate) mod nonces_mocks; +pub(crate) mod ownable_mocks; +pub(crate) mod pausable_mocks; +pub(crate) mod reentrancy_mocks; +pub(crate) mod src5_mocks; +pub(crate) mod upgrades_mocks; pub(crate) mod erc6909_mocks; pub(crate) mod erc6909_token_supply_mocks; -// pub(crate) mod accesscontrol_mocks; -// pub(crate) mod account_mocks; -// pub(crate) mod erc1155_mocks; -// pub(crate) mod erc1155_receiver_mocks; -// pub(crate) mod erc20_mocks; -// pub(crate) mod erc20_votes_mocks; -// pub(crate) mod erc721_mocks; -// pub(crate) mod erc721_receiver_mocks; -// pub(crate) mod eth_account_mocks; -// pub(crate) mod initializable_mocks; +pub(crate) mod accesscontrol_mocks; +pub(crate) mod account_mocks; +pub(crate) mod erc1155_mocks; +pub(crate) mod erc1155_receiver_mocks; +pub(crate) mod erc20_mocks; +pub(crate) mod erc20_votes_mocks; +pub(crate) mod erc721_mocks; +pub(crate) mod erc721_receiver_mocks; +pub(crate) mod eth_account_mocks; +pub(crate) mod initializable_mocks; pub(crate) mod non_implementing_mock; diff --git a/src/tests/mocks/erc6909_content_uri_mocks.cairo b/src/tests/mocks/erc6909_content_uri_mocks.cairo index 2b0760bbd..452d2f72f 100644 --- a/src/tests/mocks/erc6909_content_uri_mocks.cairo +++ b/src/tests/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/src/tests/mocks/erc6909_metadata_mocks.cairo b/src/tests/mocks/erc6909_metadata_mocks.cairo index b0eb84306..51a009d32 100644 --- a/src/tests/mocks/erc6909_metadata_mocks.cairo +++ b/src/tests/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/src/tests/mocks/erc6909_token_supply_mocks.cairo b/src/tests/mocks/erc6909_token_supply_mocks.cairo index 23ef59db6..e0f123575 100644 --- a/src/tests/mocks/erc6909_token_supply_mocks.cairo +++ b/src/tests/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 { diff --git a/src/tests/token.cairo b/src/tests/token.cairo index fd35f4364..9d7a11b67 100644 --- a/src/tests/token.cairo +++ b/src/tests/token.cairo @@ -1,6 +1,6 @@ -// pub(crate) mod erc1155; -// pub(crate) mod erc20; +pub(crate) mod erc1155; +pub(crate) mod erc20; pub(crate) mod erc6909; -// pub(crate) mod erc721; +pub(crate) mod erc721; From ca6c739ae0a4b3f958c8d94528688aa556f9c48e 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/32] 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 270b72e91..e86b3de34 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -35,6 +35,7 @@ *** xref:erc1155.adoc[ERC1155] **** xref:/api/erc1155.adoc[API Reference] *** xref:erc6909.adoc[ERC6909] +**** xref:/guides/erc6909-extensions.adoc[Extensions] **** xref:/api/erc6909.adoc[API Reference] ** xref:udc.adoc[Universal Deployer Contract] 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 21a28c6371b8db8d3a880885ea8f10ed1614c3f8 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/32] 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 fbae8e205605367ff35a98479e4b33a3c0314072 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/32] 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 c9ccaa779751c85815a984adbc3c25c241d1cabd Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Mon, 8 Jul 2024 22:50:12 +0200 Subject: [PATCH 23/32] remove trailing space --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b1eef43..d2670d6f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (2024-06-21) - ERC6909 token standard (2024-06-21) in `/src/token/` -- ERC6909 mocks in `/src/tests/mocks/` +- ERC6909 mocks in `/src/tests/mocks/` - ERC6909 tests in `/src/tests/token/erc6909/` -- New selectors for the ERC6909 standard on `src/utils/selectors.cairo` +- New selectors for the ERC6909 standard on `src/utils/selectors.cairo` - Docs page for ERC6909 in `/docs/` ## 0.14.0 (2024-06-14) From 3a5e4c0aa4448355d7a45917e8979f92b7c897bb 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 24/32] lint and typos --- .../ROOT/pages/guides/erc6909-extensions.adoc | 2 +- src/tests/mocks.cairo | 20 +++++++++---------- src/tests/mocks/erc6909_metadata_mocks.cairo | 3 ++- src/tests/token.cairo | 1 - src/token/erc6909/erc6909.cairo | 4 ++-- .../erc6909/extensions/erc6909_metadata.cairo | 4 +++- 6 files changed, 18 insertions(+), 16 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/src/tests/mocks.cairo b/src/tests/mocks.cairo index 7c79e0cd3..2f1fd2699 100644 --- a/src/tests/mocks.cairo +++ b/src/tests/mocks.cairo @@ -1,21 +1,21 @@ -pub(crate) mod erc6909_content_uri_mocks; -pub(crate) mod erc6909_metadata_mocks; -pub(crate) mod nonces_mocks; -pub(crate) mod ownable_mocks; -pub(crate) mod pausable_mocks; -pub(crate) mod reentrancy_mocks; -pub(crate) mod src5_mocks; -pub(crate) mod upgrades_mocks; -pub(crate) mod erc6909_mocks; -pub(crate) mod erc6909_token_supply_mocks; pub(crate) mod accesscontrol_mocks; pub(crate) mod account_mocks; pub(crate) mod erc1155_mocks; pub(crate) mod erc1155_receiver_mocks; pub(crate) mod erc20_mocks; pub(crate) mod erc20_votes_mocks; +pub(crate) mod erc6909_content_uri_mocks; +pub(crate) mod erc6909_metadata_mocks; +pub(crate) mod erc6909_mocks; +pub(crate) mod erc6909_token_supply_mocks; pub(crate) mod erc721_mocks; pub(crate) mod erc721_receiver_mocks; pub(crate) mod eth_account_mocks; pub(crate) mod initializable_mocks; pub(crate) mod non_implementing_mock; +pub(crate) mod nonces_mocks; +pub(crate) mod ownable_mocks; +pub(crate) mod pausable_mocks; +pub(crate) mod reentrancy_mocks; +pub(crate) mod src5_mocks; +pub(crate) mod upgrades_mocks; diff --git a/src/tests/mocks/erc6909_metadata_mocks.cairo b/src/tests/mocks/erc6909_metadata_mocks.cairo index 51a009d32..3071e5ddf 100644 --- a/src/tests/mocks/erc6909_metadata_mocks.cairo +++ b/src/tests/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/src/tests/token.cairo b/src/tests/token.cairo index 9d7a11b67..14e9a16a8 100644 --- a/src/tests/token.cairo +++ b/src/tests/token.cairo @@ -3,4 +3,3 @@ pub(crate) mod erc20; pub(crate) mod erc6909; pub(crate) mod erc721; - diff --git a/src/token/erc6909/erc6909.cairo b/src/token/erc6909/erc6909.cairo index 5e4e8c745..395a5d98c 100644 --- a/src/token/erc6909/erc6909.cairo +++ b/src/token/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/src/token/erc6909/extensions/erc6909_metadata.cairo b/src/token/erc6909/extensions/erc6909_metadata.cairo index 0ec1ed461..5f1e2956f 100644 --- a/src/token/erc6909/extensions/erc6909_metadata.cairo +++ b/src/token/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 6918d9056f58b0a4553017657360fd8ea7623077 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 25/32] make imports consistent --- src/token/erc6909/erc6909.cairo | 3 ++- src/token/erc6909/extensions/erc6909_token_supply.cairo | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/token/erc6909/erc6909.cairo b/src/token/erc6909/erc6909.cairo index 395a5d98c..2877885ff 100644 --- a/src/token/erc6909/erc6909.cairo +++ b/src/token/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/src/token/erc6909/extensions/erc6909_token_supply.cairo b/src/token/erc6909/extensions/erc6909_token_supply.cairo index 48ed8986c..b239a0be5 100644 --- a/src/token/erc6909/extensions/erc6909_token_supply.cairo +++ b/src/token/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 a27fe02eb680afaeff6aa78c0775ed128fae92cf 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 26/32] remove dual dispatchers and update comments --- src/token/erc6909.cairo | 5 +- src/token/erc6909/dual6909.cairo | 137 ----------------- src/token/erc6909/erc6909.cairo | 242 +++--------------------------- src/token/erc6909/interface.cairo | 184 ----------------------- 4 files changed, 22 insertions(+), 546 deletions(-) delete mode 100644 src/token/erc6909/dual6909.cairo diff --git a/src/token/erc6909.cairo b/src/token/erc6909.cairo index 00d355fe5..ce080adf5 100644 --- a/src/token/erc6909.cairo +++ b/src/token/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/src/token/erc6909/dual6909.cairo b/src/token/erc6909/dual6909.cairo deleted file mode 100644 index 470051f52..000000000 --- a/src/token/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/src/token/erc6909/erc6909.cairo b/src/token/erc6909/erc6909.cairo index 2877885ff..11ec48864 100644 --- a/src/token/erc6909/erc6909.cairo +++ b/src/token/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/src/token/erc6909/interface.cairo b/src/token/erc6909/interface.cairo index 0b0c44485..65bc5cac6 100644 --- a/src/token/erc6909/interface.cairo +++ b/src/token/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 03e577ef84d0445aa1440d5b1a235b94dbf65329 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 27/32] 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/src/token/erc6909/extensions/erc6909_content_uri.cairo b/src/token/erc6909/extensions/erc6909_content_uri.cairo index f8261b14c..52692acad 100644 --- a/src/token/erc6909/extensions/erc6909_content_uri.cairo +++ b/src/token/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/src/token/erc6909/extensions/erc6909_metadata.cairo b/src/token/erc6909/extensions/erc6909_metadata.cairo index 5f1e2956f..efa9b9f7f 100644 --- a/src/token/erc6909/extensions/erc6909_metadata.cairo +++ b/src/token/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/src/token/erc6909/extensions/erc6909_token_supply.cairo b/src/token/erc6909/extensions/erc6909_token_supply.cairo index b239a0be5..39c86c27a 100644 --- a/src/token/erc6909/extensions/erc6909_token_supply.cairo +++ b/src/token/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 543e1a6b30d20b412e43b1641c7c965f2adf6afd Mon Sep 17 00:00:00 2001 From: swan-of-bodom <0xHyoga@cygnusdao.finance> Date: Tue, 13 Aug 2024 13:26:48 +0200 Subject: [PATCH 28/32] remove tests --- src/tests/token.cairo | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 src/tests/token.cairo diff --git a/src/tests/token.cairo b/src/tests/token.cairo deleted file mode 100644 index 14e9a16a8..000000000 --- a/src/tests/token.cairo +++ /dev/null @@ -1,5 +0,0 @@ -pub(crate) mod erc1155; -pub(crate) mod erc20; -pub(crate) mod erc6909; -pub(crate) mod erc721; - From aa8e8c647fb43d5e1adf2a8d1a920653b7cc92f7 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 29/32] 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 40538f672aaef69bf8dc96d3c9a86b3d4c9abbb2 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 30/32] 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 56570aa20454d4b7f88242957c1908a7a5316776 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 31/32] 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 54d6f0572d8491fb9363c2d717e5d2fa3579239a 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 32/32] 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 ) {