From 20c91c2687aa7f91851ebacdf5c69907642e513c Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Sat, 25 Nov 2023 00:32:31 -0500 Subject: [PATCH] Add ERC721 preset (#811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * consolidate mocks * add erc721abi * migrate to component * use abi dispatcher * start updating tests * add pop_log_comp_indexed * update tests * fix formatting * add erc721camelabi * remove unnecessary trait * add code comments * change import style * Update src/token/erc721/erc721.cairo Co-authored-by: Eric Nordelo * remove unnecessary fn * change back to pop_log * flatten event in mock * flatten events in mocks * change receiver to component * fix formatting * simplify camel return id * fix imports * fix imports * fix tokenURI * add Component suffix * add Component suffix to receiver * fix formatting * fix formatting * add receiver tests * fix formatting and test * remove import * add code comments * fix conflicts * simplify with get_dep_component_mut * add recipient to constructor * remove unused mocks * simplify mocks * fix formatting * add erc721 preset * add erc721 preset * start preset tests * fix formatting * fix test * finish tests * fix formatting * remove import * Update src/tests/token/test_erc721.cairo Co-authored-by: Eric Nordelo * fix impl order * Apply suggestions from code review Co-authored-by: Eric Nordelo * change param to span * add constructor tests * fix formatting * add in-code comments * fix fn order * fix title * Apply suggestions from code review Co-authored-by: Martín Triay * fix formatting * fix impl order * fix src5 comment * add info in comments * fix component impl order * change back import style * fix component order * remove extra line * remove unused imports * fix comment * add drop_events * reorder preset components * fix tests * set owner as caller in setup --------- Co-authored-by: Eric Nordelo Co-authored-by: Martín Triay --- src/presets.cairo | 2 + src/presets/erc721.cairo | 99 +++ src/tests/presets.cairo | 1 + src/tests/presets/test_erc721.cairo | 1118 +++++++++++++++++++++++++++ src/tests/utils.cairo | 11 + 5 files changed, 1231 insertions(+) create mode 100644 src/presets/erc721.cairo create mode 100644 src/tests/presets/test_erc721.cairo diff --git a/src/presets.cairo b/src/presets.cairo index a167d8e1c..6d3ddc6f6 100644 --- a/src/presets.cairo +++ b/src/presets.cairo @@ -1,5 +1,7 @@ mod account; mod erc20; +mod erc721; use account::Account; use erc20::ERC20; +use erc721::ERC721; diff --git a/src/presets/erc721.cairo b/src/presets/erc721.cairo new file mode 100644 index 000000000..c9a6c16cb --- /dev/null +++ b/src/presets/erc721.cairo @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.8.0-beta.0 (presets/erc721.cairo) + +/// # ERC721 Preset +/// +/// The ERC721 contract offers a batch-mint mechanism that +/// can only be executed once upon contract construction. +#[starknet::contract] +mod ERC721 { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::ERC721Component; + use starknet::ContractAddress; + + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // ERC721 + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; + #[abi(embed_v0)] + impl ERC721CamelOnly = ERC721Component::ERC721CamelOnlyImpl; + #[abi(embed_v0)] + impl ERC721MetadataCamelOnly = + ERC721Component::ERC721MetadataCamelOnlyImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + mod Errors { + const UNEQUAL_ARRAYS: felt252 = 'Array lengths do not match'; + } + + /// Sets the token `name` and `symbol`. + /// Mints the `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, + token_uris: Span + ) { + self.erc721.initializer(name, symbol); + self._mint_assets(recipient, token_ids, token_uris); + } + + /// Mints `token_ids` to `recipient`. + /// Sets the token URI from `token_uris` to the corresponding + /// token ID of `token_ids`. + /// + /// Requirements: + /// + /// - `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 token_uris: Span + ) { + assert(token_ids.len() == token_uris.len(), Errors::UNEQUAL_ARRAYS); + + loop { + if token_ids.len() == 0 { + break; + } + let id = *token_ids.pop_front().unwrap(); + let uri = *token_uris.pop_front().unwrap(); + + self.erc721._mint(recipient, id); + self.erc721._set_token_uri(id, uri); + } + } + } +} diff --git a/src/tests/presets.cairo b/src/tests/presets.cairo index 9121ed966..15f206215 100644 --- a/src/tests/presets.cairo +++ b/src/tests/presets.cairo @@ -1,2 +1,3 @@ mod test_account; mod test_erc20; +mod test_erc721; diff --git a/src/tests/presets/test_erc721.cairo b/src/tests/presets/test_erc721.cairo new file mode 100644 index 000000000..2f2c42e74 --- /dev/null +++ b/src/tests/presets/test_erc721.cairo @@ -0,0 +1,1118 @@ +use openzeppelin::account::AccountComponent; +use openzeppelin::introspection::interface::ISRC5_ID; +use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin::presets::ERC721::InternalImpl; +use openzeppelin::presets::ERC721; +use openzeppelin::tests::mocks::account_mocks::{DualCaseAccountMock, CamelAccountMock}; +use openzeppelin::tests::mocks::erc721_receiver_mocks::{ + CamelERC721ReceiverMock, SnakeERC721ReceiverMock +}; +use openzeppelin::tests::mocks::non_implementing_mock::NonImplementingMock; +use openzeppelin::tests::utils::constants::{ + ZERO, DATA, OWNER, SPENDER, RECIPIENT, OTHER, OPERATOR, PUBKEY, NAME, SYMBOL +}; +use openzeppelin::tests::utils; +use openzeppelin::token::erc721::ERC721Component::InternalImpl as ERC721ComponentInternalTrait; +use openzeppelin::token::erc721::ERC721Component::{Approval, ApprovalForAll, Transfer}; +use openzeppelin::token::erc721::ERC721Component::{ERC721CamelOnlyImpl, ERC721Impl}; +use openzeppelin::token::erc721::ERC721Component::{ERC721MetadataImpl, ERC721MetadataCamelOnlyImpl}; +use openzeppelin::token::erc721::interface::ERC721ABI; +use openzeppelin::token::erc721::interface::{ERC721ABIDispatcher, ERC721ABIDispatcherTrait}; +use openzeppelin::token::erc721::interface::{IERC721_ID, IERC721_METADATA_ID}; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use starknet::testing; + + +// Token IDs +const TOKEN_1: u256 = 1; +const TOKEN_2: u256 = 2; +const TOKEN_3: u256 = 3; +const NONEXISTENT: u256 = 9898; + +const TOKENS_LEN: u256 = 3; + +// Token URIs +const URI_1: felt252 = 'URI_1'; +const URI_2: felt252 = 'URI_2'; +const URI_3: felt252 = 'URI_3'; + +// +// Setup +// + +fn setup_dispatcher_with_event() -> ERC721ABIDispatcher { + let mut calldata = array![]; + let mut token_ids = array![TOKEN_1, TOKEN_2, TOKEN_3]; + let mut token_uris = array![URI_1, URI_2, URI_3]; + + // Set caller as `OWNER` + testing::set_contract_address(OWNER()); + + calldata.append_serde(NAME); + calldata.append_serde(SYMBOL); + calldata.append_serde(OWNER()); + calldata.append_serde(token_ids); + calldata.append_serde(token_uris); + + let address = utils::deploy(ERC721::TEST_CLASS_HASH, calldata); + ERC721ABIDispatcher { contract_address: address } +} + +fn setup_dispatcher() -> ERC721ABIDispatcher { + let dispatcher = setup_dispatcher_with_event(); + utils::drop_events(dispatcher.contract_address, TOKENS_LEN.try_into().unwrap()); + dispatcher +} + +fn setup_receiver() -> ContractAddress { + utils::deploy(SnakeERC721ReceiverMock::TEST_CLASS_HASH, array![]) +} + +fn setup_camel_receiver() -> ContractAddress { + utils::deploy(CamelERC721ReceiverMock::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) +} + +// +// _mint_assets +// + +#[test] +#[available_gas(2000000000)] +fn test__mint_assets() { + let mut state = ERC721::contract_state_for_testing(); + let mut token_ids = array![TOKEN_1, TOKEN_2, TOKEN_3].span(); + let mut token_uris = array![URI_1, URI_2, URI_3].span(); + + state._mint_assets(OWNER(), token_ids, token_uris); + + assert(state.erc721.balance_of(OWNER()) == TOKENS_LEN, 'Should equal IDs length'); + + loop { + if token_ids.len() == 0 { + break; + } + + let id = *token_ids.pop_front().unwrap(); + let uri = *token_uris.pop_front().unwrap(); + + assert(state.erc721.owner_of(id) == OWNER(), 'Should be owned by OWNER'); + assert(state.erc721.token_uri(id) == uri, 'Should equal correct URI'); + }; +} + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('Array lengths do not match',))] +fn test__mint_assets_mismatched_arrays_1() { + let mut state = ERC721::contract_state_for_testing(); + + let token_ids = array![TOKEN_1, TOKEN_2, TOKEN_3].span(); + let short_uris = array![URI_1, URI_2].span(); + state._mint_assets(OWNER(), token_ids, short_uris); +} + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('Array lengths do not match',))] +fn test__mint_assets_mismatched_arrays_2() { + let mut state = ERC721::contract_state_for_testing(); + + let short_ids = array![TOKEN_1, TOKEN_2].span(); + let token_uris = array![URI_1, URI_2, URI_3].span(); + state._mint_assets(OWNER(), short_ids, token_uris); +} + +// +// constructor +// + +#[test] +#[available_gas(2000000000)] +fn test_constructor() { + let dispatcher = setup_dispatcher_with_event(); + + // Check interface registration + let mut interface_ids = array![ISRC5_ID, IERC721_ID, IERC721_METADATA_ID]; + loop { + let id = interface_ids.pop_front().unwrap(); + if interface_ids.len() == 0 { + break; + } + assert(dispatcher.supports_interface(id), 'Should support interface'); + }; + + // Check token balance and owner + let mut tokens = array![TOKEN_1, TOKEN_2, TOKEN_3]; + assert(dispatcher.balance_of(OWNER()) == TOKENS_LEN, 'Should equal TOKENS_LEN'); + loop { + let token = tokens.pop_front().unwrap(); + if tokens.len() == 0 { + break; + } + assert(dispatcher.owner_of(token) == OWNER(), 'Should be owned by OWNER'); + }; +} + +#[test] +#[available_gas(2000000000)] +fn test_constructor_events() { + let dispatcher = setup_dispatcher_with_event(); + let mut tokens = array![TOKEN_1, TOKEN_2, TOKEN_3]; + + loop { + let token = tokens.pop_front().unwrap(); + if tokens.len() == 0 { + // Includes event queue check + assert_only_event_transfer(dispatcher.contract_address, ZERO(), OWNER(), token); + break; + } + assert_event_transfer(dispatcher.contract_address, ZERO(), OWNER(), token); + }; +} + +// +// Getters +// + +#[test] +#[available_gas(20000000)] +fn test_balance_of() { + let dispatcher = setup_dispatcher(); + assert(dispatcher.balance_of(OWNER()) == TOKENS_LEN, 'Should return balance'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid account', 'ENTRYPOINT_FAILED'))] +fn test_balance_of_zero() { + let dispatcher = setup_dispatcher(); + dispatcher.balance_of(ZERO()); +} + +#[test] +#[available_gas(20000000)] +fn test_owner_of() { + let dispatcher = setup_dispatcher(); + assert(dispatcher.owner_of(TOKEN_1) == OWNER(), 'Should return owner'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED'))] +fn test_owner_of_non_minted() { + let dispatcher = setup_dispatcher(); + dispatcher.owner_of(7); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED'))] +fn test_token_uri_non_minted() { + let dispatcher = setup_dispatcher(); + dispatcher.token_uri(7); +} + +#[test] +#[available_gas(20000000)] +fn test_get_approved() { + let dispatcher = setup_dispatcher(); + let spender = SPENDER(); + let token_id = TOKEN_1; + + assert(dispatcher.get_approved(token_id) == ZERO(), 'Should return non-approval'); + + dispatcher.approve(spender, token_id); + assert(dispatcher.get_approved(token_id) == spender, 'Should return approval'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED'))] +fn test_get_approved_nonexistent() { + let dispatcher = setup_dispatcher(); + dispatcher.get_approved(NONEXISTENT); +} + +// +// approve +// + +#[test] +#[available_gas(20000000)] +fn test_approve_from_owner() { + let dispatcher = setup_dispatcher(); + + dispatcher.approve(SPENDER(), TOKEN_1); + assert_event_approval(dispatcher.contract_address, OWNER(), SPENDER(), TOKEN_1); + + assert(dispatcher.get_approved(TOKEN_1) == SPENDER(), 'Spender not approved correctly'); +} + +#[test] +#[available_gas(20000000)] +fn test_approve_from_operator() { + let dispatcher = setup_dispatcher(); + + dispatcher.set_approval_for_all(OPERATOR(), true); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.approve(SPENDER(), TOKEN_1); + assert_event_approval(dispatcher.contract_address, OWNER(), SPENDER(), TOKEN_1); + + assert(dispatcher.get_approved(TOKEN_1) == SPENDER(), 'Spender not approved correctly'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller', 'ENTRYPOINT_FAILED'))] +fn test_approve_from_unauthorized() { + let dispatcher = setup_dispatcher(); + + testing::set_contract_address(OTHER()); + dispatcher.approve(SPENDER(), TOKEN_1); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: approval to owner', 'ENTRYPOINT_FAILED'))] +fn test_approve_to_owner() { + let dispatcher = setup_dispatcher(); + + dispatcher.approve(OWNER(), TOKEN_1); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED'))] +fn test_approve_nonexistent() { + let dispatcher = setup_dispatcher(); + dispatcher.approve(SPENDER(), NONEXISTENT); +} + +// +// set_approval_for_all +// + +#[test] +#[available_gas(20000000)] +fn test_set_approval_for_all() { + let dispatcher = setup_dispatcher(); + + assert(!dispatcher.is_approved_for_all(OWNER(), OPERATOR()), 'Invalid default value'); + + dispatcher.set_approval_for_all(OPERATOR(), true); + assert_event_approval_for_all(dispatcher.contract_address, OWNER(), OPERATOR(), true); + + assert(dispatcher.is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly'); + + dispatcher.set_approval_for_all(OPERATOR(), false); + assert_event_approval_for_all(dispatcher.contract_address, OWNER(), OPERATOR(), false); + + assert(!dispatcher.is_approved_for_all(OWNER(), OPERATOR()), 'Approval not revoked correctly'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval', 'ENTRYPOINT_FAILED'))] +fn test_set_approval_for_all_owner_equal_operator_true() { + let dispatcher = setup_dispatcher(); + dispatcher.set_approval_for_all(OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval', 'ENTRYPOINT_FAILED'))] +fn test_set_approval_for_all_owner_equal_operator_false() { + let dispatcher = setup_dispatcher(); + dispatcher.set_approval_for_all(OWNER(), false); +} + +// +// transfer_from & transferFrom +// + +#[test] +#[available_gas(20000000)] +fn test_transfer_from_owner() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let owner = OWNER(); + let recipient = RECIPIENT(); + + // set approval to check reset + dispatcher.approve(OTHER(), token_id); + utils::drop_event(dispatcher.contract_address); + + assert_state_before_transfer(dispatcher, owner, recipient, token_id); + assert(dispatcher.get_approved(token_id) == OTHER(), 'Approval not implicitly reset'); + + dispatcher.transfer_from(owner, recipient, token_id); + assert_only_event_transfer(dispatcher.contract_address, owner, recipient, token_id); + + assert_state_after_transfer(dispatcher, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_transferFrom_owner() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let owner = OWNER(); + let recipient = RECIPIENT(); + + // set approval to check reset + dispatcher.approve(OTHER(), token_id); + utils::drop_event(dispatcher.contract_address); + + assert_state_before_transfer(dispatcher, owner, recipient, token_id); + assert(dispatcher.get_approved(token_id) == OTHER(), 'Approval not implicitly reset'); + + dispatcher.transferFrom(owner, recipient, token_id); + assert_only_event_transfer(dispatcher.contract_address, owner, recipient, token_id); + + assert_state_after_transfer(dispatcher, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED'))] +fn test_transfer_from_nonexistent() { + let dispatcher = setup_dispatcher(); + dispatcher.transfer_from(OWNER(), RECIPIENT(), NONEXISTENT); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED'))] +fn test_transferFrom_nonexistent() { + let dispatcher = setup_dispatcher(); + dispatcher.transferFrom(OWNER(), RECIPIENT(), NONEXISTENT); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver', 'ENTRYPOINT_FAILED'))] +fn test_transfer_from_to_zero() { + let dispatcher = setup_dispatcher(); + dispatcher.transfer_from(OWNER(), ZERO(), TOKEN_1); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver', 'ENTRYPOINT_FAILED'))] +fn test_transferFrom_to_zero() { + let dispatcher = setup_dispatcher(); + dispatcher.transferFrom(OWNER(), ZERO(), TOKEN_1); +} + +#[test] +#[available_gas(20000000)] +fn test_transfer_from_to_owner() { + let dispatcher = setup_dispatcher(); + + assert_state_transfer_to_self(dispatcher, OWNER(), TOKEN_1, TOKENS_LEN); + dispatcher.transfer_from(OWNER(), OWNER(), TOKEN_1); + assert_only_event_transfer(dispatcher.contract_address, OWNER(), OWNER(), TOKEN_1); + + assert_state_transfer_to_self(dispatcher, OWNER(), TOKEN_1, TOKENS_LEN); +} + +#[test] +#[available_gas(20000000)] +fn test_transferFrom_to_owner() { + let dispatcher = setup_dispatcher(); + + assert_state_transfer_to_self(dispatcher, OWNER(), TOKEN_1, TOKENS_LEN); + dispatcher.transferFrom(OWNER(), OWNER(), TOKEN_1); + assert_only_event_transfer(dispatcher.contract_address, OWNER(), OWNER(), TOKEN_1); + + assert_state_transfer_to_self(dispatcher, OWNER(), TOKEN_1, TOKENS_LEN); +} + +#[test] +#[available_gas(20000000)] +fn test_transfer_from_approved() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let owner = OWNER(); + let recipient = RECIPIENT(); + assert_state_before_transfer(dispatcher, owner, recipient, token_id); + + dispatcher.approve(OPERATOR(), token_id); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.transfer_from(owner, recipient, token_id); + assert_only_event_transfer(dispatcher.contract_address, owner, recipient, token_id); + + assert_state_after_transfer(dispatcher, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_transferFrom_approved() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let owner = OWNER(); + let recipient = RECIPIENT(); + assert_state_before_transfer(dispatcher, owner, recipient, token_id); + + dispatcher.approve(OPERATOR(), token_id); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.transferFrom(owner, recipient, token_id); + assert_only_event_transfer(dispatcher.contract_address, owner, recipient, token_id); + + assert_state_after_transfer(dispatcher, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_transfer_from_approved_for_all() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(dispatcher, owner, recipient, token_id); + + dispatcher.set_approval_for_all(OPERATOR(), true); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.transfer_from(owner, recipient, token_id); + assert_only_event_transfer(dispatcher.contract_address, owner, recipient, token_id); + + assert_state_after_transfer(dispatcher, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_transferFrom_approved_for_all() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(dispatcher, owner, recipient, token_id); + + dispatcher.set_approval_for_all(OPERATOR(), true); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.transferFrom(owner, recipient, token_id); + assert_only_event_transfer(dispatcher.contract_address, owner, recipient, token_id); + + assert_state_after_transfer(dispatcher, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller', 'ENTRYPOINT_FAILED'))] +fn test_transfer_from_unauthorized() { + let dispatcher = setup_dispatcher(); + testing::set_contract_address(OTHER()); + dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_1); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller', 'ENTRYPOINT_FAILED'))] +fn test_transferFrom_unauthorized() { + let dispatcher = setup_dispatcher(); + testing::set_contract_address(OTHER()); + dispatcher.transferFrom(OWNER(), RECIPIENT(), TOKEN_1); +} + +// +// safe_transfer_from & safeTransferFrom +// + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_to_account() { + let dispatcher = setup_dispatcher(); + let account = setup_account(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, account, token_id); + + dispatcher.safe_transfer_from(owner, account, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, account, token_id); + + assert_state_after_transfer(dispatcher, owner, account, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_to_account() { + let dispatcher = setup_dispatcher(); + let account = setup_account(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, account, token_id); + + dispatcher.safeTransferFrom(owner, account, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, account, token_id); + + assert_state_after_transfer(dispatcher, owner, account, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_to_account_camel() { + let dispatcher = setup_dispatcher(); + let account = setup_camel_account(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, account, token_id); + + dispatcher.safe_transfer_from(owner, account, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, account, token_id); + + assert_state_after_transfer(dispatcher, owner, account, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_to_account_camel() { + let dispatcher = setup_dispatcher(); + let account = setup_camel_account(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, account, token_id); + + dispatcher.safeTransferFrom(owner, account, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, account, token_id); + + assert_state_after_transfer(dispatcher, owner, account, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_to_receiver() { + let dispatcher = setup_dispatcher(); + let receiver = setup_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_to_receiver() { + let dispatcher = setup_dispatcher(); + let receiver = setup_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_to_receiver_camel() { + let dispatcher = setup_dispatcher(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_to_receiver_camel() { + let dispatcher = setup_dispatcher(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: safe transfer failed', 'ENTRYPOINT_FAILED'))] +fn test_safe_transfer_from_to_receiver_failure() { + let dispatcher = setup_dispatcher(); + let receiver = setup_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(false)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: safe transfer failed', 'ENTRYPOINT_FAILED'))] +fn test_safeTransferFrom_to_receiver_failure() { + let dispatcher = setup_dispatcher(); + let receiver = setup_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(false)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: safe transfer failed', 'ENTRYPOINT_FAILED'))] +fn test_safe_transfer_from_to_receiver_failure_camel() { + let dispatcher = setup_dispatcher(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(false)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: safe transfer failed', 'ENTRYPOINT_FAILED'))] +fn test_safeTransferFrom_to_receiver_failure_camel() { + let dispatcher = setup_dispatcher(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(false)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] +fn test_safe_transfer_from_to_non_receiver() { + let dispatcher = setup_dispatcher(); + let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); + let token_id = TOKEN_1; + let owner = OWNER(); + + dispatcher.safe_transfer_from(owner, recipient, token_id, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] +fn test_safeTransferFrom_to_non_receiver() { + let dispatcher = setup_dispatcher(); + let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); + let token_id = TOKEN_1; + let owner = OWNER(); + + dispatcher.safeTransferFrom(owner, recipient, token_id, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED'))] +fn test_safe_transfer_from_nonexistent() { + let dispatcher = setup_dispatcher(); + dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), NONEXISTENT, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED'))] +fn test_safeTransferFrom_nonexistent() { + let dispatcher = setup_dispatcher(); + dispatcher.safeTransferFrom(OWNER(), RECIPIENT(), NONEXISTENT, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver', 'ENTRYPOINT_FAILED'))] +fn test_safe_transfer_from_to_zero() { + let dispatcher = setup_dispatcher(); + dispatcher.safe_transfer_from(OWNER(), ZERO(), TOKEN_1, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver', 'ENTRYPOINT_FAILED'))] +fn test_safeTransferFrom_to_zero() { + let dispatcher = setup_dispatcher(); + dispatcher.safeTransferFrom(OWNER(), ZERO(), TOKEN_1, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_to_owner() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let receiver = setup_receiver(); + + dispatcher.transfer_from(OWNER(), receiver, token_id); + utils::drop_event(dispatcher.contract_address); + + assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); + + testing::set_contract_address(receiver); + dispatcher.safe_transfer_from(receiver, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, receiver, receiver, token_id); + + assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_to_owner() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let receiver = setup_receiver(); + + dispatcher.transfer_from(OWNER(), receiver, token_id); + utils::drop_event(dispatcher.contract_address); + + assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); + + testing::set_contract_address(receiver); + dispatcher.safeTransferFrom(receiver, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, receiver, receiver, token_id); + + assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_to_owner_camel() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let receiver = setup_camel_receiver(); + + dispatcher.transfer_from(OWNER(), receiver, token_id); + utils::drop_event(dispatcher.contract_address); + + assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); + + testing::set_contract_address(receiver); + dispatcher.safe_transfer_from(receiver, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, receiver, receiver, token_id); + + assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_to_owner_camel() { + let dispatcher = setup_dispatcher(); + let token_id = TOKEN_1; + let receiver = setup_camel_receiver(); + + dispatcher.transfer_from(OWNER(), receiver, token_id); + utils::drop_event(dispatcher.contract_address); + + assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); + + testing::set_contract_address(receiver); + dispatcher.safeTransferFrom(receiver, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, receiver, receiver, token_id); + + assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_approved() { + let dispatcher = setup_dispatcher(); + let receiver = setup_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.approve(OPERATOR(), token_id); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_approved() { + let dispatcher = setup_dispatcher(); + let receiver = setup_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.approve(OPERATOR(), token_id); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_approved_camel() { + let dispatcher = setup_dispatcher(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.approve(OPERATOR(), token_id); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_approved_camel() { + let dispatcher = setup_dispatcher(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.approve(OPERATOR(), token_id); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_approved_for_all() { + let dispatcher = setup_dispatcher(); + let receiver = setup_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.set_approval_for_all(OPERATOR(), true); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_approved_for_all() { + let dispatcher = setup_dispatcher(); + let receiver = setup_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.set_approval_for_all(OPERATOR(), true); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safe_transfer_from_approved_for_all_camel() { + let dispatcher = setup_dispatcher(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.set_approval_for_all(OPERATOR(), true); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +fn test_safeTransferFrom_approved_for_all_camel() { + let dispatcher = setup_dispatcher(); + let receiver = setup_camel_receiver(); + let token_id = TOKEN_1; + let owner = OWNER(); + + assert_state_before_transfer(dispatcher, owner, receiver, token_id); + + dispatcher.set_approval_for_all(OPERATOR(), true); + utils::drop_event(dispatcher.contract_address); + + testing::set_contract_address(OPERATOR()); + dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); + assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); + + assert_state_after_transfer(dispatcher, owner, receiver, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller', 'ENTRYPOINT_FAILED'))] +fn test_safe_transfer_from_unauthorized() { + let dispatcher = setup_dispatcher(); + testing::set_contract_address(OTHER()); + dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_1, DATA(true)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller', 'ENTRYPOINT_FAILED'))] +fn test_safeTransferFrom_unauthorized() { + let dispatcher = setup_dispatcher(); + testing::set_contract_address(OTHER()); + dispatcher.safeTransferFrom(OWNER(), RECIPIENT(), TOKEN_1, DATA(true)); +} + +// +// Helpers +// + +fn assert_state_before_transfer( + dispatcher: ERC721ABIDispatcher, + owner: ContractAddress, + recipient: ContractAddress, + token_id: u256 +) { + assert(dispatcher.owner_of(token_id) == owner, 'Ownership before'); + assert(dispatcher.balance_of(owner) == TOKENS_LEN, 'Balance of owner before'); + assert(dispatcher.balance_of(recipient) == 0, 'Balance of recipient before'); +} + +fn assert_state_after_transfer( + dispatcher: ERC721ABIDispatcher, + owner: ContractAddress, + recipient: ContractAddress, + token_id: u256 +) { + assert(dispatcher.owner_of(token_id) == recipient, 'Ownership after'); + assert(dispatcher.balance_of(owner) == TOKENS_LEN - 1, 'Balance of owner after'); + assert(dispatcher.balance_of(recipient) == 1, 'Balance of recipient after'); + assert(dispatcher.get_approved(token_id) == ZERO(), 'Approval not implicitly reset'); +} + +fn assert_state_transfer_to_self( + dispatcher: ERC721ABIDispatcher, target: ContractAddress, token_id: u256, token_balance: u256 +) { + assert(dispatcher.owner_of(token_id) == target, 'Ownership before'); + assert(dispatcher.balance_of(target) == token_balance, 'Balance of owner before'); +} + +fn assert_event_approval_for_all( + contract: ContractAddress, owner: ContractAddress, operator: ContractAddress, approved: bool +) { + let event = utils::pop_log::(contract).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.operator == operator, 'Invalid `operator`'); + assert(event.approved == approved, 'Invalid `approved`'); + utils::assert_no_events_left(contract); + + // 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_approval( + contract: ContractAddress, owner: ContractAddress, approved: ContractAddress, token_id: u256 +) { + let event = utils::pop_log::(contract).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.approved == approved, 'Invalid `approved`'); + assert(event.token_id == token_id, 'Invalid `token_id`'); + utils::assert_no_events_left(contract); + + // Check indexed keys + let mut indexed_keys = array![]; + indexed_keys.append_serde(owner); + indexed_keys.append_serde(approved); + indexed_keys.append_serde(token_id); + utils::assert_indexed_keys(event, indexed_keys.span()); +} + +fn assert_event_transfer( + contract: ContractAddress, from: ContractAddress, to: ContractAddress, token_id: u256 +) { + let event = utils::pop_log::(contract).unwrap(); + assert(event.from == from, 'Invalid `from`'); + assert(event.to == to, 'Invalid `to`'); + assert(event.token_id == token_id, 'Invalid `token_id`'); + + // Check indexed keys + let mut indexed_keys = array![]; + indexed_keys.append_serde(from); + indexed_keys.append_serde(to); + indexed_keys.append_serde(token_id); + utils::assert_indexed_keys(event, indexed_keys.span()); +} + +fn assert_only_event_transfer( + contract: ContractAddress, from: ContractAddress, to: ContractAddress, value: u256 +) { + assert_event_transfer(contract, from, to, value); + utils::assert_no_events_left(contract); +} diff --git a/src/tests/utils.cairo b/src/tests/utils.cairo index 1de7d0488..56d8544c9 100644 --- a/src/tests/utils.cairo +++ b/src/tests/utils.cairo @@ -53,3 +53,14 @@ fn assert_no_events_left(address: ContractAddress) { fn drop_event(address: ContractAddress) { testing::pop_log_raw(address); } + +fn drop_events(address: ContractAddress, count: felt252) { + let mut _count = count; + loop { + if _count == 0 { + break; + } + drop_event(address); + _count -= 1; + } +}