diff --git a/CHANGELOG.md b/CHANGELOG.md index 5531becf4..353e5184b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +<<<<<<< HEAD +### 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.15.0 (2024-08-08) ### Added @@ -49,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (Breaking) - Removed `num_checkpoints` and `checkpoints` from `ERC20VotesABI`. +>>>>>>> 0638a4a640a19c0d93570dc4ab7324bcfe1ef1c3 ## 0.14.0 (2024-06-14) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 6eb123c17..a58a40805 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -35,6 +35,9 @@ **** xref:/api/erc721.adoc[API Reference] *** 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] *** 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..ac5c54111 --- /dev/null +++ b/docs/modules/ROOT/pages/api/erc6909.adoc @@ -0,0 +1,511 @@ +: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] + += 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, unless called by an operator. + +[.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-supports_interface]] +==== `[.contract-item-name]#++supports_interface++#++(interface_id: felt252) → bool++` [.item-kind]#external# + +Checks if a contract implements `interface_id`. + +[#IERC6909-Events] +==== Events + +[.contract-item] +[[IERC6909-Transfer]] +==== `[.contract-item-name]#++Transfer++#++(caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#event# + +Emitted when `amount` of `id` are moved from `sender` to `receiver`. + +Note that `amount` may be zero. + +[.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] +[[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++`] +-- + +[.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-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-_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] +.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-balance_of]] +==== `[.contract-item-name]#++balance_of++#++(@self: ContractState, owner: 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-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# + +See <>. + +Requirements: + +- `receiver` cannot be the zero address. +- The caller must have a balance 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 at least `amount`. +- `receiver` cannot be the zero address. +- The caller must have allowance for ``sender``'s tokens of at least `amount`. + +[.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-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# + +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, 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 + +[.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 `from` 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 `to` 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 `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 +functionality before and/or after the transfer, mint, or burn. + +Emits a <> event. + +[.contract-item] +[[ERC6909Component-_transfer]] +==== `[.contract-item-name]#++_transfer++#++(ref self: ContractState, caller: ContractAddress, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256)++` [.item-kind]#internal# + +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. + +Emits a <> event. + +Requirements: + +- `from` cannot be the zero address. +- `to` cannot be the zero address. +- `from` must have a balance of `id` tokens of at least `amount`. + +[.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` based on spent `amount` for `id` tokens. + +This internal function does not update the allowance value in the case of infinite allowance or if spender is 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, value: 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] +[[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. + diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc new file mode 100644 index 000000000..2fcb36625 --- /dev/null +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -0,0 +1,133 @@ += 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 via unique IDs. The main difference is +that in ERC6909 "the callbacks and batching have been removed from the interface and the permission system is a hybrid operator-approval +scheme for granular and scalable permissions. Functionally, the interface has been reduced to the bare minimum +required to manage multiple tokens under the same contract." {eip-6909} + +== Usage + +:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] +:erc6909-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 an example of a basic ERC6909 contract: + +[,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 + } + + #[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.mint(recipient, token_id, initial_supply); + } +} +---- + +`MyERC6909Token` integrates the `ERC6909Impl` with the embed directive which marks the implementation as external in the contract +by importing the `ERC6909Mixin` which has both camel and snake-case functions. + +The above example also includes the `ERC6909InternalImpl` instance to access internal functions (such as `mint`) + +There are 3 optional extensions which can also be imported into `MyERC6909Token`: + +* `ERC6909ContentURI` - Allows to set the base contract URI and thus show individual token URIs. +* `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. +* `ERC6909TokenSupply` - Allows to keep track of individual token supplies upon mints and burns. + +TIP: For a more complete guide on using these extensions, see {erc6909-extensions}. + +== Interface + +: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] + +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] +---- +#[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] +: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 `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. 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..31cb1cb69 --- /dev/null +++ b/docs/modules/ROOT/pages/guides/erc6909-extensions.adoc @@ -0,0 +1,351 @@ += ERC6909 Extensions + +:eip-6909: https://eips.ethereum.org/EIPS/eip-6909[EIP-6909] + +{eip-6909} is a fungible-agnostic multi-token standard, but does not define +certain characteristics typically found across fungible tokens: Such as metadata and +token supplies. + +This is why there are 3 optional extensions which can also be imported into `MyERC6909Token` out of the box to be more accessible: + +* `ERC6909ContentURI` - Allows to set the base contract URI and thus show individual token URIs. +* `ERC6909Metadata` - Allows to set the `name`, `symbol` and `decimals` of each token ID. +* `ERC6909TokenSupply` - Allows to keep track of individual token supplies upon mints and burns. + +The `ERC6909Component` always requires for hooks to be implemented. In the case of the first extension +(Content URI) simply importing the `HooksEmptyImpl` is enough. The other extensions make use of hooks +so we must implement these. + +This guide will go over these extensions and how to integrate them into your `ERC6909` contracts, with an example +for each component integration. + + +== ERC6909 Content URI + +Let's say we want to create a ERC6909 token named `MyERC6909TokenWithURI` with a contract URI. As explained the +contract URI is not part of the {eip-6909} but rather an optional extension. Therefore to achieve +this we can make use of the `ERC6909ContentURI` extension. + +[,cairo] +---- +#[starknet::contract] +pub mod MyERC6909ContentURI { + // 1. Import the Content URI Component + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; + use starknet::ContractAddress; + + // 2. Declare the component to access its storage and events + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!( + path: ERC6909ContentURIComponent, + storage: erc6909_content_uri, + event: ERC6909ContentURIEvent + ); + + // 3. Embed ABI to access external functions + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIComponentImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; + + // 4. Implement internal implementations to access internal functions + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; + + // 5. Include component storage and events + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event, + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + } + + // 6. Initialize contract URI in the constructor via the component's internal `initializer` function + #[constructor] + fn constructor( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray + ) { + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri); + } +} +---- + +There's a few things happening in our contract so let's go from the beginning. + +To include the URI extension we must import the `ERC6909ContentURI` component (along with the `ERC6909` base component). + +The `ERC6909Component` always requires us to implement the hooks, we are simply importing the `ERC6909HooksEmptyImpl` as we do not +require any hooks for our token, so importing empty hooks suffices in this case. + +Once imported, we declare both components with `component!(path, storage, events)`. +This tells the compiler to generate an implementation for `HasComponent`, constructing the component state from the associated storage and event types (step 5). + +We then embed the ABI for both components so each function in the implementation is now accessible externally, and the impl/interface are reflected in the ABI. +Notice that we are also implementing the `ERC6909InternalImpl` and `ERC6909COntentURIInternalImpl` to access the internal functions of each component (such as `mint` or `initializer`). + +Finally, in the constructor we mint an initial token supply to `receiver` and set the contract URI via `ERC6909ContentURIComponent` initializer. Notice that the `initializer` +function is called in the constructor in this case, but since it is an internal function it can be called anytime, however it is usually recommended to set it once in the +constructor to not be accessible again. + +== ERC6909 Metadata + +Now let's say we want to add Metadata to our token. To do this we can import the `ERC6909MetadataComponent`. Since ERC6909 is a multi-token standard, +each token ID can have different metadata associated with it! + +To set the individual token IDs metadata we have two options: + +* Set the metadata during mints via hooks +* Set the metadata for each token manually + +The easiest way to set the metadata is via hooks. To do so, we import the `ERC6909MetadataComponent` and follow the same steps as above, with one small +exception: We do not import the `ERC6909EmptyHooksImpl` and instead we define the logic ourselves. Here's what it would look like: + +[,cairo] +---- +#[starknet::contract] +pub mod MyERC6909TokenMetadata { + // 1. Import the Metadata Component + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; + use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; + use starknet::ContractAddress; + + // 2. Declare the component to access its storage and events + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!( + path: ERC6909ContentURIComponent, + storage: erc6909_content_uri, + event: ERC6909ContentURIEvent + ); + component!( + path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent + ); + + // 3. Embed ABI to access external functions + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIComponentImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; + #[abi(embed_v0)] + impl ERC6909MetadataComponentImpl = + ERC6909MetadataComponent::ERC6909MetadataImpl; + + // 4. Implement internal implementations to access internal functions + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; + impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; + + // 5. Include component storage and events + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + #[substorage(v0)] + erc6909_metadata: ERC6909MetadataComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event, + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + #[flat] + ERC6909MetadataEvent: ERC6909MetadataComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray + ) { + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri); + } + + // 6. Implement the hook to set update metadata upon mints + impl ERC6909HooksImpl< + TContractState, + impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, + impl HasComponent: ERC6909Component::HasComponent, + +Drop + > of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) { + let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); + + let name = "MyERC6909Token"; + let symbol = "MET"; + let decimals = 18; + + erc6909_metadata_component._update_token_metadata(from, id, name, symbol, decimals); + } + } +} +---- + +The `ERC6909Metadata` component has a function to check and update metadata if it hasn't been set yet. The `_update_token_metadata` +updates token metadata only upon mints, not transfers or burns. Thus while minting a new token ID, if it has not metadata associated with it +we can make use of the `after_update` hook to set the new metadata. + +In this case we used a fixed name and symbol, but during the hook you could define your own logic. For example, if the underlying deposit +is something like an LP Token, you could get the symbol of each token in the LP and use both as symbol, etc. + +The rest of the contract is identical to the `ContentURI` implementation shown above. + +== ERC6909 Token Supply + +Keeping track of each token ID supply in our ERC6909 contract is also possible by importing the `ERC6909TokenSupplyComponent` extension . The mechanism is the same as +the `ERC6909Metadata` implementation. + +The `ERC6909TokenSupplyComponent` implementation has a function to be used in the ERC6909 hooks to update supply upon mints and burns. + +Here is an example of how to implement it: + +[,cairo] +---- +#[starknet::contract] +pub mod MyERC6909TokenTotalSupply { + // 1. Import the Metadata Component + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; + use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; + use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; + use starknet::ContractAddress; + + // 2. Declare the component to access its storage and events + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + component!( + path: ERC6909ContentURIComponent, + storage: erc6909_content_uri, + event: ERC6909ContentURIEvent + ); + component!( + path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent + ); + component!( + path: ERC6909TokenSupplyComponent, + storage: erc6909_token_supply, + event: ERC6909TokenSupplyEvent + ); + + // 3. Embed ABI to access external functions + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + #[abi(embed_v0)] + impl ERC6909ContentURIComponentImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; + #[abi(embed_v0)] + impl ERC6909MetadataComponentImpl = + ERC6909MetadataComponent::ERC6909MetadataImpl; + #[abi(embed_v0)] + impl ERC6909TokenSupplyComponentImpl = + ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; + + // 4. Implement internal implementations to access internal functions + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; + impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; + impl ERC6909TokenSuppplyInternalImpl = ERC6909TokenSupplyComponent::InternalImpl; + + // 5. Include component storage and events + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + #[substorage(v0)] + erc6909_metadata: ERC6909MetadataComponent::Storage, + #[substorage(v0)] + erc6909_token_supply: ERC6909TokenSupplyComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event, + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + #[flat] + ERC6909MetadataEvent: ERC6909MetadataComponent::Event, + #[flat] + ERC6909TokenSupplyEvent: ERC6909TokenSupplyComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray + ) { + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri); + } + + // 6. Implement the hook to update total supply upon mints and burns + impl ERC6909HooksImpl< + TContractState, + impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, + impl ERC6909TokenSupply: ERC6909TokenSupplyComponent::HasComponent, + impl HasComponent: ERC6909Component::HasComponent, + +Drop + > of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) { + let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); + erc6909_metadata_component + ._update_token_metadata(from, id, "MyERC6909Token", "MET", 18); + + let mut erc6909_token_supply_component = get_dep_component_mut!( + ref self, ERC6909TokenSupply + ); + erc6909_token_supply_component._update_token_supply(from, recipient, id, amount); + } + } +} +---- + +The logic is the exact same as when implementing the Metadata component. The `ERC6909TokenSupplyComponent` has an internal +function (`_update_token_supply`) which updates the supply of a token ID only upon mints and/or burns. diff --git a/packages/token/src/erc6909.cairo b/packages/token/src/erc6909.cairo new file mode 100644 index 000000000..ce080adf5 --- /dev/null +++ b/packages/token/src/erc6909.cairo @@ -0,0 +1,8 @@ +pub mod erc6909; +pub mod extensions; +pub mod interface; + +pub use erc6909::ERC6909Component; +pub use erc6909::ERC6909HooksEmptyImpl; +pub use interface::IERC6909Dispatcher; +pub use interface::IERC6909DispatcherTrait; diff --git a/packages/token/src/erc6909/erc6909.cairo b/packages/token/src/erc6909/erc6909.cairo new file mode 100644 index 000000000..9171e12b5 --- /dev/null +++ b/packages/token/src/erc6909/erc6909.cairo @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/erc6909.cairo) + +use core::starknet::{ContractAddress}; + +/// TODO: ADD SRC5 As a component + +/// # ERC6909 Component +/// +/// 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::num::traits::Bounded; + use core::num::traits::Zero; + use openzeppelin_account::interface::ISRC6_ID; + use openzeppelin_token::erc6909::interface; + use starknet::storage::Map; + use starknet::{ContractAddress, get_caller_address}; + + #[storage] + struct Storage { + ERC6909_balances: Map<(ContractAddress, u256), u256>, + ERC6909_allowances: Map<(ContractAddress, ContractAddress, u256), u256>, + ERC6909_operators: Map<(ContractAddress, ContractAddress), bool>, + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event { + Transfer: Transfer, + Approval: Approval, + OperatorSet: OperatorSet + } + + /// Emitted when `id` tokens are moved from address `from` to address `to`. + #[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, + } + + /// 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] + pub owner: ContractAddress, + #[key] + pub spender: ContractAddress, + #[key] + pub id: u256, + pub amount: u256 + } + + /// Emitted when `account` enables or disables (`approved`) `spender` to manage + /// all of its assets. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct OperatorSet { + #[key] + pub owner: ContractAddress, + #[key] + pub spender: ContractAddress, + pub approved: bool, + } + + pub mod Errors { + pub const INSUFFICIENT_BALANCE: felt252 = 'ERC6909: insufficient balance'; + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'ERC6909: insufficient allowance'; + pub const TRANSFER_FROM_ZERO: felt252 = 'ERC6909: transfer from 0'; + pub const TRANSFER_TO_ZERO: felt252 = 'ERC6909: transfer to 0'; + pub const MINT_TO_ZERO: felt252 = 'ERC6909: mint to 0'; + pub const BURN_FROM_ZERO: felt252 = 'ERC6909: burn from 0'; + pub const APPROVE_FROM_ZERO: felt252 = 'ERC6909: approve from 0'; + pub const APPROVE_TO_ZERO: felt252 = 'ERC6909: approve to 0'; + } + + // + // 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> { + /// 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)) + } + + /// 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, + spender: ContractAddress, + id: u256 + ) -> u256 { + self.ERC6909_allowances.read((owner, spender, id)) + } + + /// Returns if a spender is approved by an owner as an operator + fn is_operator( + self: @ComponentState, owner: ContractAddress, spender: ContractAddress + ) -> bool { + self.ERC6909_operators.read((owner, spender)) + } + + /// Transfers an amount of an id to a receiver. + fn transfer( + ref self: ComponentState, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._transfer(caller, receiver, id, amount); + true + } + + /// Transfers an amount of an id from a sender to a receiver. + 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(sender, receiver, id, amount); + true + } + + /// Approves an amount of an id to a spender. + fn approve( + ref self: ComponentState, + spender: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._approve(caller, spender, id, amount); + true + } + + /// Sets or unsets a spender as an operator for the caller. + fn set_operator( + ref self: ComponentState, spender: ContractAddress, approved: bool + ) -> bool { + let caller = get_caller_address(); + self._set_operator(caller, spender, approved); + true + } + + /// Checks if a contract implements an interface. + fn supports_interface( + self: @ComponentState, interface_id: felt252 + ) -> bool { + interface_id == interface::IERC6909_ID || interface_id == ISRC6_ID + } + } + + + #[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(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(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. + /// + /// 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( + ref self: ComponentState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) { + Hooks::before_update(ref self, sender, receiver, id, amount); + + 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.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: get_caller_address(), sender, receiver, id, amount }); + + Hooks::after_update(ref self, sender, receiver, id, amount); + } + + /// Sets or unsets a spender as an operator for the caller. + 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 or if spender is + /// operator. + fn _spend_allowance( + ref self: ComponentState, + 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 owner != spender && !self.ERC6909_operators.read((owner, spender)) { + let sender_allowance = self.ERC6909_allowances.read((owner, spender, id)); + + if sender_allowance != Bounded::MAX { + assert(sender_allowance >= amount, Errors::INSUFFICIENT_ALLOWANCE); + self._approve(owner, 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_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 }); + } + + /// 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, + 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(sender, receiver, id, amount); + } + } +} + +/// An empty implementation of the ERC6909 hooks to be used in basic ERC6909 preset contracts. +pub impl ERC6909HooksEmptyImpl< + TContractState +> of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} +} diff --git a/packages/token/src/erc6909/extensions.cairo b/packages/token/src/erc6909/extensions.cairo new file mode 100644 index 000000000..edbe7bead --- /dev/null +++ b/packages/token/src/erc6909/extensions.cairo @@ -0,0 +1,7 @@ +pub mod erc6909_content_uri; +pub mod erc6909_metadata; +pub mod erc6909_token_supply; + +pub use erc6909_content_uri::ERC6909ContentURIComponent; +pub use erc6909_metadata::ERC6909MetadataComponent; +pub use erc6909_token_supply::ERC6909TokenSupplyComponent; diff --git a/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo new file mode 100644 index 000000000..ae8e6c70c --- /dev/null +++ b/packages/token/src/erc6909/extensions/erc6909_content_uri.cairo @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.14.0 (token/erc6909/extensions/erc6909_votes.cairo) + +use starknet::ContractAddress; + +/// # ERC6909ContentURI Component +/// +/// The ERC6909ContentURI component allows to set the contract and token ID URIs. +/// The internal function `initializer` should be used ideally in the constructor. +#[starknet::component] +pub mod ERC6909ContentURIComponent { + use openzeppelin_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> { + /// Returns the contract level URI. + fn contract_uri(self: @ComponentState) -> ByteArray { + self.ERC6909ContentURI_contract_uri.read() + } + + /// Returns the token level URI. + fn token_uri(self: @ComponentState, id: u256) -> ByteArray { + let contract_uri = self.contract_uri(); + if contract_uri.len() == 0 { + "" + } else { + 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 initializer(ref self: ComponentState, contract_uri: ByteArray) { + self.ERC6909ContentURI_contract_uri.write(contract_uri); + } + } +} + diff --git a/packages/token/src/erc6909/extensions/erc6909_metadata.cairo b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo new file mode 100644 index 000000000..d002f66b4 --- /dev/null +++ b/packages/token/src/erc6909/extensions/erc6909_metadata.cairo @@ -0,0 +1,110 @@ +// 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. +/// The internal function `_update_token_metadata` should be used inside the ERC6909 Hooks. +#[starknet::component] +pub mod ERC6909MetadataComponent { + use core::num::traits::Zero; + use openzeppelin_token::erc6909::ERC6909Component; + use openzeppelin_token::erc6909::interface; + use starknet::ContractAddress; + use starknet::storage::Map; + + #[storage] + struct Storage { + ERC6909Metadata_name: Map, + ERC6909Metadata_symbol: Map, + ERC6909Metadata_decimals: Map, + } + + #[embeddable_as(ERC6909MetadataImpl)] + impl ERC6909Metadata< + TContractState, + +HasComponent, + +ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of interface::IERC6909Metadata> { + /// Returns the name of a token ID + fn name(self: @ComponentState, id: u256) -> ByteArray { + self.ERC6909Metadata_name.read(id) + } + + /// Returns the symbol of a token ID + fn symbol(self: @ComponentState, id: u256) -> ByteArray { + self.ERC6909Metadata_symbol.read(id) + } + + /// Returns the decimals of a token ID + 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 { + /// Updates the metadata of a token ID. + fn _update_token_metadata( + ref self: ComponentState, + sender: ContractAddress, + id: u256, + name: ByteArray, + symbol: ByteArray, + decimals: u8 + ) { + // In case of new ID mints update the token metadata + 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) + } + } + } + + /// 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; + } + + /// Updates the token metadata for `id`. + 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); + } + + /// Sets the token symbol. + fn _set_token_symbol( + ref self: ComponentState, id: u256, symbol: ByteArray + ) { + self.ERC6909Metadata_symbol.write(id, symbol); + } + + /// Sets the token decimals. + fn _set_token_decimals(ref self: ComponentState, id: u256, decimals: u8) { + self.ERC6909Metadata_decimals.write(id, decimals); + } + } +} diff --git a/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo new file mode 100644 index 000000000..73ad3bbe5 --- /dev/null +++ b/packages/token/src/erc6909/extensions/erc6909_token_supply.cairo @@ -0,0 +1,72 @@ +// 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 openzeppelin_token::erc6909::ERC6909Component; + use openzeppelin_token::erc6909::interface; + use starknet::ContractAddress; + use starknet::storage::Map; + + #[storage] + struct Storage { + ERC6909TokenSupply_total_supply: Map, + } + + #[embeddable_as(ERC6909TokenSupplyImpl)] + impl ERC6909TokenSupply< + TContractState, + +HasComponent, + +ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of interface::IERC6909TokenSupply> { + /// Returns the total supply of a token. + fn total_supply(self: @ComponentState, id: u256) -> u256 { + self.ERC6909TokenSupply_total_supply.read(id) + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl ERC6909: ERC6909Component::HasComponent, + +ERC6909Component::ERC6909HooksTrait, + +Drop + > of InternalTrait { + /// 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, + receiver: ContractAddress, + id: u256, + amount: u256 + ) { + // In case of mints we increase the total supply of this token ID + 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.is_zero()) { + let total_supply = self.ERC6909TokenSupply_total_supply.read(id); + self.ERC6909TokenSupply_total_supply.write(id, total_supply - amount); + } + } + } +} diff --git a/packages/token/src/erc6909/interface.cairo b/packages/token/src/erc6909/interface.cairo new file mode 100644 index 000000000..65bc5cac6 --- /dev/null +++ b/packages/token/src/erc6909/interface.cairo @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +use starknet::ContractAddress; + +// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909.sol +pub const IERC6909_ID: felt252 = 0x32cb2c2fe3eafecaa713aaa072ee54795f66abbd45618bd0ff07284d97116ee; + +#[starknet::interface] +pub trait IERC6909 { + fn balance_of(self: @TState, owner: ContractAddress, id: u256) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress, id: u256) -> u256; + fn is_operator(self: @TState, owner: ContractAddress, spender: ContractAddress) -> bool; + fn transfer(ref self: TState, receiver: ContractAddress, id: u256, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, id: u256, amount: u256) -> bool; + fn set_operator(ref self: TState, spender: ContractAddress, approved: bool) -> bool; + fn supports_interface(self: @TState, interface_id: felt252) -> bool; +} + +// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909Metadata.sol +#[starknet::interface] +pub trait IERC6909Metadata { + fn name(self: @TState, id: u256) -> ByteArray; + fn symbol(self: @TState, id: u256) -> ByteArray; + fn decimals(self: @TState, id: u256) -> u8; +} + +// https://github.com/jtriley-eth/ERC-6909/blob/main/src/interfaces/IERC6909TokenSupply.sol +#[starknet::interface] +pub trait IERC6909TokenSupply { + fn total_supply(self: @TState, id: u256) -> u256; +} + +//https://github.com/jtriley-eth/ERC-6909/blob/main/src/ERC6909ContentURI.sol +#[starknet::interface] +pub trait IERC6909ContentURI { + fn contract_uri(self: @TState) -> ByteArray; + fn token_uri(self: @TState, id: u256) -> ByteArray; +} diff --git a/packages/token/src/lib.cairo b/packages/token/src/lib.cairo index 2c0afce45..f3bd02c65 100644 --- a/packages/token/src/lib.cairo +++ b/packages/token/src/lib.cairo @@ -1,5 +1,6 @@ pub mod erc1155; pub mod erc20; +pub mod erc6909; pub mod erc721; pub mod tests; diff --git a/packages/token/src/tests/erc6909.cairo b/packages/token/src/tests/erc6909.cairo new file mode 100644 index 000000000..8d9685af3 --- /dev/null +++ b/packages/token/src/tests/erc6909.cairo @@ -0,0 +1,7 @@ +pub(crate) mod common; + +mod test_dual6909; +mod test_erc6909; +mod test_erc6909_content_uri; +mod test_erc6909_metadata; +mod test_erc6909_token_supply; diff --git a/packages/token/src/tests/erc6909/common.cairo b/packages/token/src/tests/erc6909/common.cairo new file mode 100644 index 000000000..0c8fdba98 --- /dev/null +++ b/packages/token/src/tests/erc6909/common.cairo @@ -0,0 +1,90 @@ +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet, InternalImpl}; +use openzeppelin::token::erc6909::ERC6909Component; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; + +// Approval +pub(crate) fn assert_event_approval( + contract: ContractAddress, + owner: ContractAddress, + spender: ContractAddress, + id: u256, + amount: u256 +) { + let event = utils::pop_log::(contract).unwrap(); + let expected = ERC6909Component::Event::Approval(Approval { owner, spender, id, amount }); + assert!(event == expected); + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("Approval")); + indexed_keys.append_serde(owner); + indexed_keys.append_serde(spender); + indexed_keys.append_serde(id); + utils::assert_indexed_keys(event, indexed_keys.span()) +} + +pub(crate) fn assert_only_event_approval( + contract: ContractAddress, + owner: ContractAddress, + spender: ContractAddress, + id: u256, + amount: u256 +) { + assert_event_approval(contract, owner, spender, id, amount); + utils::assert_no_events_left(contract); +} + +// Transfer +pub(crate) fn assert_event_transfer( + contract: ContractAddress, + caller: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 +) { + let event = utils::pop_log::(contract).unwrap(); + let expected = ERC6909Component::Event::Transfer( + Transfer { caller, sender, receiver, id, amount } + ); + assert!(event == expected); + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("Transfer")); + indexed_keys.append_serde(sender); + indexed_keys.append_serde(receiver); + indexed_keys.append_serde(id); + utils::assert_indexed_keys(event, indexed_keys.span()); +} + +pub(crate) fn assert_only_event_transfer( + contract: ContractAddress, + caller: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 +) { + assert_event_transfer(contract, caller, sender, receiver, id, amount); + utils::assert_no_events_left(contract); +} + +// OperatorSet +pub(crate) fn assert_only_event_operator_set( + contract: ContractAddress, owner: ContractAddress, spender: ContractAddress, approved: bool, +) { + assert_event_operator_set(contract, owner, spender, approved); + utils::assert_no_events_left(contract); +} + +pub(crate) fn assert_event_operator_set( + contract: ContractAddress, owner: ContractAddress, spender: ContractAddress, approved: bool +) { + let event = utils::pop_log::(contract).unwrap(); + let expected = ERC6909Component::Event::OperatorSet(OperatorSet { owner, spender, approved }); + assert!(event == expected); + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("OperatorSet")); + indexed_keys.append_serde(owner); + indexed_keys.append_serde(spender); + utils::assert_indexed_keys(event, indexed_keys.span()) +} diff --git a/packages/token/src/tests/erc6909/test_dual6909.cairo b/packages/token/src/tests/erc6909/test_dual6909.cairo new file mode 100644 index 000000000..82b4d38cc --- /dev/null +++ b/packages/token/src/tests/erc6909/test_dual6909.cairo @@ -0,0 +1,260 @@ +use openzeppelin::tests::mocks::erc6909_mocks::{CamelERC6909Mock, SnakeERC6909Mock}; +use openzeppelin::tests::mocks::erc6909_mocks::{CamelERC6909Panic, SnakeERC6909Panic}; +use openzeppelin::tests::mocks::non_implementing_mock::NonImplementingMock; +use openzeppelin::tests::utils::constants::{ + OWNER, RECIPIENT, SPENDER, OPERATOR, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::dual6909::{DualCaseERC6909, DualCaseERC6909Trait}; +use openzeppelin::token::erc6909::interface::{ + IERC6909CamelDispatcher, IERC6909CamelDispatcherTrait +}; +use openzeppelin::token::erc6909::interface::{IERC6909Dispatcher, IERC6909DispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::testing::set_contract_address; + +// +// Setup +// + +pub const TOKEN_ID: u256 = 420; + +fn setup_snake() -> (DualCaseERC6909, IERC6909Dispatcher) { + let mut calldata = array![]; + calldata.append_serde(OWNER()); + calldata.append_serde(TOKEN_ID); + calldata.append_serde(SUPPLY); + let target = utils::deploy(SnakeERC6909Mock::TEST_CLASS_HASH, calldata); + (DualCaseERC6909 { contract_address: target }, IERC6909Dispatcher { contract_address: target }) +} + +fn setup_camel() -> (DualCaseERC6909, IERC6909CamelDispatcher) { + let mut calldata = array![]; + calldata.append_serde(OWNER()); + calldata.append_serde(TOKEN_ID); + calldata.append_serde(SUPPLY); + let target = utils::deploy(CamelERC6909Mock::TEST_CLASS_HASH, calldata); + ( + DualCaseERC6909 { contract_address: target }, + IERC6909CamelDispatcher { contract_address: target } + ) +} + +fn setup_non_erc6909() -> DualCaseERC6909 { + let calldata = array![]; + let target = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, calldata); + DualCaseERC6909 { contract_address: target } +} + +fn setup_erc6909_panic() -> (DualCaseERC6909, DualCaseERC6909) { + let snake_target = utils::deploy(SnakeERC6909Panic::TEST_CLASS_HASH, array![]); + let camel_target = utils::deploy(CamelERC6909Panic::TEST_CLASS_HASH, array![]); + ( + DualCaseERC6909 { contract_address: snake_target }, + DualCaseERC6909 { contract_address: camel_target } + ) +} + +// +// Case agnostic methods +// + +#[test] +fn test_dual_transfer() { + let (snake_dispatcher, snake_target) = setup_snake(); + set_contract_address(OWNER()); + assert!(snake_dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE)); + assert_eq!(snake_target.balance_of(RECIPIENT(), TOKEN_ID), VALUE); + + let (camel_dispatcher, camel_target) = setup_camel(); + set_contract_address(OWNER()); + assert!(camel_dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE)); + assert_eq!(camel_target.balanceOf(RECIPIENT(), TOKEN_ID), VALUE); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_transfer() { + let dispatcher = setup_non_erc6909(); + dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_transfer_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.transfer(RECIPIENT(), TOKEN_ID, VALUE); +} + + +#[test] +fn test_dual_approve() { + let (snake_dispatcher, snake_target) = setup_snake(); + set_contract_address(OWNER()); + assert!(snake_dispatcher.approve(SPENDER(), TOKEN_ID, VALUE)); + + let snake_allowance = snake_target.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(snake_allowance, VALUE); + + let (camel_dispatcher, camel_target) = setup_camel(); + set_contract_address(OWNER()); + assert!(camel_dispatcher.approve(SPENDER(), TOKEN_ID, VALUE)); + + let camel_allowance = camel_target.allowance(OWNER(), SPENDER(), TOKEN_ID); + assert_eq!(camel_allowance, VALUE); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_approve() { + let dispatcher = setup_non_erc6909(); + dispatcher.approve(SPENDER(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_approve_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.approve(SPENDER(), TOKEN_ID, VALUE); +} + +// +// snake_case target +// + +#[test] +fn test_dual_balance_of() { + let (dispatcher, _) = setup_snake(); + assert_eq!(dispatcher.balance_of(OWNER(), TOKEN_ID), SUPPLY); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_balance_of() { + let dispatcher = setup_non_erc6909(); + dispatcher.balance_of(OWNER(), TOKEN_ID); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_balance_of_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.balance_of(OWNER(), TOKEN_ID); +} + +#[test] +fn test_dual_transfer_from() { + let (dispatcher, target) = setup_snake(); + set_contract_address(OWNER()); + target.approve(OPERATOR(), TOKEN_ID, VALUE); + + set_contract_address(OPERATOR()); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + assert_eq!(target.balance_of(RECIPIENT(), TOKEN_ID), VALUE); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_transfer_from() { + let dispatcher = setup_non_erc6909(); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_transfer_from_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); +} + +// set_operator +#[test] +fn test_dual_set_operator() { + let (dispatcher, target) = setup_snake(); + set_contract_address(OWNER()); + target.set_operator(OPERATOR(), true); + + set_contract_address(OPERATOR()); + assert!(dispatcher.is_operator(OWNER(), OPERATOR())); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_set_operator() { + let dispatcher = setup_non_erc6909(); + dispatcher.set_operator(OPERATOR(), true); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_set_operator_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.set_operator(OPERATOR(), true); +} + +// is_operator +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_is_operator() { + let dispatcher = setup_non_erc6909(); + dispatcher.is_operator(OWNER(), OPERATOR()); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_is_operator_exists_and_panics() { + let (dispatcher, _) = setup_erc6909_panic(); + dispatcher.is_operator(OWNER(), OPERATOR()); +} + +// +// camelCase target +// + +#[test] +fn test_dual_balanceOf() { + let (dispatcher, _) = setup_camel(); + assert_eq!(dispatcher.balance_of(OWNER(), TOKEN_ID), SUPPLY); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_balanceOf_exists_and_panics() { + let (_, dispatcher) = setup_erc6909_panic(); + dispatcher.balance_of(OWNER(), TOKEN_ID); +} + +#[test] +fn test_dual_transferFrom() { + let (dispatcher, target) = setup_camel(); + set_contract_address(OWNER()); + target.approve(OPERATOR(), TOKEN_ID, VALUE); + + set_contract_address(OPERATOR()); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); + assert_eq!(target.balanceOf(RECIPIENT(), TOKEN_ID), VALUE); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_transferFrom_exists_and_panics() { + let (_, dispatcher) = setup_erc6909_panic(); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, VALUE); +} + +#[test] +fn test_dual_setOperator() { + let (dispatcher, target) = setup_camel(); + set_contract_address(OWNER()); + target.setOperator(OPERATOR(), true); + + set_contract_address(OPERATOR()); + assert!(dispatcher.is_operator(OWNER(), OPERATOR())); +} + +#[test] +#[should_panic(expected: ("Some error", 'ENTRYPOINT_FAILED',))] +fn test_dual_setOperator_exists_and_panics() { + let (_, dispatcher) = setup_erc6909_panic(); + dispatcher.set_operator(OPERATOR(), true); +} diff --git a/packages/token/src/tests/erc6909/test_erc6909.cairo b/packages/token/src/tests/erc6909/test_erc6909.cairo new file mode 100644 index 000000000..98979e574 --- /dev/null +++ b/packages/token/src/tests/erc6909/test_erc6909.cairo @@ -0,0 +1,538 @@ +use core::integer::BoundedInt; +use core::starknet::{ContractAddress, testing}; +use openzeppelin::introspection::interface::ISRC5_ID; +use openzeppelin::tests::mocks::erc6909_mocks::DualCaseERC6909Mock; +use openzeppelin::tests::utils::constants::{ + ZERO, OWNER, SPENDER, RECIPIENT, SUPPLY, VALUE, OPERATOR +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::{ + InternalImpl, ERC6909Impl, ERC6909CamelOnlyImpl +}; +use openzeppelin::token::erc6909::ERC6909Component::{Approval, Transfer, OperatorSet}; +use openzeppelin::token::erc6909::ERC6909Component; +use super::common::{ + assert_event_approval, assert_only_event_approval, assert_only_event_transfer, + assert_only_event_operator_set, assert_event_operator_set +}; + +// +// Setup +// + +const TOKEN_ID: u256 = 420; + +type ComponentState = ERC6909Component::ComponentState; + +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_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); +} + +#[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 +// + +#[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); +} + +#[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); +} + +#[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); +} + +#[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!(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); +} + +#[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); +} + +#[test] +#[should_panic(expected: ('ERC6909: burn from 0',))] +fn test__burn_from_zero() { + let mut state = setup(); + state.burn(ZERO(), TOKEN_ID, VALUE); +} + +// +// 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/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo b/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo new file mode 100644 index 000000000..770e84cdd --- /dev/null +++ b/packages/token/src/tests/erc6909/test_erc6909_content_uri.cairo @@ -0,0 +1,105 @@ +use core::integer::BoundedInt; +use core::num::traits::Zero; +use openzeppelin::tests::mocks::erc6909_content_uri_mocks::DualCaseERC6909ContentURIMock; +use openzeppelin::tests::utils::constants::{ + OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO, BASE_URI, BASE_URI_2 +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::InternalImpl as InternalERC6909Impl; +use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent::{ + ERC6909ContentURIImpl, InternalImpl, +}; +use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::storage::{StorageMapMemberAccessTrait, StorageMemberAccessTrait}; +use starknet::testing; + +use super::common::{ + assert_event_approval, assert_only_event_approval, assert_only_event_transfer, + assert_only_event_operator_set, assert_event_operator_set +}; + +// +// Setup +// + +const TOKEN_ID: u256 = 420; + +type ComponentState = + ERC6909ContentURIComponent::ComponentState; + +fn CONTRACT_STATE() -> DualCaseERC6909ContentURIMock::ContractState { + DualCaseERC6909ContentURIMock::contract_state_for_testing() +} + +fn COMPONENT_STATE() -> ComponentState { + ERC6909ContentURIComponent::component_state_for_testing() +} + +fn setup() -> (ComponentState, DualCaseERC6909ContentURIMock::ContractState) { + let mut state = COMPONENT_STATE(); + let mut mock_state = CONTRACT_STATE(); + mock_state.erc6909.mint(OWNER(), TOKEN_ID, SUPPLY); + utils::drop_event(ZERO()); + (state, mock_state) +} + +// +// Getters +// + +#[test] +fn test_unset_content_uri() { + let (mut state, _) = setup(); + let mut uri = state.contract_uri(); + assert_eq!(uri, ""); +} + +#[test] +fn test_unset_token_uri() { + let (mut state, _) = setup(); + let uri = state.token_uri(TOKEN_ID); + assert_eq!(uri, ""); +} + +// +// internal setters +// + +#[test] +fn test_set_contract_uri() { + let (mut state, _) = setup(); + testing::set_caller_address(OWNER()); + state.initializer(BASE_URI()); + let uri = state.contract_uri(); + assert_eq!(uri, BASE_URI()); +} + +#[test] +fn test_set_token_uri() { + let (mut state, _) = setup(); + testing::set_caller_address(OWNER()); + state.initializer(BASE_URI()); + let uri = state.token_uri(TOKEN_ID); + let expected = format!("{}{}", BASE_URI(), TOKEN_ID); + assert_eq!(uri, expected); +} + +// Updates the URI once set +#[test] +fn test_update_token_uri() { + let (mut state, _) = setup(); + testing::set_caller_address(OWNER()); + state.initializer(BASE_URI()); + let mut uri = state.token_uri(TOKEN_ID); + let mut expected = format!("{}{}", BASE_URI(), TOKEN_ID); + assert_eq!(uri, expected); + + testing::set_caller_address(OWNER()); + state.initializer(BASE_URI_2()); + let mut uri = state.token_uri(TOKEN_ID); + let expected = format!("{}{}", BASE_URI_2(), TOKEN_ID); + assert_eq!(uri, expected); +} diff --git a/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo b/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo new file mode 100644 index 000000000..5bbcc101e --- /dev/null +++ b/packages/token/src/tests/erc6909/test_erc6909_metadata.cairo @@ -0,0 +1,108 @@ +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 + +// The mocks use this metadata +// Check that minting a token updates the metadata using the ERC6909Hooks +#[test] +fn test_name() { + let (mut state, _) = setup(); + let mut name = state.ERC6909Metadata_name.read(TOKEN_ID); + assert_eq!(name, "MyERC6909Token"); +} + +#[test] +fn test_symbol() { + let (mut state, _) = setup(); + let mut symbol = state.ERC6909Metadata_symbol.read(TOKEN_ID); + assert_eq!(symbol, "MET"); +} + +#[test] +fn test_decimals() { + let (mut state, _) = setup(); + let mut decimals = state.ERC6909Metadata_decimals.read(TOKEN_ID); + assert_eq!(decimals, 18); +} + +// internal setters + +#[test] +fn test_set_name() { + let (_, mut mock_state) = setup(); + testing::set_caller_address(OWNER()); + mock_state.erc6909_metadata._set_token_name(TOKEN_ID, "some token"); + let mut name = mock_state.name(TOKEN_ID); + assert_eq!(name, "some token"); + + let mut name = mock_state.name(TOKEN_ID + 69); + assert_eq!(name, ""); +} + +#[test] +fn test_set_symbol() { + let (_, mut mock_state) = setup(); + testing::set_caller_address(OWNER()); + mock_state.erc6909_metadata._set_token_symbol(TOKEN_ID, "some symbol"); + let mut symbol = mock_state.symbol(TOKEN_ID); + assert_eq!(symbol, "some symbol"); + + let mut symbol = mock_state.symbol(TOKEN_ID + 69); + assert_eq!(symbol, ""); +} + +#[test] +fn test_set_decimals() { + let (_, mut mock_state) = setup(); + testing::set_caller_address(OWNER()); + mock_state.erc6909_metadata._set_token_decimals(TOKEN_ID, 18); + let mut decimals = mock_state.decimals(TOKEN_ID); + assert_eq!(decimals, 18); + + let mut decimals = mock_state.decimals(TOKEN_ID + 69); + assert_eq!(decimals, 0); +} diff --git a/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo b/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo new file mode 100644 index 000000000..4bdf62dcf --- /dev/null +++ b/packages/token/src/tests/erc6909/test_erc6909_token_supply.cairo @@ -0,0 +1,164 @@ +use core::integer::BoundedInt; +use core::num::traits::Zero; +use openzeppelin::tests::mocks::erc6909_token_supply_mocks::DualCaseERC6909TokenSupplyMock; +use openzeppelin::tests::utils::constants::{OWNER, SPENDER, RECIPIENT, SUPPLY, ZERO}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc6909::ERC6909Component::{ + InternalImpl as InternalERC6909Impl, ERC6909Impl +}; +use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent::{ + ERC6909TokenSupplyImpl, InternalImpl, +}; +use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; +use openzeppelin::utils::serde::SerializedAppend; +use 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.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); + 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.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.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); +} + +// 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/packages/token/src/tests/mocks/erc6909_content_uri_mocks.cairo b/packages/token/src/tests/mocks/erc6909_content_uri_mocks.cairo new file mode 100644 index 000000000..452d2f72f --- /dev/null +++ b/packages/token/src/tests/mocks/erc6909_content_uri_mocks.cairo @@ -0,0 +1,50 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC6909ContentURIMock { + use openzeppelin::token::erc6909::extensions::ERC6909ContentURIComponent; + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + component!( + path: ERC6909ContentURIComponent, + storage: erc6909_content_uri, + event: ERC6909ContentURIEvent + ); + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + // ERC6909ContentURI + #[abi(embed_v0)] + impl ERC6909ContentURIComponentImpl = + ERC6909ContentURIComponent::ERC6909ContentURIImpl; + + // ERC6909Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909ContentURIInternalImpl = ERC6909ContentURIComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909_content_uri: ERC6909ContentURIComponent::Storage, + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909ContentURIEvent: ERC6909ContentURIComponent::Event, + #[flat] + ERC6909Event: ERC6909Component::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256, uri: ByteArray + ) { + self.erc6909.mint(receiver, id, amount); + self.erc6909_content_uri.initializer(uri); + } +} diff --git a/packages/token/src/tests/mocks/erc6909_metadata_mocks.cairo b/packages/token/src/tests/mocks/erc6909_metadata_mocks.cairo new file mode 100644 index 000000000..3071e5ddf --- /dev/null +++ b/packages/token/src/tests/mocks/erc6909_metadata_mocks.cairo @@ -0,0 +1,75 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC6909MetadataMock { + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::extensions::ERC6909MetadataComponent; + use starknet::ContractAddress; + + component!( + path: ERC6909MetadataComponent, storage: erc6909_metadata, event: ERC6909MetadataEvent + ); + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + // ERC6909Metadata + #[abi(embed_v0)] + impl ERC6909MetadataComponentImpl = + ERC6909MetadataComponent::ERC6909MetadataImpl; + + // ERC6909Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909MetadataInternalImpl = ERC6909MetadataComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909_metadata: ERC6909MetadataComponent::Storage, + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909MetadataEvent: ERC6909MetadataComponent::Event, + #[flat] + ERC6909Event: ERC6909Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } + + impl ERC6909MetadataHooksImpl< + TContractState, + impl ERC6909Metadata: ERC6909MetadataComponent::HasComponent, + impl HasComponent: ERC6909Component::HasComponent, + +Drop + > of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + /// Update after any transfer + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) { + let mut erc6909_metadata_component = get_dep_component_mut!(ref self, ERC6909Metadata); + let name = "MyERC6909Token"; + let symbol = "MET"; + let decimals = 18; + erc6909_metadata_component._update_token_metadata(from, id, name, symbol, decimals); + } + } +} diff --git a/packages/token/src/tests/mocks/erc6909_mocks.cairo b/packages/token/src/tests/mocks/erc6909_mocks.cairo new file mode 100644 index 000000000..460a11502 --- /dev/null +++ b/packages/token/src/tests/mocks/erc6909_mocks.cairo @@ -0,0 +1,263 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC6909Mock { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + /// Component + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + /// ABI of Components + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + #[abi(embed_v0)] + impl ERC6909CamelOnlyImpl = + ERC6909Component::ERC6909CamelOnlyImpl; + + /// Internal logic + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } +} + +#[starknet::contract] +pub(crate) mod SnakeERC6909Mock { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + /// Component + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + /// ABI of Components + #[abi(embed_v0)] + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + + /// Internal logic + impl InternalImpl = ERC6909Component::InternalImpl; + + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } +} + +#[starknet::contract] +pub(crate) mod CamelERC6909Mock { + use openzeppelin::token::erc6909::{ERC6909Component, ERC6909HooksEmptyImpl}; + use starknet::ContractAddress; + + /// Component + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + #[abi(embed_v0)] + impl ERC6909CamelOnlyImpl = + ERC6909Component::ERC6909CamelOnlyImpl; + + // `ERC6909Impl` is not embedded because it would defeat the purpose of the + // mock. The `ERC6909Impl` case-agnostic methods are manually exposed. + impl ERC6909Impl = ERC6909Component::ERC6909Impl; + impl InternalImpl = ERC6909Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909: ERC6909Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909Event: ERC6909Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } + + #[abi(per_item)] + #[generate_trait] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, + ) -> u256 { + self.erc6909.allowance(owner, spender, id) + } + + #[external(v0)] + fn transfer( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool { + self.erc6909.transfer(receiver, id, amount) + } + + #[external(v0)] + fn approve( + ref self: ContractState, spender: ContractAddress, id: u256, amount: u256 + ) -> bool { + self.erc6909.approve(spender, id, amount) + } + } +} + +/// Although these modules are designed to panic, functions +/// still need a valid return value. We chose: +/// +/// 3 for felt252, u8, and u256 +/// zero for ContractAddress +/// false for bool +#[starknet::contract] +pub(crate) mod SnakeERC6909Panic { + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[abi(per_item)] + #[generate_trait] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn balance_of(self: @ContractState, account: ContractAddress, id: u256) -> u256 { + panic!("Some error"); + 3 + } + + #[external(v0)] + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, id: u256, + ) -> u256 { + panic!("Some error"); + 3 + } + + #[external(v0)] + fn is_operator( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn transfer( + ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256 + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + receiver: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn approve( + ref self: ContractState, spender: ContractAddress, id: u256, amount: u256 + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn set_operator(ref self: ContractState, spender: ContractAddress, approved: bool) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + panic!("Some error"); + false + } + } +} + +#[starknet::contract] +pub(crate) mod CamelERC6909Panic { + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[abi(per_item)] + #[generate_trait] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn balanceOf(self: @ContractState, account: ContractAddress, id: u256) -> u256 { + panic!("Some error"); + 3 + } + + #[external(v0)] + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn setOperator(ref self: ContractState, spender: ContractAddress, approved: bool) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn supportsInterface(self: @ContractState, interface_id: felt252) -> bool { + panic!("Some error"); + false + } + + #[external(v0)] + fn isOperator( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> bool { + panic!("Some error"); + false + } + } +} + diff --git a/packages/token/src/tests/mocks/erc6909_token_supply_mocks.cairo b/packages/token/src/tests/mocks/erc6909_token_supply_mocks.cairo new file mode 100644 index 000000000..e0f123575 --- /dev/null +++ b/packages/token/src/tests/mocks/erc6909_token_supply_mocks.cairo @@ -0,0 +1,76 @@ +#[starknet::contract] +pub(crate) mod DualCaseERC6909TokenSupplyMock { + use openzeppelin::token::erc6909::ERC6909Component; + use openzeppelin::token::erc6909::extensions::ERC6909TokenSupplyComponent; + use starknet::ContractAddress; + + component!( + path: ERC6909TokenSupplyComponent, + storage: erc6909_token_supply, + event: ERC6909TokenSupplyEvent + ); + component!(path: ERC6909Component, storage: erc6909, event: ERC6909Event); + + // ERC6909TokenSupply + #[abi(embed_v0)] + impl ERC6909TokenSupplyComponentImpl = + ERC6909TokenSupplyComponent::ERC6909TokenSupplyImpl; + + // ERC6909Mixin + #[abi(embed_v0)] + impl ERC6909MixinImpl = ERC6909Component::ERC6909MixinImpl; + + impl ERC6909InternalImpl = ERC6909Component::InternalImpl; + impl ERC6909TokenSupplyInternalImpl = ERC6909TokenSupplyComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc6909_token_supply: ERC6909TokenSupplyComponent::Storage, + #[substorage(v0)] + erc6909: ERC6909Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC6909TokenSupplyEvent: ERC6909TokenSupplyComponent::Event, + #[flat] + ERC6909Event: ERC6909Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, receiver: ContractAddress, id: u256, amount: u256) { + self.erc6909.mint(receiver, id, amount); + } + + impl ERC6909TokenSupplyHooksImpl< + TContractState, + impl ERC6909TokenSupply: ERC6909TokenSupplyComponent::HasComponent, + impl HasComponent: ERC6909Component::HasComponent, + +Drop + > of ERC6909Component::ERC6909HooksTrait { + fn before_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) {} + + /// Update after any transfer + fn after_update( + ref self: ERC6909Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + id: u256, + amount: u256 + ) { + let mut erc6909_token_supply_component = get_dep_component_mut!( + ref self, ERC6909TokenSupply + ); + erc6909_token_supply_component._update_token_supply(from, recipient, id, amount); + } + } +} diff --git a/packages/utils/src/selectors.cairo b/packages/utils/src/selectors.cairo index 03de0e017..3fb7ec310 100644 --- a/packages/utils/src/selectors.cairo +++ b/packages/utils/src/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");