diff --git a/src/presets.cairo b/src/presets.cairo index 6d3ddc6f6..7be370f78 100644 --- a/src/presets.cairo +++ b/src/presets.cairo @@ -1,7 +1,9 @@ mod account; +mod erc1155; mod erc20; mod erc721; use account::Account; +use erc1155::ERC1155; use erc20::ERC20; use erc721::ERC721; diff --git a/src/presets/erc1155.cairo b/src/presets/erc1155.cairo new file mode 100644 index 000000000..1b8ef675a --- /dev/null +++ b/src/presets/erc1155.cairo @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.8.0 (presets/erc1155.cairo) + +/// # ERC1155 Preset +/// +/// The ERC1155 contract offers a batch-mint mechanism that +/// can only be executed once upon contract construction. +#[starknet::contract] +mod ERC1155 { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::ERC1155Component; + use starknet::ContractAddress; + + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // ERC1155 + #[abi(embed_v0)] + impl ERC1155Impl = ERC1155Component::ERC1155Impl; + #[abi(embed_v0)] + impl ERC1155MetadataImpl = ERC1155Component::ERC1155MetadataImpl; + #[abi(embed_v0)] + impl ERC1155CamelOnly = ERC1155Component::ERC1155CamelOnlyImpl; + + impl ERC1155InternalImpl = ERC1155Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc1155: ERC1155Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155Event: ERC1155Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + mod Errors { + const UNEQUAL_ARRAYS_VALUES: felt252 = 'Values array len do not match'; + const UNEQUAL_ARRAYS_URI: felt252 = 'URI Array len do not match'; + } + + /// Sets the token `name` and `symbol`. + /// Mints the `values` for `token_ids` tokens to `recipient` and sets + /// each token's URI. + #[constructor] + fn constructor( + ref self: ContractState, + name: felt252, + symbol: felt252, + recipient: ContractAddress, + token_ids: Span, + values: Span, + token_uris: Span + ) { + self.erc1155.initializer(name, symbol); + self._mint_assets(recipient, token_ids, values, token_uris); + } + + /// Mints the `values` for `token_ids` tokens to `recipient`. + /// Sets the token URI from `token_uris` to the corresponding + /// token ID of `token_ids`. + /// + /// Requirements: + /// + /// - `values` must be equal in length to `token_ids`. + /// - `token_ids` must be equal in length to `token_uris`. + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _mint_assets( + ref self: ContractState, + recipient: ContractAddress, + mut token_ids: Span, + mut values: Span, + mut token_uris: Span + ) { + assert(token_ids.len() == values.len(), Errors::UNEQUAL_ARRAYS_VALUES); + assert(token_ids.len() == token_uris.len(), Errors::UNEQUAL_ARRAYS_URI); + + loop { + if token_ids.len() == 0 { + break; + } + let id = *token_ids.pop_front().unwrap(); + let value = *values.pop_front().unwrap(); + let uri = *token_uris.pop_front().unwrap(); + + self.erc1155._mint(recipient, id, value); + self.erc1155._set_uri(id, uri); + } + } + } +} diff --git a/src/tests/mocks.cairo b/src/tests/mocks.cairo index 4bc03de22..37adcdf1d 100644 --- a/src/tests/mocks.cairo +++ b/src/tests/mocks.cairo @@ -1,5 +1,7 @@ mod accesscontrol_mocks; mod account_mocks; +mod erc1155_mocks; +mod erc1155_receiver_mocks; mod erc20_mocks; mod erc721_mocks; mod erc721_receiver_mocks; diff --git a/src/tests/mocks/erc1155_mocks.cairo b/src/tests/mocks/erc1155_mocks.cairo new file mode 100644 index 000000000..40365a640 --- /dev/null +++ b/src/tests/mocks/erc1155_mocks.cairo @@ -0,0 +1,364 @@ +#[starknet::contract] +mod DualCaseERC1155Mock { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::ERC1155Component; + use starknet::ContractAddress; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + // ERC1155 + #[abi(embed_v0)] + impl ERC1155Impl = ERC1155Component::ERC1155Impl; + #[abi(embed_v0)] + impl ERC1155MetadataImpl = ERC1155Component::ERC1155MetadataImpl; + #[abi(embed_v0)] + impl ERC721CamelOnly = ERC1155Component::ERC1155CamelOnlyImpl; + + impl ERC1155InternalImpl = ERC1155Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc1155: ERC1155Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155Event: ERC1155Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: felt252, + symbol: felt252, + recipient: ContractAddress, + token_id: u256, + value: u256, + uri: felt252 + ) { + self.erc1155.initializer(name, symbol); + self.erc1155._mint(recipient, token_id, value); + self.erc1155._set_uri(token_id, uri); + } +} + +#[starknet::contract] +mod SnakeERC1155Mock { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::ERC1155Component; + use starknet::ContractAddress; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + // ERC1155 + #[abi(embed_v0)] + impl ERC1155Impl = ERC1155Component::ERC1155Impl; + #[abi(embed_v0)] + impl ERC1155MetadataImpl = ERC1155Component::ERC1155MetadataImpl; + + impl ERC1155InternalImpl = ERC1155Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc1155: ERC1155Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155Event: ERC1155Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: felt252, + symbol: felt252, + recipient: ContractAddress, + token_id: u256, + value: u256, + uri: felt252 + ) { + self.erc1155.initializer(name, symbol); + self.erc1155._mint(recipient, token_id, value); + self.erc1155._set_uri(token_id, uri); + } +} + +#[starknet::contract] +mod CamelERC1155Mock { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::ERC1155Component::{ERC1155Impl, ERC1155MetadataImpl}; + use openzeppelin::token::erc1155::ERC1155Component; + use starknet::ContractAddress; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + // ERC1155 + #[abi(embed_v0)] + impl ERC1155CamelOnly = ERC1155Component::ERC1155CamelOnlyImpl; + + impl ERC1155InternalImpl = ERC1155Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc1155: ERC1155Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155Event: ERC1155Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: felt252, + symbol: felt252, + recipient: ContractAddress, + token_id: u256, + value: u256, + uri: felt252 + ) { + self.erc1155.initializer(name, symbol); + self.erc1155._mint(recipient, token_id, value); + self.erc1155._set_uri(token_id, uri); + } + + /// The following external methods are included because they are case-agnostic + /// and this contract should not embed the snake_case impl. + #[generate_trait] + #[external(v0)] + impl ExternalImpl of ExternalTrait { + fn name(self: @ContractState) -> felt252 { + self.erc1155.name() + } + + fn symbol(self: @ContractState) -> felt252 { + self.erc1155.symbol() + } + } +} + +#[starknet::contract] +mod SnakeERC1155PanicMock { + use starknet::ContractAddress; + use zeroable::Zeroable; + + #[storage] + struct Storage {} + + #[generate_trait] + #[external(v0)] + impl ExternalImpl of ExternalTrait { + fn name(self: @ContractState) -> felt252 { + panic_with_felt252('Some error'); + 3 + } + + fn symbol(self: @ContractState) -> felt252 { + panic_with_felt252('Some error'); + 3 + } + + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + panic_with_felt252('Some error'); + false + } + + fn uri(self: @ContractState, token_id: u256) -> felt252 { + panic_with_felt252('Some error'); + 3 + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + panic_with_felt252('Some error'); + u256 { low: 3, high: 3 } + } + + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + panic_with_felt252('Some error'); + false + } + + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool + ) { + panic_with_felt252('Some error'); + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + panic_with_felt252('Some error'); + } + + fn safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ) { + panic_with_felt252('Some error'); + } + + fn balance_of_batch( + self: @ContractState, accounts: Span, token_ids: Span + ) -> Span { + panic_with_felt252('Some error'); + array![u256 { low: 3, high: 3 }].span() + } + + fn safe_batch_transfer_from( + ref self: ContractState, + from: starknet::ContractAddress, + to: starknet::ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) { + panic_with_felt252('Some error'); + } + + fn batch_transfer_from( + ref self: ContractState, + from: starknet::ContractAddress, + to: starknet::ContractAddress, + token_ids: Span, + values: Span + ) { + panic_with_felt252('Some error'); + } + } +} + +#[starknet::contract] +mod CamelERC1155PanicMock { + use starknet::ContractAddress; + use zeroable::Zeroable; + + #[storage] + struct Storage {} + + #[generate_trait] + #[external(v0)] + impl ExternalImpl of ExternalTrait { + fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { + panic_with_felt252('Some error'); + false + } + + fn name(self: @ContractState) -> felt252 { + panic_with_felt252('Some error'); + 3 + } + + fn symbol(self: @ContractState) -> felt252 { + panic_with_felt252('Some error'); + 3 + } + + fn uri(self: @ContractState, tokenId: u256) -> felt252 { + panic_with_felt252('Some error'); + 3 + } + + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + panic_with_felt252('Some error'); + u256 { low: 3, high: 3 } + } + + fn isApprovedForAll( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + panic_with_felt252('Some error'); + false + } + + fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { + panic_with_felt252('Some error'); + } + + fn transferFrom( + ref self: ContractState, from: ContractAddress, to: ContractAddress, tokenId: u256 + ) { + panic_with_felt252('Some error'); + } + + fn safeTransferFrom( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + data: Span + ) { + panic_with_felt252('Some error'); + } + + fn balanceOfBatch( + self: @ContractState, accounts: Span, token_ids: Span + ) -> Span { + panic_with_felt252('Some error'); + array![u256 { low: 3, high: 3 }].span() + } + + fn safeBatchTransferFrom( + ref self: ContractState, + from: starknet::ContractAddress, + to: starknet::ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) { + panic_with_felt252('Some error'); + } + + fn batchTransferFrom( + ref self: ContractState, + from: starknet::ContractAddress, + to: starknet::ContractAddress, + token_ids: Span, + values: Span + ) { + panic_with_felt252('Some error'); + } + } +} diff --git a/src/tests/mocks/erc1155_receiver_mocks.cairo b/src/tests/mocks/erc1155_receiver_mocks.cairo new file mode 100644 index 000000000..f4214406d --- /dev/null +++ b/src/tests/mocks/erc1155_receiver_mocks.cairo @@ -0,0 +1,262 @@ +use openzeppelin::tests::utils::constants::SUCCESS; + +#[starknet::contract] +mod DualCaseERC1155ReceiverMock { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::ERC1155ReceiverComponent; + use starknet::ContractAddress; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!( + path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent + ); + + // ERC1155Receiver + impl ERC1155ReceiverImpl = ERC1155ReceiverComponent::ERC1155ReceiverImpl; + impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc1155_receiver: ERC1155ReceiverComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.erc1155_receiver.initializer(); + } + + #[external(v0)] + fn on_erc1155_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252 { + if *data.at(0) == super::SUCCESS { + self.erc1155_receiver.on_erc1155_received(operator, from, token_id, value, data) + } else { + 0 + } + } + + #[external(v0)] + fn on_erc1155_batch_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> felt252 { + if *data.at(0) == super::SUCCESS { + self.erc1155_receiver.on_erc1155_batch_received(operator, from, token_ids, values, data) + } else { + 0 + } + } +} + +#[starknet::contract] +mod SnakeERC1155ReceiverMock { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::ERC1155ReceiverComponent; + use starknet::ContractAddress; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!( + path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent + ); + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + // ERC1155Receiver + impl ERC1155ReceiverImpl = ERC1155ReceiverComponent::ERC1155ReceiverImpl; + impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc1155_receiver: ERC1155ReceiverComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.erc1155_receiver.initializer(); + } + + #[external(v0)] + fn on_erc1155_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252 { + if *data.at(0) == super::SUCCESS { + self.erc1155_receiver.on_erc1155_received(operator, from, token_id, value, data) + } else { + 0 + } + } + + #[external(v0)] + fn on_erc1155_batch_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> felt252 { + if *data.at(0) == super::SUCCESS { + self.erc1155_receiver.on_erc1155_batch_received(operator, from, token_ids, values, data) + } else { + 0 + } + } +} + +#[starknet::contract] +mod CamelERC1155ReceiverMock { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::ERC1155ReceiverComponent; + use starknet::ContractAddress; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!( + path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent + ); + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + // ERC1155Receiver + impl ERC1155ReceiverCamelImpl = + ERC1155ReceiverComponent::ERC1155ReceiverCamelImpl; + impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc1155_receiver: ERC1155ReceiverComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.erc1155_receiver.initializer(); + } + + #[external(v0)] + fn onERC1155Received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ) -> felt252 { + if *data.at(0) == super::SUCCESS { + self.erc1155_receiver.onERC1155Received(operator, from, tokenId, value, data) + } else { + 0 + } + } + + #[external(v0)] + fn onERC1155BatchReceived( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + tokenIds: Span, + values: Span, + data: Span + ) -> felt252 { + if *data.at(0) == super::SUCCESS { + self.erc1155_receiver.onERC1155BatchReceived(operator, from, tokenIds, values, data) + } else { + 0 + } + } +} + +#[starknet::contract] +mod SnakeERC1155ReceiverPanicMock { + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[external(v0)] + fn on_erc1155_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252 { + panic_with_felt252('Some error'); + 3 + } +} + +#[starknet::contract] +mod CamelERC1155ReceiverPanicMock { + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[external(v0)] + fn onERC1155Received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ) -> felt252 { + panic_with_felt252('Some error'); + 3 + } +} diff --git a/src/tests/token.cairo b/src/tests/token.cairo index 5b30d8743..6732b39ce 100644 --- a/src/tests/token.cairo +++ b/src/tests/token.cairo @@ -1,6 +1,9 @@ +mod test_dual1155; +mod test_dual1155_receiver; mod test_dual20; mod test_dual721; mod test_dual721_receiver; +mod test_erc1155; mod test_erc20; mod test_erc721; mod test_erc721_receiver; diff --git a/src/tests/token/test_dual1155.cairo b/src/tests/token/test_dual1155.cairo new file mode 100644 index 000000000..e4108f862 --- /dev/null +++ b/src/tests/token/test_dual1155.cairo @@ -0,0 +1,258 @@ +use openzeppelin::tests::mocks::erc1155_mocks::{CamelERC1155Mock, SnakeERC1155Mock}; +use openzeppelin::tests::mocks::erc1155_mocks::{CamelERC1155PanicMock, SnakeERC1155PanicMock}; + +use openzeppelin::tests::mocks::erc1155_receiver_mocks::DualCaseERC1155ReceiverMock; +use openzeppelin::tests::mocks::non_implementing_mock::NonImplementingMock; +use openzeppelin::tests::utils::constants::{ + DATA, OWNER, RECIPIENT, SPENDER, OPERATOR, OTHER, NAME, SYMBOL, TOKEN_ID, TOKEN_VALUE, URI +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc1155::dual1155::{DualCaseERC1155, DualCaseERC1155Trait}; +use openzeppelin::token::erc1155::interface::IERC1155_ID; +use openzeppelin::token::erc1155::interface::{ + IERC1155CamelOnlyDispatcher, IERC1155CamelOnlyDispatcherTrait +}; +use openzeppelin::token::erc1155::interface::{IERC1155Dispatcher, IERC1155DispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use starknet::testing::set_caller_address; +use starknet::testing::set_contract_address; + +// +// Setup +// + +fn setup_snake() -> (DualCaseERC1155, IERC1155Dispatcher) { + let mut calldata = array![]; + calldata.append_serde(NAME); + calldata.append_serde(SYMBOL); + calldata.append_serde(OWNER()); + calldata.append_serde(TOKEN_ID); + calldata.append_serde(TOKEN_VALUE); + calldata.append_serde(URI); + set_contract_address(OWNER()); + let target = utils::deploy(SnakeERC1155Mock::TEST_CLASS_HASH, calldata); + (DualCaseERC1155 { contract_address: target }, IERC1155Dispatcher { contract_address: target }) +} + +fn setup_camel() -> (DualCaseERC1155, IERC1155CamelOnlyDispatcher) { + let mut calldata = array![]; + calldata.append_serde(NAME); + calldata.append_serde(SYMBOL); + calldata.append_serde(OWNER()); + calldata.append_serde(TOKEN_ID); + calldata.append_serde(URI); + set_contract_address(OWNER()); + let target = utils::deploy(CamelERC1155Mock::TEST_CLASS_HASH, calldata); + ( + DualCaseERC1155 { contract_address: target }, + IERC1155CamelOnlyDispatcher { contract_address: target } + ) +} + +fn setup_non_erc1155() -> DualCaseERC1155 { + let calldata = array![]; + let target = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, calldata); + DualCaseERC1155 { contract_address: target } +} + +fn setup_erc1155_panic() -> (DualCaseERC1155, DualCaseERC1155) { + let snake_target = utils::deploy(SnakeERC1155PanicMock::TEST_CLASS_HASH, array![]); + let camel_target = utils::deploy(CamelERC1155PanicMock::TEST_CLASS_HASH, array![]); + ( + DualCaseERC1155 { contract_address: snake_target }, + DualCaseERC1155 { contract_address: camel_target } + ) +} + +fn setup_receiver() -> ContractAddress { + utils::deploy(DualCaseERC1155ReceiverMock::TEST_CLASS_HASH, array![]) +} + +// +// Tests +// + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_name() { + let dispatcher = setup_non_erc1155(); + dispatcher.name(); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_name_exists_and_panics() { + let (dispatcher, _) = setup_erc1155_panic(); + dispatcher.name(); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_symbol() { + let dispatcher = setup_non_erc1155(); + dispatcher.symbol(); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_symbol_exists_and_panics() { + let (dispatcher, _) = setup_erc1155_panic(); + dispatcher.symbol(); +} + +// +// snake_case target +// + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_balance_of() { + let dispatcher = setup_non_erc1155(); + dispatcher.balance_of(OWNER(), TOKEN_ID); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_transfer_from() { + let dispatcher = setup_non_erc1155(); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_safe_transfer_from() { + let dispatcher = setup_non_erc1155(); + dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +#[test] +#[available_gas(2000000)] +fn test_dual_set_approval_for_all() { + let (dispatcher, target) = setup_snake(); + set_contract_address(OWNER()); + dispatcher.set_approval_for_all(OPERATOR(), true); + assert(target.is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_set_approval_for_all() { + let dispatcher = setup_non_erc1155(); + dispatcher.set_approval_for_all(OPERATOR(), true); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_set_approval_for_all_exists_and_panics() { + let (dispatcher, _) = setup_erc1155_panic(); + dispatcher.set_approval_for_all(OPERATOR(), true); +} + +#[test] +#[available_gas(2000000)] +fn test_dual_is_approved_for_all() { + let (dispatcher, target) = setup_snake(); + set_contract_address(OWNER()); + target.set_approval_for_all(OPERATOR(), true); + assert(dispatcher.is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_is_approved_for_all() { + let dispatcher = setup_non_erc1155(); + dispatcher.is_approved_for_all(OWNER(), OPERATOR()); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_is_approved_for_all_exists_and_panics() { + let (dispatcher, _) = setup_erc1155_panic(); + dispatcher.is_approved_for_all(OWNER(), OPERATOR()); +} + +#[test] +#[available_gas(2000000)] +fn test_dual_uri() { + let (dispatcher, target) = setup_snake(); + assert(dispatcher.uri(TOKEN_ID) == URI, 'Should return URI'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_uri() { + let dispatcher = setup_non_erc1155(); + dispatcher.uri(TOKEN_ID); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_uri_exists_and_panics() { + let (dispatcher, _) = setup_erc1155_panic(); + dispatcher.uri(TOKEN_ID); +} + +#[test] +#[available_gas(2000000)] +fn test_dual_supports_interface() { + let (dispatcher, _) = setup_snake(); + assert(dispatcher.supports_interface(IERC1155_ID), 'Should support own interface'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_supports_interface() { + let dispatcher = setup_non_erc1155(); + dispatcher.supports_interface(IERC1155_ID); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_supports_interface_exists_and_panics() { + let (dispatcher, _) = setup_erc1155_panic(); + dispatcher.supports_interface(IERC1155_ID); +} + +// +// camelCase target +// + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_setApprovalForAll_exists_and_panics() { + let (_, dispatcher) = setup_erc1155_panic(); + dispatcher.set_approval_for_all(OPERATOR(), true); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_tokenURI_exists_and_panics() { + let (_, dispatcher) = setup_erc1155_panic(); + dispatcher.uri(TOKEN_ID); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_supportsInterface_exists_and_panics() { + let (_, dispatcher) = setup_erc1155_panic(); + dispatcher.supports_interface(IERC1155_ID); +} diff --git a/src/tests/token/test_dual1155_receiver.cairo b/src/tests/token/test_dual1155_receiver.cairo new file mode 100644 index 000000000..dcf5a56de --- /dev/null +++ b/src/tests/token/test_dual1155_receiver.cairo @@ -0,0 +1,149 @@ +use core::array::ArrayTrait; +use openzeppelin::tests::mocks::erc1155_receiver_mocks::{ + CamelERC1155ReceiverMock, CamelERC1155ReceiverPanicMock, SnakeERC1155ReceiverMock, + SnakeERC1155ReceiverPanicMock +}; +use openzeppelin::tests::mocks::non_implementing_mock::NonImplementingMock; +use openzeppelin::tests::utils::constants::{DATA, OPERATOR, OWNER, TOKEN_ID, TOKEN_VALUE}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc1155::dual1155_receiver::{ + DualCaseERC1155Receiver, DualCaseERC1155ReceiverTrait +}; +use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; +use openzeppelin::token::erc1155::interface::{ + IERC1155ReceiverCamelDispatcher, IERC1155ReceiverCamelDispatcherTrait +}; +use openzeppelin::token::erc1155::interface::{ + IERC1155ReceiverDispatcher, IERC1155ReceiverDispatcherTrait +}; + +// +// Setup +// + +fn setup_snake() -> (DualCaseERC1155Receiver, IERC1155ReceiverDispatcher) { + let mut calldata = ArrayTrait::new(); + let target = utils::deploy(SnakeERC1155ReceiverMock::TEST_CLASS_HASH, calldata); + ( + DualCaseERC1155Receiver { contract_address: target }, + IERC1155ReceiverDispatcher { contract_address: target } + ) +} + +fn setup_camel() -> (DualCaseERC1155Receiver, IERC1155ReceiverCamelDispatcher) { + let mut calldata = ArrayTrait::new(); + let target = utils::deploy(CamelERC1155ReceiverMock::TEST_CLASS_HASH, calldata); + ( + DualCaseERC1155Receiver { contract_address: target }, + IERC1155ReceiverCamelDispatcher { contract_address: target } + ) +} + +fn setup_non_erc1155_receiver() -> DualCaseERC1155Receiver { + let calldata = ArrayTrait::new(); + let target = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, calldata); + DualCaseERC1155Receiver { contract_address: target } +} + +fn setup_erc1155_receiver_panic() -> (DualCaseERC1155Receiver, DualCaseERC1155Receiver) { + let snake_target = utils::deploy( + SnakeERC1155ReceiverPanicMock::TEST_CLASS_HASH, ArrayTrait::new() + ); + let camel_target = utils::deploy( + CamelERC1155ReceiverPanicMock::TEST_CLASS_HASH, ArrayTrait::new() + ); + ( + DualCaseERC1155Receiver { contract_address: snake_target }, + DualCaseERC1155Receiver { contract_address: camel_target } + ) +} + +// +// snake_case target +// + +#[test] +#[available_gas(2000000)] +fn test_dual_on_erc1155_received() { + let (dispatcher, _) = setup_snake(); + assert( + dispatcher + .on_erc1155_received( + OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, DATA(true) + ) == IERC1155_RECEIVER_ID, + 'Should return interface id' + ); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_dual_no_on_erc1155_received() { + let dispatcher = setup_non_erc1155_receiver(); + dispatcher.on_erc1155_received(OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_on_erc1155_received_exists_and_panics() { + let (dispatcher, _) = setup_erc1155_receiver_panic(); + dispatcher.on_erc1155_received(OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +#[test] +#[available_gas(2000000)] +fn test_dual_on_erc1155_batch_received() { + let (dispatcher, _) = setup_snake(); + let token_ids = array![TOKEN_ID, TOKEN_ID]; + let values = array![TOKEN_VALUE, TOKEN_VALUE]; + assert( + dispatcher + .on_erc1155_batch_received( + OPERATOR(), OWNER(), token_ids.span(), values.span(), DATA(true) + ) == IERC1155_RECEIVER_ID, + 'Should return interface id' + ); +} + + +// +// camelCase target +// + +#[test] +#[available_gas(2000000)] +fn test_dual_onERC1155Received() { + let (dispatcher, _) = setup_camel(); + assert( + dispatcher + .on_erc1155_received( + OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, DATA(true) + ) == IERC1155_RECEIVER_ID, + 'Should return interface id' + ); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('Some error', 'ENTRYPOINT_FAILED',))] +fn test_dual_onERC1155Received_exists_and_panics() { + let (_, dispatcher) = setup_erc1155_receiver_panic(); + dispatcher.on_erc1155_received(OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +#[test] +#[available_gas(2000000)] +fn test_dual_onERC1155BatchReceived() { + let (dispatcher, _) = setup_camel(); + let token_ids = array![TOKEN_ID, TOKEN_ID]; + let values = array![TOKEN_VALUE, TOKEN_VALUE]; + assert( + dispatcher + .on_erc1155_batch_received( + OPERATOR(), OWNER(), token_ids.span(), values.span(), DATA(true) + ) == IERC1155_RECEIVER_ID, + 'Should return interface id' + ); +} + diff --git a/src/tests/token/test_erc1155.cairo b/src/tests/token/test_erc1155.cairo new file mode 100644 index 000000000..fe2255bdc --- /dev/null +++ b/src/tests/token/test_erc1155.cairo @@ -0,0 +1,488 @@ +use integer::u256_from_felt252; +use openzeppelin::account::AccountComponent; +use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin::introspection::src5; +use openzeppelin::introspection; +use openzeppelin::tests::mocks::account_mocks::{DualCaseAccountMock, CamelAccountMock}; +use openzeppelin::tests::mocks::erc1155_mocks::DualCaseERC1155Mock; +use openzeppelin::tests::mocks::erc1155_receiver_mocks::{ + CamelERC1155ReceiverMock, SnakeERC1155ReceiverMock +}; +use openzeppelin::tests::mocks::non_implementing_mock::NonImplementingMock; +use openzeppelin::tests::utils::constants::{ + DATA, ZERO, OWNER, RECIPIENT, SPENDER, OPERATOR, OTHER, NAME, SYMBOL, URI, TOKEN_ID, + TOKEN_VALUE, PUBKEY, +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc1155::ERC1155Component::ERC1155CamelOnlyImpl; +use openzeppelin::token::erc1155::ERC1155Component::{ + ERC1155Impl, ERC1155MetadataImpl, InternalImpl +}; +use openzeppelin::token::erc1155::ERC1155Component::{TransferBatch, ApprovalForAll, TransferSingle}; +use openzeppelin::token::erc1155::ERC1155Component; +use openzeppelin::token::erc1155; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::storage::StorageMapMemberAccessTrait; +use starknet::testing; + +// +// Setup +// + +fn STATE() -> DualCaseERC1155Mock::ContractState { + DualCaseERC1155Mock::contract_state_for_testing() +} + +fn setup() -> DualCaseERC1155Mock::ContractState { + let mut state = STATE(); + state.erc1155.initializer(NAME, SYMBOL); + state.erc1155._mint(OWNER(), TOKEN_ID, TOKEN_VALUE); + utils::drop_event(ZERO()); + state +} + +fn setup_receiver() -> ContractAddress { + utils::deploy(SnakeERC1155ReceiverMock::TEST_CLASS_HASH, array![]) +} + +fn setup_camel_receiver() -> ContractAddress { + utils::deploy(CamelERC1155ReceiverMock::TEST_CLASS_HASH, array![]) +} + +fn setup_account() -> ContractAddress { + let mut calldata = array![PUBKEY]; + utils::deploy(DualCaseAccountMock::TEST_CLASS_HASH, calldata) +} + +fn setup_camel_account() -> ContractAddress { + let mut calldata = array![PUBKEY]; + utils::deploy(CamelAccountMock::TEST_CLASS_HASH, calldata) +} + +// +// Initializers +// + +#[test] +#[available_gas(20000000)] +fn test_initialize() { + let mut state = STATE(); + state.erc1155.initializer(NAME, SYMBOL); + + assert(state.erc1155.name() == NAME, 'Name should be NAME'); + assert(state.erc1155.symbol() == SYMBOL, 'Symbol should be SYMBOL'); + assert(state.erc1155.balance_of(OWNER(), TOKEN_ID) == 0, 'Balance should be zero'); + + assert(state.src5.supports_interface(erc1155::interface::IERC1155_ID), 'Missing interface ID'); + assert( + state.src5.supports_interface(erc1155::interface::IERC1155_METADATA_ID), + 'Missing interface ID' + ); + assert( + state.src5.supports_interface(introspection::interface::ISRC5_ID), 'Missing interface ID' + ); +} + +// +// Getters +// + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid account',))] +fn test_balance_of_zero() { + let state = setup(); + state.erc1155.balance_of(ZERO(), TOKEN_ID); +} + +// +// set_approval_for_all & _set_approval_for_all +// + +#[test] +#[available_gas(20000000)] +fn test_set_approval_for_all() { + let mut state = STATE(); + testing::set_caller_address(OWNER()); + + assert(!state.erc1155.is_approved_for_all(OWNER(), OPERATOR()), 'Invalid default value'); + + state.erc1155.set_approval_for_all(OPERATOR(), true); + assert_event_approval_for_all(OWNER(), OPERATOR(), true); + + assert( + state.erc1155.is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly' + ); + + state.erc1155.set_approval_for_all(OPERATOR(), false); + assert_event_approval_for_all(OWNER(), OPERATOR(), false); + + assert( + !state.erc1155.is_approved_for_all(OWNER(), OPERATOR()), 'Approval not revoked correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: self approval',))] +fn test_set_approval_for_all_owner_equal_operator_true() { + let mut state = STATE(); + testing::set_caller_address(OWNER()); + state.erc1155.set_approval_for_all(OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: self approval',))] +fn test_set_approval_for_all_owner_equal_operator_false() { + let mut state = STATE(); + testing::set_caller_address(OWNER()); + state.erc1155.set_approval_for_all(OWNER(), false); +} + +#[test] +#[available_gas(20000000)] +fn test__set_approval_for_all() { + let mut state = STATE(); + assert(!state.erc1155.is_approved_for_all(OWNER(), OPERATOR()), 'Invalid default value'); + + state.erc1155._set_approval_for_all(OWNER(), OPERATOR(), true); + assert_event_approval_for_all(OWNER(), OPERATOR(), true); + + assert( + state.erc1155.is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly' + ); + + state.erc1155._set_approval_for_all(OWNER(), OPERATOR(), false); + assert_event_approval_for_all(OWNER(), OPERATOR(), false); + + assert( + !state.erc1155.is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: self approval',))] +fn test__set_approval_for_all_owner_equal_operator_true() { + let mut state = STATE(); + state.erc1155._set_approval_for_all(OWNER(), OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: self approval',))] +fn test__set_approval_for_all_owner_equal_operator_false() { + let mut state = STATE(); + state.erc1155._set_approval_for_all(OWNER(), OWNER(), false); +} + +// +// transfer_from & transferFrom +// + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test_update_balances_from_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.erc1155.transfer_from(OWNER(), ZERO(), TOKEN_ID, TOKEN_VALUE); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test_update_balancesFrom_to_zero() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + state.erc1155.transferFrom(OWNER(), ZERO(), TOKEN_ID, TOKEN_VALUE); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: unauthorized caller',))] +fn test_update_balances_from_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + state.erc1155.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: unauthorized caller',))] +fn test_update_balancesFrom_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + state.erc1155.transferFrom(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE); +} + +// +// safe_transfer_from & safeTransferFrom +// + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: safe transfer failed',))] +fn test_safe_transfer_from_to_receiver_failure() { + let mut state = setup(); + let receiver = setup_receiver(); + let token_id = TOKEN_ID; + let value = TOKEN_VALUE; + let owner = OWNER(); + + testing::set_caller_address(owner); + state.erc1155.safe_transfer_from(owner, receiver, token_id, value, DATA(false)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: safe transfer failed',))] +fn test_safeTransferFrom_to_receiver_failure() { + let mut state = setup(); + let receiver = setup_receiver(); + let token_id = TOKEN_ID; + let value = TOKEN_VALUE; + let owner = OWNER(); + + testing::set_caller_address(owner); + state.erc1155.safeTransferFrom(owner, receiver, token_id, value, DATA(false)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: safe transfer failed',))] +fn test_safeTransferFrom_to_receiver_failure_camel() { + let mut state = setup(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_ID; + let value = TOKEN_VALUE; + let owner = OWNER(); + + testing::set_caller_address(owner); + state.erc1155.safeTransferFrom(owner, receiver, token_id, value, DATA(false)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_safe_transfer_from_to_non_receiver() { + let mut state = setup(); + let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); + let token_id = TOKEN_ID; + let value = TOKEN_VALUE; + let owner = OWNER(); + + testing::set_caller_address(owner); + state.erc1155.safe_transfer_from(owner, recipient, token_id, value, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_safeTransferFrom_to_non_receiver() { + let mut state = setup(); + let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); + let token_id = TOKEN_ID; + let value = TOKEN_VALUE; + let owner = OWNER(); + + testing::set_caller_address(owner); + state.erc1155.safeTransferFrom(owner, recipient, token_id, value, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test_safe_transfer_from_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.erc1155.safe_transfer_from(OWNER(), ZERO(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test_safeTransferFrom_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + state.erc1155.safeTransferFrom(OWNER(), ZERO(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: unauthorized caller',))] +fn test_safe_transfer_from_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + state.erc1155.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: unauthorized caller',))] +fn test_safeTransferFrom_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + state.erc1155.safeTransferFrom(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +// +// _update_balances +// + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test__update_balances_to_zero() { + let mut state = setup(); + state.erc1155._update_balances(OWNER(), ZERO(), TOKEN_ID, TOKEN_VALUE); +} + +// +// _mint +// + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test__mint_to_zero() { + let mut state = STATE(); + state.erc1155._mint(ZERO(), TOKEN_ID, TOKEN_VALUE); +} + +// +// _safe_mint +// + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test__safe_mint_to_non_receiver() { + let mut state = STATE(); + let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); + let token_id = TOKEN_ID; + let value = TOKEN_VALUE; + + assert_state_before_mint(recipient, token_id); + state.erc1155._safe_mint(recipient, token_id, value, DATA(true)); + assert_state_after_mint(recipient, token_id, value); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: safe mint failed',))] +fn test__safe_mint_to_receiver_failure() { + let mut state = STATE(); + let recipient = setup_receiver(); + let token_id = TOKEN_ID; + let value = TOKEN_VALUE; + + assert_state_before_mint(recipient, token_id); + state.erc1155._safe_mint(recipient, token_id, value, DATA(false)); + assert_state_after_mint(recipient, token_id, value); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: safe mint failed',))] +fn test__safe_mint_to_receiver_failure_camel() { + let mut state = STATE(); + let recipient = setup_camel_receiver(); + let token_id = TOKEN_ID; + let value = TOKEN_VALUE; + + assert_state_before_mint(recipient, token_id); + state.erc1155._safe_mint(recipient, token_id, value, DATA(false)); + assert_state_after_mint(recipient, token_id, value); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test__safe_mint_to_zero() { + let mut state = STATE(); + state.erc1155._safe_mint(ZERO(), TOKEN_ID, TOKEN_VALUE, DATA(true)); +} + +// +// _set_uri +// + +#[test] +#[available_gas(20000000)] +fn test__set_uri() { + let mut state = setup(); + + assert(state.erc1155.uri(TOKEN_ID) == 0, 'URI should be 0'); + state.erc1155._set_uri(TOKEN_ID, URI); + assert(state.erc1155.uri(TOKEN_ID) == URI, 'URI should be set'); +} + +// +// Helpers +// + +fn assert_state_before_update_balances( + owner: ContractAddress, recipient: ContractAddress, token_id: u256, value: u256 +) { + let state = STATE(); + assert(state.erc1155.balance_of(owner, token_id) == value, 'Balance of owner before'); + assert(state.erc1155.balance_of(recipient, token_id) == 0, 'Balance of recipient before'); +} + +fn assert_state_after_update_balances( + owner: ContractAddress, recipient: ContractAddress, token_id: u256, value: u256 +) { + let state = STATE(); + assert(state.erc1155.balance_of(owner, token_id) == 0, 'Balance of owner after'); + assert(state.erc1155.balance_of(recipient, token_id) == value, 'Balance of recipient after'); +} + +fn assert_state_before_mint(recipient: ContractAddress, token_id: u256) { + let state = STATE(); + assert(state.erc1155.balance_of(recipient, token_id) == 0, 'Balance of recipient before'); +} + +fn assert_state_after_mint(recipient: ContractAddress, token_id: u256, value: u256) { + let state = STATE(); + assert(state.erc1155.balance_of(recipient, token_id) == value, 'Balance of recipient after'); +} + +fn assert_event_approval_for_all( + owner: ContractAddress, operator: ContractAddress, approved: bool +) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.operator == operator, 'Invalid `operator`'); + assert(event.approved == approved, 'Invalid `approved`'); + utils::assert_no_events_left(ZERO()); + + // Check indexed keys + let mut indexed_keys = array![]; + indexed_keys.append_serde(owner); + indexed_keys.append_serde(operator); + utils::assert_indexed_keys(event, indexed_keys.span()); +} + +fn assert_event_update_balances( + from: ContractAddress, to: ContractAddress, token_id: u256, value: u256 +) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.from == from, 'Invalid `from`'); + assert(event.to == to, 'Invalid `to`'); + assert(event.id == token_id, 'Invalid `token_id`'); + assert(event.value == value, 'Invalid `value`'); + utils::assert_no_events_left(ZERO()); + + // Check indexed keys + let mut indexed_keys = array![]; + indexed_keys.append_serde(from); + indexed_keys.append_serde(to); + indexed_keys.append_serde(token_id); + indexed_keys.append_serde(value); + utils::assert_indexed_keys(event, indexed_keys.span()); +} diff --git a/src/tests/utils/constants.cairo b/src/tests/utils/constants.cairo index 846a899fc..4cb15e2ab 100644 --- a/src/tests/utils/constants.cairo +++ b/src/tests/utils/constants.cairo @@ -12,6 +12,7 @@ const ROLE: felt252 = 'ROLE'; const OTHER_ROLE: felt252 = 'OTHER_ROLE'; const URI: felt252 = 'URI'; const TOKEN_ID: u256 = 21; +const TOKEN_VALUE: u256 = 42; const PUBKEY: felt252 = 'PUBKEY'; const NEW_PUBKEY: felt252 = 'NEW_PUBKEY'; const SALT: felt252 = 'SALT'; diff --git a/src/token.cairo b/src/token.cairo index f9a848d01..f5b6d2a37 100644 --- a/src/token.cairo +++ b/src/token.cairo @@ -1,2 +1,3 @@ +mod erc1155; mod erc20; mod erc721; diff --git a/src/token/erc1155.cairo b/src/token/erc1155.cairo new file mode 100644 index 000000000..43434d35d --- /dev/null +++ b/src/token/erc1155.cairo @@ -0,0 +1,8 @@ +mod dual1155; +mod dual1155_receiver; +mod erc1155; +mod erc1155_receiver; +mod interface; + +use erc1155::ERC1155Component; +use erc1155_receiver::ERC1155ReceiverComponent; diff --git a/src/token/erc1155/dual1155.cairo b/src/token/erc1155/dual1155.cairo new file mode 100644 index 000000000..6960fad5d --- /dev/null +++ b/src/token/erc1155/dual1155.cairo @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.8.0 (token/erc1155/dual1155.cairo) + +use openzeppelin::utils::UnwrapAndCast; +use openzeppelin::utils::selectors; +use openzeppelin::utils::serde::SerializedAppend; +use openzeppelin::utils::try_selector_with_fallback; +use starknet::ContractAddress; +use starknet::SyscallResultTrait; +use starknet::call_contract_syscall; + +#[derive(Copy, Drop)] +struct DualCaseERC1155 { + contract_address: ContractAddress +} + +trait DualCaseERC1155Trait { + fn supports_interface(self: @DualCaseERC1155, interface_id: felt252) -> bool; + fn name(self: @DualCaseERC1155) -> felt252; + fn symbol(self: @DualCaseERC1155) -> felt252; + fn uri(self: @DualCaseERC1155, token_uri: u256) -> felt252; + fn balance_of(self: @DualCaseERC1155, account: ContractAddress, token_id: u256) -> u256; + fn balance_of_batch( + self: @DualCaseERC1155, accounts: Span, token_ids: Span + ) -> Span; + fn safe_transfer_from( + self: @DualCaseERC1155, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256, + data: Span + ); + fn transfer_from( + self: @DualCaseERC1155, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256 + ); + fn safe_batch_transfer_from( + self: @DualCaseERC1155, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ); + fn batch_transfer_from( + self: @DualCaseERC1155, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span, + ); + fn is_approved_for_all( + self: @DualCaseERC1155, owner: ContractAddress, operator: ContractAddress + ) -> bool; + fn set_approval_for_all(self: @DualCaseERC1155, operator: ContractAddress, approved: bool); +} + +impl DualCaseERC1155Impl of DualCaseERC1155Trait { + fn supports_interface(self: @DualCaseERC1155, interface_id: felt252) -> bool { + let mut args = array![]; + args.append_serde(interface_id); + + try_selector_with_fallback( + *self.contract_address, + selectors::supports_interface, + selectors::supportsInterface, + args.span() + ) + .unwrap_and_cast() + } + + fn name(self: @DualCaseERC1155) -> felt252 { + call_contract_syscall(*self.contract_address, selectors::name, array![].span()) + .unwrap_and_cast() + } + + fn symbol(self: @DualCaseERC1155) -> felt252 { + call_contract_syscall(*self.contract_address, selectors::symbol, array![].span()) + .unwrap_and_cast() + } + + fn uri(self: @DualCaseERC1155, token_uri: u256) -> felt252 { + let mut args = array![]; + args.append_serde(token_uri); + + try_selector_with_fallback( + *self.contract_address, selectors::uri, selectors::uri, args.span() + ) + .unwrap_and_cast() + } + + fn balance_of(self: @DualCaseERC1155, account: ContractAddress, token_id: u256) -> u256 { + let mut args = array![]; + args.append_serde(account); + args.append_serde(token_id); + + try_selector_with_fallback( + *self.contract_address, selectors::balance_of, selectors::balanceOf, args.span() + ) + .unwrap_and_cast() + } + + fn balance_of_batch( + self: @DualCaseERC1155, accounts: Span, token_ids: Span + ) -> Span { + let mut args = array![]; + args.append_serde(accounts); + args.append_serde(token_ids); + + try_selector_with_fallback( + *self.contract_address, + selectors::balance_of_batch, + selectors::balanceOfBatch, + args.span() + ) + .unwrap_and_cast() + } + + fn safe_transfer_from( + self: @DualCaseERC1155, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) { + let mut args = array![]; + args.append_serde(from); + args.append_serde(to); + args.append_serde(token_id); + args.append_serde(value); + args.append_serde(data); + + try_selector_with_fallback( + *self.contract_address, + selectors::safe_transfer_from, + selectors::safeTransferFrom, + args.span() + ) + .unwrap_syscall(); + } + + fn transfer_from( + self: @DualCaseERC1155, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256 + ) { + let mut args = array![]; + args.append_serde(from); + args.append_serde(to); + args.append_serde(token_id); + args.append_serde(value); + + try_selector_with_fallback( + *self.contract_address, selectors::transfer_from, selectors::transferFrom, args.span() + ) + .unwrap_syscall(); + } + + fn safe_batch_transfer_from( + self: @DualCaseERC1155, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) { + let mut args = array![]; + args.append_serde(from); + args.append_serde(to); + args.append_serde(token_ids); + args.append_serde(values); + args.append_serde(data); + + try_selector_with_fallback( + *self.contract_address, + selectors::safe_batch_transfer_from, + selectors::safeBatchTransferFrom, + args.span() + ) + .unwrap_syscall(); + } + + fn batch_transfer_from( + self: @DualCaseERC1155, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span, + ) { + let mut args = array![]; + args.append_serde(from); + args.append_serde(to); + args.append_serde(token_ids); + args.append_serde(values); + + try_selector_with_fallback( + *self.contract_address, + selectors::batch_transfer_from, + selectors::batchTransferFrom, + args.span() + ) + .unwrap_syscall(); + } + + fn is_approved_for_all( + self: @DualCaseERC1155, owner: ContractAddress, operator: ContractAddress + ) -> bool { + let mut args = array![]; + args.append_serde(owner); + args.append_serde(operator); + + try_selector_with_fallback( + *self.contract_address, + selectors::is_approved_for_all, + selectors::isApprovedForAll, + args.span() + ) + .unwrap_and_cast() + } + + fn set_approval_for_all(self: @DualCaseERC1155, operator: ContractAddress, approved: bool) { + let mut args = array![]; + args.append_serde(operator); + args.append_serde(approved); + + try_selector_with_fallback( + *self.contract_address, + selectors::set_approval_for_all, + selectors::setApprovalForAll, + args.span() + ) + .unwrap_syscall(); + } +} diff --git a/src/token/erc1155/dual1155_receiver.cairo b/src/token/erc1155/dual1155_receiver.cairo new file mode 100644 index 000000000..6fafc5dcd --- /dev/null +++ b/src/token/erc1155/dual1155_receiver.cairo @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.8.0 (token/erc1155/dual1155_receiver.cairo) + +use openzeppelin::utils::UnwrapAndCast; +use openzeppelin::utils::selectors; +use openzeppelin::utils::serde::SerializedAppend; +use openzeppelin::utils::try_selector_with_fallback; +use starknet::ContractAddress; + +#[derive(Copy, Drop)] +struct DualCaseERC1155Receiver { + contract_address: ContractAddress +} + +trait DualCaseERC1155ReceiverTrait { + fn on_erc1155_received( + self: @DualCaseERC1155Receiver, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252; + + fn on_erc1155_batch_received( + self: @DualCaseERC1155Receiver, + operator: ContractAddress, + from: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> felt252; +} + +impl DualCaseERC1155ReceiverImpl of DualCaseERC1155ReceiverTrait { + fn on_erc1155_received( + self: @DualCaseERC1155Receiver, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252 { + let mut args = array![]; + args.append_serde(operator); + args.append_serde(from); + args.append_serde(token_id); + args.append_serde(value); + args.append_serde(data); + + try_selector_with_fallback( + *self.contract_address, + selectors::on_erc1155_received, + selectors::onERC1155Received, + args.span() + ) + .unwrap_and_cast() + } + + fn on_erc1155_batch_received( + self: @DualCaseERC1155Receiver, + operator: ContractAddress, + from: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> felt252 { + let mut args = array![]; + args.append_serde(operator); + args.append_serde(from); + args.append_serde(token_ids); + args.append_serde(values); + args.append_serde(data); + + try_selector_with_fallback( + *self.contract_address, + selectors::on_erc1155_batch_received, + selectors::onERC1155BatchReceived, + args.span() + ) + .unwrap_and_cast() + } +} diff --git a/src/token/erc1155/erc1155.cairo b/src/token/erc1155/erc1155.cairo new file mode 100644 index 000000000..dcbd72724 --- /dev/null +++ b/src/token/erc1155/erc1155.cairo @@ -0,0 +1,747 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.8.0 (token/erc1155/erc1155.cairo) + +/// # IERC1155 Component +/// +/// The IERC1155 component provides implementations for both the IIERC1155 interface +/// and the IIERC1155Metadata interface. +#[starknet::component] +mod ERC1155Component { + use openzeppelin::account; + use openzeppelin::introspection::dual_src5::{DualCaseSRC5, DualCaseSRC5Trait}; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::dual1155_receiver::{ + DualCaseERC1155Receiver, DualCaseERC1155ReceiverTrait + }; + use openzeppelin::token::erc1155::interface; + use starknet::ContractAddress; + use starknet::get_caller_address; + + #[storage] + struct Storage { + ERC1155_name: felt252, + ERC1155_symbol: felt252, + ERC1155_balances: LegacyMap<(u256, ContractAddress), u256>, + ERC1155_operator_approvals: LegacyMap<(ContractAddress, ContractAddress), bool>, + ERC1155_uri: LegacyMap, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + TransferSingle: TransferSingle, + TransferBatch: TransferBatch, + ApprovalForAll: ApprovalForAll, + URI: URI, + } + + /// Emitted when `value` token is transferred from `from` to `to` for `id`. + #[derive(Drop, starknet::Event)] + struct TransferSingle { + #[key] + from: ContractAddress, + #[key] + to: ContractAddress, + #[key] + id: u256, + value: u256 + } + + /// Emitted when `values` are transferred from `from` to `to` for `id`. + #[derive(Drop, starknet::Event)] + struct TransferBatch { + #[key] + operator: starknet::ContractAddress, + #[key] + from: starknet::ContractAddress, + #[key] + to: starknet::ContractAddress, + #[key] + ids: Span, + values: Span, + } + + /// Emitted when `owner` enables or disables (`approved`) `operator` to manage + /// all of its assets. + #[derive(Drop, starknet::Event)] + struct ApprovalForAll { + #[key] + owner: ContractAddress, + #[key] + operator: ContractAddress, + approved: bool + } + + /// Emitted when the `URI` is updated for a token `id`. + /// all of its assets. + #[derive(Drop, starknet::Event)] + struct URI { + value: felt252, + #[key] + id: u256, + } + + mod Errors { + const INVALID_TOKEN_ID: felt252 = 'ERC1155: invalid token ID'; + const INVALID_ACCOUNT: felt252 = 'ERC1155: invalid account'; + const UNAUTHORIZED: felt252 = 'ERC1155: unauthorized caller'; + const APPROVAL_TO_OWNER: felt252 = 'ERC1155: approval to owner'; + const SELF_APPROVAL: felt252 = 'ERC1155: self approval'; + const INVALID_RECEIVER: felt252 = 'ERC1155: invalid receiver'; + const ALREADY_MINTED: felt252 = 'ERC1155: token already minted'; + const WRONG_SENDER: felt252 = 'ERC1155: wrong sender'; + const SAFE_MINT_FAILED: felt252 = 'ERC1155: safe mint failed'; + const SAFE_TRANSFER_FAILED: felt252 = 'ERC1155: safe transfer failed'; + const INVALID_LEN_ACCOUNTS_IDS: felt252 = 'ERC1155: no equal array length'; + const INVALID_ARRAY_LENGTH: felt252 = 'ERC1155: invalid array length'; + const INSUFFICIENT_BALANCE: felt252 = 'ERC1155: insufficient balance'; + } + + // + // External + // + + #[embeddable_as(ERC1155Impl)] + impl ERC1155< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::IERC1155> { + /// Returns the number of value of NFT owned by `account`. + /// Retrieves the balance of a specific ERC1155 token for a given account. + /// + /// Parameters: + /// - account: The address of the account to check the balance for. + /// - token_id: The ID of the ERC1155 token. + /// + /// Returns: + /// - The balance of the specified ERC1155 token for the given account. + fn balance_of( + self: @ComponentState, account: ContractAddress, token_id: u256 + ) -> u256 { + assert(!account.is_zero(), Errors::INVALID_ACCOUNT); + self.ERC1155_balances.read((token_id, account)) + } + + + /// Retrieves the batch balances of multiple accounts for a given set of token IDs. + /// + /// # Arguments + /// + /// - `accounts`: A span of contract addresses representing the accounts to retrieve balances for. + /// - `token_ids`: A span of u256 values representing the token IDs to retrieve balances for. + /// + /// # Returns + /// + /// A span of u256 values representing the batch balances of the accounts for the specified token IDs. + /// + /// # Panics + /// + /// This function will panic if the length of `accounts` is not equal to the length of `token_ids`. + fn balance_of_batch( + self: @ComponentState, + accounts: Span, + token_ids: Span + ) -> Span { + assert(accounts.len() == token_ids.len(), Errors::INVALID_LEN_ACCOUNTS_IDS); + + let mut batch_balances = array![]; + let mut index = 0; + loop { + if index == token_ids.len() { + break batch_balances.clone(); + } + batch_balances.append(self.balance_of(*accounts.at(index), *token_ids.at(index))); + index += 1; + }; + + batch_balances.span() + } + + /// Transfers ownership of `token_id` from `from` if `to` is either an account or `IERC1155Receiver`. + /// + /// `data` is additional data, it has no specified format and it is sent in call to `to`. + /// + /// Requirements: + /// + /// - Caller is either approved or the `token_id` owner. + /// - `to` is not the zero address. + /// - `from` is not the zero address. + /// - `token_id` exists. + /// - `to` is either an account contract or supports the `IERC1155Receiver` interface. + /// + /// Emits a `Transfer` event. + fn safe_transfer_from( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) { + assert(self.is_approved_for_all(get_caller_address(), from), Errors::UNAUTHORIZED); + self._safe_update_balances(from, to, token_id, value, data); + } + + /// Transfers ownership of `token_id` from `from` to `to`. + /// + /// Requirements: + /// + /// - Caller is either approved or the `token_id` owner. + /// - `to` is not the zero address. + /// - `from` is not the zero address. + /// - `token_id` exists. + /// + /// Emits a `Transfer` event. + fn transfer_from( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256 + ) { + assert(self.is_approved_for_all(get_caller_address(), from), Errors::UNAUTHORIZED); + self._update_balances(from, to, token_id, value); + } + + fn safe_batch_transfer_from( + ref self: ComponentState, + from: starknet::ContractAddress, + to: starknet::ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + assert(from.is_non_zero(), Errors::WRONG_SENDER); + assert(self.is_approved_for_all(get_caller_address(), from), Errors::UNAUTHORIZED); + + self._safe_batch_transfer_from(from, to, token_ids, values, data); + } + + fn batch_transfer_from( + ref self: ComponentState, + from: starknet::ContractAddress, + to: starknet::ContractAddress, + token_ids: Span, + values: Span + ) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + assert(from.is_non_zero(), Errors::WRONG_SENDER); + assert(self.is_approved_for_all(get_caller_address(), from), Errors::UNAUTHORIZED); + + self._batch_transfer_from(from, to, token_ids, values); + } + + /// Enable or disable approval for `operator` to manage all of the + /// caller s assets. + /// + /// Requirements: + /// + /// - `operator` cannot be the caller. + /// + /// Emits an `Approval` event. + fn set_approval_for_all( + ref self: ComponentState, operator: ContractAddress, approved: bool + ) { + self._set_approval_for_all(get_caller_address(), operator, approved) + } + + /// Query if `operator` is an authorized operator for `owner`. + fn is_approved_for_all( + self: @ComponentState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.ERC1155_operator_approvals.read((owner, operator)) || owner == operator + } + } + + #[embeddable_as(ERC1155MetadataImpl)] + impl ERC1155Metadata< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::IERC1155Metadata> { + /// Returns the NFT Collection name. + fn name(self: @ComponentState) -> felt252 { + self.ERC1155_name.read() + } + + /// Returns the NFT Collection symbol. + fn symbol(self: @ComponentState) -> felt252 { + self.ERC1155_symbol.read() + } + /// Returns the Uniform Resource Identifier (URI) for the `token_id` token. + /// If the URI is not set for the `token_id`, the return value will be `0`. + /// + /// Requirements: + /// + /// - `token_id` exists. + fn uri(self: @ComponentState, token_id: u256) -> felt252 { + self.ERC1155_uri.read(token_id) + } + } + + /// Adds camelCase support for `IERC1155`. + #[embeddable_as(ERC1155CamelOnlyImpl)] + impl ERC1155CamelOnly< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::IERC1155CamelOnly> { + fn balanceOf( + self: @ComponentState, account: ContractAddress, tokenId: u256 + ) -> u256 { + self.balance_of(account, tokenId) + } + + fn balanceOfBatch( + self: @ComponentState, + accounts: Span, + tokenIds: Span + ) -> Span { + self.balance_of_batch(accounts, tokenIds) + } + + fn safeTransferFrom( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ) { + self.safe_transfer_from(from, to, tokenId, value, data) + } + + fn transferFrom( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + value: u256 + ) { + self.transfer_from(from, to, tokenId, value) + } + + fn safeBatchTransferFrom( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + tokenIds: Span, + values: Span, + data: Span + ) { + self.safe_batch_transfer_from(from, to, tokenIds, values, data) + } + + fn batchTransferFrom( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + tokenIds: Span, + values: Span + ) { + self.batch_transfer_from(from, to, tokenIds, values) + } + + fn setApprovalForAll( + ref self: ComponentState, operator: ContractAddress, approved: bool + ) { + self.set_approval_for_all(operator, approved) + } + + fn isApprovedForAll( + self: @ComponentState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.is_approved_for_all(owner, operator) + } + } + + // + // Internal + // + + #[generate_trait] + impl InternalImpl< + TContractState, + +HasComponent, + impl SRC5: SRC5Component::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the contract by setting the token name and symbol. + /// This should only be used inside the contract's constructor. + fn initializer(ref self: ComponentState, name: felt252, symbol: felt252) { + self.ERC1155_name.write(name); + self.ERC1155_symbol.write(symbol); + + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(interface::IERC1155_ID); + src5_component.register_interface(interface::IERC1155_METADATA_ID); + } + + /// Enables or disables approval for `operator` to manage + /// all of the `owner` assets. + /// + /// Requirements: + /// + /// - `operator` cannot be the caller. + /// + /// Emits an `Approval` event. + fn _set_approval_for_all( + ref self: ComponentState, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + assert(owner != operator, Errors::SELF_APPROVAL); + self.ERC1155_operator_approvals.write((owner, operator), approved); + self.emit(ApprovalForAll { owner, operator, approved }); + } + + fn _safe_batch_transfer_from( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + mut token_ids: Span, + mut values: Span, + data: Span + ) { + assert(token_ids.len() == values.len(), Errors::INVALID_ARRAY_LENGTH); + + loop { + if token_ids.len() == 0 { + break (); + } + let token_id = *token_ids.pop_front().unwrap(); + let value = *values.pop_front().unwrap(); + + self._safe_update_balances(from, to, token_id, value, data); + }; + + self + .emit( + TransferBatch { + operator: get_caller_address(), from, to, ids: token_ids, values + } + ); + } + + fn _batch_transfer_from( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + mut token_ids: Span, + mut values: Span + ) { + assert(token_ids.len() == values.len(), Errors::INVALID_ARRAY_LENGTH); + + loop { + if token_ids.len() == 0 { + break (); + } + let token_id = *token_ids.pop_front().unwrap(); + let value = *values.pop_front().unwrap(); + + self._update_balances(from, to, token_id, value); + }; + + self + .emit( + TransferBatch { + operator: get_caller_address(), from, to, ids: token_ids, values + } + ); + } + + /// Transfers ownership of `token_id` from `from` if `to` is either an account or `IERC1155Receiver`. + /// + /// `data` is additional data, it has no specified format and it is sent in call to `to`. + /// + /// Requirements: + /// + /// - `to` cannot be the zero address. + /// - `from` must be the token owner. + /// - `token_id` exists. + /// - `to` is either an account contract or supports the `IERC1155Receiver` interface. + /// + /// Emits a `Transfer` event. + fn _safe_update_balances( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) { + self._update_balances(from, to, token_id, value); + assert( + _check_on_ERC1155_received(from, to, token_id, value, data), + Errors::SAFE_TRANSFER_FAILED + ); + } + + /// Transfers `value` from `from` to `to` of the `token_id`. + /// + /// Internal function without access restriction. + /// + /// Requirements: + /// + /// - `to` is not the zero address. + /// - `from` is the token owner. + /// - `token_id` exists. + /// - `value` exists. + /// + /// Emits a `Transfer` event. + fn _update_balances( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256 + ) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + assert(from.is_non_zero(), Errors::WRONG_SENDER); + assert( + self.ERC1155_balances.read((token_id, from)) >= value, Errors::INSUFFICIENT_BALANCE + ); + + self + .ERC1155_balances + .write((token_id, from), self.ERC1155_balances.read((token_id, from)) - value); + self + .ERC1155_balances + .write((token_id, to), self.ERC1155_balances.read((token_id, to)) + value); + + self.emit(TransferSingle { from, to, id: token_id, value }); + } + + fn _batch_burn( + ref self: ComponentState, + from: ContractAddress, + mut token_ids: Span, + mut values: Span + ) { + assert(token_ids.len() == values.len(), Errors::INVALID_ARRAY_LENGTH); + + loop { + if token_ids.len() == 0 { + break (); + } + let token_id = *token_ids.pop_front().unwrap(); + let value = *values.pop_front().unwrap(); + + self._burn(from, token_id, value); + }; + + self + .emit( + TransferBatch { + operator: get_caller_address(), + from, + to: Zeroable::zero(), + ids: token_ids, + values + } + ); + } + + /// Destroys `value`. The approval is cleared when the token is burned. + /// + /// This internal function does not check if the caller is authorized + /// to operate on the token. + /// + /// Requirements: + /// + /// - `value` >= balances. + /// + /// Emits a `Transfer` event. + fn _burn( + ref self: ComponentState, + from: ContractAddress, + token_id: u256, + value: u256 + ) { + assert( + self.ERC1155_balances.read((token_id, from)) >= value, Errors::INSUFFICIENT_BALANCE + ); + + self._update_balances(from, Zeroable::zero(), token_id, value); + + self.emit(TransferSingle { from, to: Zeroable::zero(), id: token_id, value }); + } + + /// batch Mints `values` and transfers it to `to`. + /// + /// `data` is additional data, it has no specified format and it is sent in call to `to`. + /// + /// Requirements: + /// + /// - `to` is either an account contract or supports the `IERC1155Receiver` interface. + /// + /// Emits a `Transfer` event. + fn _batch_mint( + ref self: ComponentState, + to: ContractAddress, + mut token_ids: Span, + mut values: Span + ) { + assert(token_ids.len() == values.len(), Errors::INVALID_ARRAY_LENGTH); + + loop { + if token_ids.len() == 0 { + break (); + } + let token_id = *token_ids.pop_front().unwrap(); + let value = *values.pop_front().unwrap(); + + self._mint(to, token_id, value); + }; + + self + .emit( + TransferBatch { + operator: get_caller_address(), + from: Zeroable::zero(), + to, + ids: token_ids, + values + } + ); + } + + /// Mints `values` and transfers it to `to`. + /// Internal function without access restriction. + /// + /// Requirements: + /// + /// - `to` is not the zero address. + /// + /// Emits a `Transfer` event. + fn _mint( + ref self: ComponentState, + to: ContractAddress, + token_id: u256, + value: u256 + ) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + + self + .ERC1155_balances + .write((token_id, to), self.ERC1155_balances.read((token_id, to)) + value); + + self.emit(TransferSingle { from: Zeroable::zero(), to, id: token_id, value }); + } + + /// Safe batch Mints `values` and transfers it to `to`. + /// + /// `data` is additional data, it has no specified format and it is sent in call to `to`. + /// + /// Requirements: + /// + /// - `to` is either an account contract or supports the `IERC1155Receiver` interface. + /// + /// Emits a `Transfer` event. + fn _safe_batch_mint( + ref self: ComponentState, + to: ContractAddress, + mut token_ids: Span, + mut values: Span, + data: Span + ) { + assert(token_ids.len() == values.len(), Errors::INVALID_ARRAY_LENGTH); + + loop { + if token_ids.len() == 0 { + break (); + } + let token_id = *token_ids.pop_front().unwrap(); + let value = *values.pop_front().unwrap(); + + self._safe_mint(to, token_id, value, data); + }; + + self + .emit( + TransferBatch { + operator: get_caller_address(), + from: Zeroable::zero(), + to, + ids: token_ids, + values + } + ); + } + + /// Mints `value` if `to` is either an account or `IERC1155Receiver`. + /// + /// `data` is additional data, it has no specified format and it is sent in call to `to`. + /// + /// Requirements: + /// + /// - `to` is either an account contract or supports the `IERC1155Receiver` interface. + /// + /// Emits a `Transfer` event. + fn _safe_mint( + ref self: ComponentState, + to: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) { + self._mint(to, token_id, value); + assert( + _check_on_ERC1155_received(Zeroable::zero(), to, token_id, value, data), + Errors::SAFE_MINT_FAILED + ); + } + + /// Sets the `uri` of `token_id`. + fn _set_uri(ref self: ComponentState, token_id: u256, uri: felt252) { + self.ERC1155_uri.write(token_id, uri); + + self.emit(URI { value: uri, id: token_id }); + } + } + + /// Checks if `to` either is an account contract or has registered support + /// for the `IERC1155Receiver` interface through SRC5. The transaction will + /// fail if both cases are false. + fn _check_on_ERC1155_received( + from: ContractAddress, to: ContractAddress, token_id: u256, value: u256, data: Span + ) -> bool { + if (DualCaseSRC5 { contract_address: to } + .supports_interface(interface::IERC1155_RECEIVER_ID)) { + DualCaseERC1155Receiver { contract_address: to } + .on_erc1155_received( + get_caller_address(), from, token_id, value, data + ) == interface::IERC1155_RECEIVER_ID + } else { + DualCaseSRC5 { contract_address: to }.supports_interface(account::interface::ISRC6_ID) + } + } + + /// Checks if `to` either is an account contract or has registered support + /// for the `IERC1155Receiver` interface through SRC5. The transaction will + /// fail if both cases are false. + fn _check_on_ERC1155_batch_received( + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> bool { + if (DualCaseSRC5 { contract_address: to } + .supports_interface(interface::IERC1155_RECEIVER_ID)) { + DualCaseERC1155Receiver { contract_address: to } + .on_erc1155_batch_received( + get_caller_address(), from, token_ids, values, data + ) == interface::IERC1155_RECEIVER_ID + } else { + DualCaseSRC5 { contract_address: to }.supports_interface(account::interface::ISRC6_ID) + } + } +} diff --git a/src/token/erc1155/erc1155_receiver.cairo b/src/token/erc1155/erc1155_receiver.cairo new file mode 100644 index 000000000..75370bee9 --- /dev/null +++ b/src/token/erc1155/erc1155_receiver.cairo @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.8.0 (token/erc1155/erc1155_receiver.cairo) + +/// # ERC1155Receiver Component +/// +/// The ERC1155Receiver component provides implementations for the IERC1155Receiver +/// interface. Integrating this component allows contracts to support ERC1155 +/// safe transfers. +#[starknet::component] +mod ERC1155ReceiverComponent { + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; + use openzeppelin::token::erc1155::interface::{IERC1155Receiver, IERC1155ReceiverCamel}; + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[embeddable_as(ERC1155ReceiverImpl)] + impl ERC1155Receiver< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of IERC1155Receiver> { + /// Called whenever the implementing contract receives `value` through + /// a safe transfer. This function must return `IERC1155_RECEIVER_ID` + /// to confirm the token transfer. + fn on_erc1155_received( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252 { + IERC1155_RECEIVER_ID + } + + fn on_erc1155_batch_received( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> felt252 { + IERC1155_RECEIVER_ID + } + } + + /// Adds camelCase support for `IERC1155Receiver`. + #[embeddable_as(ERC1155ReceiverCamelImpl)] + impl ERC1155ReceiverCamel< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of IERC1155ReceiverCamel> { + fn onERC1155Received( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ) -> felt252 { + IERC1155_RECEIVER_ID + } + + fn onERC1155BatchReceived( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + tokenIds: Span, + values: Span, + data: Span + ) -> felt252 { + IERC1155_RECEIVER_ID + } + } + + #[generate_trait] + impl InternalImpl< + TContractState, + +HasComponent, + impl SRC5: SRC5Component::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the contract by registering the IERC1155Receiver interface ID. + /// This should be used inside the contract's constructor. + fn initializer(ref self: ComponentState) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(IERC1155_RECEIVER_ID); + } + } +} diff --git a/src/token/erc1155/interface.cairo b/src/token/erc1155/interface.cairo new file mode 100644 index 000000000..5a8f6ddbc --- /dev/null +++ b/src/token/erc1155/interface.cairo @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.8.0 (token/erc1155/interface.cairo) + +use starknet::ContractAddress; + +const IERC1155_ID: felt252 = 0xdef955e77a50cefb767c39f5e3bacb4d24f75e2de1d930ae214fcd6f7d42f3; +const IERC1155_METADATA_ID: felt252 = + 0x3d7b708e1a6bd1a69c8d4deedf7ad6adc6cda9cc81bd97c49dc1c82e172d1fc; +const IERC1155_RECEIVER_ID: felt252 = + 0x15e8665b5af20040c3af1670509df02eb916375cdf7d8cbaf7bd553a257515e; + + +#[starknet::interface] +trait IERC1155 { + fn balance_of(self: @TState, account: ContractAddress, token_id: u256) -> u256; + fn balance_of_batch( + self: @TState, accounts: Span, token_ids: Span + ) -> Span; + fn safe_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256, + data: Span + ); + fn transfer_from( + ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256, value: u256 + ); + fn safe_batch_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ); + fn batch_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span, + ); + fn is_approved_for_all( + self: @TState, owner: ContractAddress, operator: ContractAddress + ) -> bool; + fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); +} + +#[starknet::interface] +trait IERC1155Metadata { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn uri(self: @TState, token_id: u256) -> felt252; +} + +#[starknet::interface] +trait IERC1155CamelOnly { + fn balanceOf(self: @TState, account: ContractAddress, tokenId: u256) -> u256; + fn balanceOfBatch( + self: @TState, accounts: Span, tokenIds: Span + ) -> Span; + fn safeTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ); + fn transferFrom( + ref self: TState, from: ContractAddress, to: ContractAddress, tokenId: u256, value: u256 + ); + fn safeBatchTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + tokenIds: Span, + values: Span, + data: Span + ); + fn batchTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + tokenIds: Span, + values: Span, + ); + fn isApprovedForAll(self: @TState, owner: ContractAddress, operator: ContractAddress) -> bool; + fn setApprovalForAll(ref self: TState, operator: ContractAddress, approved: bool); +} + +// +// ERC1155 ABI +// + +#[starknet::interface] +trait ERC1155ABI { + // IERC1155 + fn balance_of(self: @TState, account: ContractAddress, token_id: u256) -> u256; + fn balance_of_batch( + self: @TState, accounts: Span, token_ids: Span + ) -> Span; + fn safe_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + value: u256, + data: Span + ); + fn transfer_from( + ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256, value: u256 + ); + fn safe_batch_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ); + fn batch_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span + ); + fn is_approved_for_all( + self: @TState, owner: ContractAddress, operator: ContractAddress + ) -> bool; + fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); + + // ISRC5 + fn supports_interface(self: @TState, interface_id: felt252) -> bool; + + // IERC1155Metadata + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn uri(self: @TState, token_id: u256) -> felt252; + + // IERC1155CamelOnly + fn balanceOf(self: @TState, account: ContractAddress, tokenId: u256) -> u256; + fn balanceOfBatch( + self: @TState, accounts: Span, tokenIds: Span + ) -> Span; + fn safeTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ); + fn transferFrom( + ref self: TState, from: ContractAddress, to: ContractAddress, tokenId: u256, value: u256 + ); + fn safeBatchTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + tokenIds: Span, + values: Span, + data: Span + ); + fn batchTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + tokenIds: Span, + values: Span + ); + fn isApprovedForAll(self: @TState, owner: ContractAddress, operator: ContractAddress) -> bool; + fn setApprovalForAll(ref self: TState, operator: ContractAddress, approved: bool); + + // ISRC5Camel + fn supportsInterface(self: @TState, interfaceId: felt252) -> bool; +} + +// +// IERC1155Receiver +// + +#[starknet::interface] +trait IERC1155Receiver { + fn on_erc1155_received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252; + fn on_erc1155_batch_received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> felt252; +} +#[starknet::interface] +trait IERC1155ReceiverCamel { + fn onERC1155Received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ) -> felt252; + fn onERC1155BatchReceived( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + tokenIds: Span, + values: Span, + data: Span + ) -> felt252; +} diff --git a/src/utils/selectors.cairo b/src/utils/selectors.cairo index 197524c16..3a4b7c966 100644 --- a/src/utils/selectors.cairo +++ b/src/utils/selectors.cairo @@ -57,6 +57,30 @@ const safeTransferFrom: felt252 = selector!("safeTransferFrom"); const on_erc721_received: felt252 = selector!("on_erc721_received"); const onERC721Received: felt252 = selector!("onERC721Received"); +// +// ERC1155 +// + +// The following ERC1155 selectors are already defined in ERC721 above: +// balance_of, balanceOf, transfer_from, transferFrom, approve, set_approval_for_all, +// setApprovalForAll, safe_transfer_from, safeTransferFrom +const uri: felt252 = selector!("uri"); +const balance_of_batch: felt252 = selector!("balance_of_batch"); +const balanceOfBatch: felt252 = selector!("balanceOfBatch"); +const safe_batch_transfer_from: felt252 = selector!("safe_batch_transfer_from"); +const safeBatchTransferFrom: felt252 = selector!("safeBatchTransferFrom"); +const batch_transfer_from: felt252 = selector!("batch_transfer_from"); +const batchTransferFrom: felt252 = selector!("batchTransferFrom"); + +// +// ERC1155Receiver +// + +const on_erc1155_received: felt252 = selector!("on_erc1155_received"); +const onERC1155Received: felt252 = selector!("onERC1155Received"); +const on_erc1155_batch_received: felt252 = selector!("on_erc1155_batch_received"); +const onERC1155BatchReceived: felt252 = selector!("onERC1155BatchReceived"); + // // ERC20 //