From d86eb73879c0ae2ef30ab7762ea26235d6c425e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Triay?= Date: Tue, 30 May 2023 15:36:53 -0300 Subject: [PATCH] Migrate ERC721 (#619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * continue account implementation * add missing account interface functions * tidy up module * fix validate * bump cairo + account changes * fix __execute__, add serde, rename felt>felt252 * tidy up code * WIP ERC721 * make format * fix dispatcher call * clean * working on tests * replace match with `is_some()` and `is_none()` for readeability * erc721 tests * use Option.expect * add account tests * check panic reason * rename _owner() to _owner_of() * spacing * complete account implementation * apply recommandation for PR * Apply suggestions from code review Co-authored-by: Andrew Fleming * check low level ownership int * prefix test function with test_ * Apply suggestions from code review Co-authored-by: Andrew Fleming Co-authored-by: Hadrien Croubois * apply review suggestions * remove unused import * clarify __execute__ guard * add account tests * add internals * tidy up * update ERC165 ids to u32 * apply sugestions from code review * Apply suggestions from code review Co-authored-by: Martín Triay * update & expand tests * update lock * add internal macro * add internal macro * add deregister * update erc165 * wip (dispatched issue) * fix abi * start array→span transition * minimise account dependency * add SpanSerde in utils * add dual interfaces * update test message. fix linter * split interfaces from preset module * fix linter * rename metadata id var * add camelCase to traits * fully implement dual interface traits * simplify _owner_of * add constructor and getter tests * add token_uri tests * remove conflictive test * add erc721receiver. add IERC721ABI * add safe_transfer_from tests * add safe_mint tests * Update src/openzeppelin/token/erc721/interface.cairo Co-authored-by: Eric Nordelo * move erc721 abi next to module * address review comments --------- Co-authored-by: Hadrien Croubois Co-authored-by: Andrew Fleming Co-authored-by: Andrew Fleming Co-authored-by: Eric Nordelo --- src/openzeppelin/introspection/erc165.cairo | 1 + src/openzeppelin/tests.cairo | 3 +- src/openzeppelin/tests/mocks.cairo | 1 + .../tests/mocks/erc721_receiver.cairo | 62 ++ src/openzeppelin/tests/test_erc721.cairo | 791 ++++++++++++++++++ src/openzeppelin/token.cairo | 1 + src/openzeppelin/token/erc721.cairo | 3 + src/openzeppelin/token/erc721/erc721.cairo | 462 ++++++++++ src/openzeppelin/token/erc721/interface.cairo | 71 ++ src/openzeppelin/utils.cairo | 1 + src/openzeppelin/utils/serde.cairo | 19 + 11 files changed, 1414 insertions(+), 1 deletion(-) create mode 100644 src/openzeppelin/tests/mocks/erc721_receiver.cairo create mode 100644 src/openzeppelin/tests/test_erc721.cairo create mode 100644 src/openzeppelin/token/erc721.cairo create mode 100644 src/openzeppelin/token/erc721/erc721.cairo create mode 100644 src/openzeppelin/token/erc721/interface.cairo create mode 100644 src/openzeppelin/utils/serde.cairo diff --git a/src/openzeppelin/introspection/erc165.cairo b/src/openzeppelin/introspection/erc165.cairo index 16f13205a..4a3b01778 100644 --- a/src/openzeppelin/introspection/erc165.cairo +++ b/src/openzeppelin/introspection/erc165.cairo @@ -1,6 +1,7 @@ const IERC165_ID: u32 = 0x01ffc9a7_u32; const INVALID_ID: u32 = 0xffffffff_u32; +#[abi] trait IERC165 { fn supports_interface(interface_id: u32) -> bool; } diff --git a/src/openzeppelin/tests.cairo b/src/openzeppelin/tests.cairo index e13d78136..5aa74d96f 100644 --- a/src/openzeppelin/tests.cairo +++ b/src/openzeppelin/tests.cairo @@ -4,7 +4,8 @@ mod test_ownable; mod test_erc165; mod test_account; mod test_erc20; -mod test_pausable; +mod test_erc721; mod test_initializable; +mod test_pausable; mod mocks; mod utils; diff --git a/src/openzeppelin/tests/mocks.cairo b/src/openzeppelin/tests/mocks.cairo index ca522a93c..b8f645b23 100644 --- a/src/openzeppelin/tests/mocks.cairo +++ b/src/openzeppelin/tests/mocks.cairo @@ -1,3 +1,4 @@ mod reentrancy_attacker_mock; mod reentrancy_mock; +mod erc721_receiver; mod mock_pausable; diff --git a/src/openzeppelin/tests/mocks/erc721_receiver.cairo b/src/openzeppelin/tests/mocks/erc721_receiver.cairo new file mode 100644 index 000000000..c9e848630 --- /dev/null +++ b/src/openzeppelin/tests/mocks/erc721_receiver.cairo @@ -0,0 +1,62 @@ +const SUCCESS: felt252 = 123123; +const FAILURE: felt252 = 456456; + +#[contract] +mod ERC721Receiver { + use openzeppelin::token::erc721::interface::IERC721Receiver; + use openzeppelin::token::erc721::interface::IERC721ReceiverCamel; + use openzeppelin::token::erc721::interface::IERC721_RECEIVER_ID; + use openzeppelin::introspection::erc165::ERC165; + + use openzeppelin::utils::serde::SpanSerde; + use starknet::ContractAddress; + use array::SpanTrait; + + impl ERC721ReceiverImpl of IERC721Receiver { + fn on_erc721_received( + operator: ContractAddress, from: ContractAddress, token_id: u256, data: Span + ) -> u32 { + if *data.at(0) == super::SUCCESS { + IERC721_RECEIVER_ID + } else { + 0 + } + } + } + + impl ERC721ReceiverCamelImpl of IERC721ReceiverCamel { + fn onERC721Received( + operator: ContractAddress, from: ContractAddress, tokenId: u256, data: Span + ) -> u32 { + ERC721ReceiverImpl::on_erc721_received(operator, from, tokenId, data) + } + } + + #[constructor] + fn constructor() { + ERC165::register_interface(IERC721_RECEIVER_ID); + } + + #[view] + fn supports_interface(interface_id: u32) -> bool { + ERC165::supports_interface(interface_id) + } + + #[external] + fn on_erc721_received( + operator: ContractAddress, from: ContractAddress, token_id: u256, data: Span + ) -> u32 { + ERC721ReceiverImpl::on_erc721_received(operator, from, token_id, data) + } + + #[external] + fn onERC721Received( + operator: ContractAddress, from: ContractAddress, tokenId: u256, data: Span + ) -> u32 { + ERC721ReceiverCamelImpl::onERC721Received(operator, from, tokenId, data) + } +} + + +#[contract] +mod ERC721NonReceiver {} diff --git a/src/openzeppelin/tests/test_erc721.cairo b/src/openzeppelin/tests/test_erc721.cairo new file mode 100644 index 000000000..3b81360ae --- /dev/null +++ b/src/openzeppelin/tests/test_erc721.cairo @@ -0,0 +1,791 @@ +use openzeppelin::introspection::erc165; +use openzeppelin::token::erc721; +use openzeppelin::token::erc721::ERC721; +use openzeppelin::account::Account; + +use openzeppelin::tests::utils; +use openzeppelin::tests::mocks::erc721_receiver::ERC721Receiver; +use openzeppelin::tests::mocks::erc721_receiver::ERC721NonReceiver; +use openzeppelin::tests::mocks::erc721_receiver::SUCCESS; +use openzeppelin::tests::mocks::erc721_receiver::FAILURE; + +use starknet::contract_address_const; +use starknet::ContractAddress; +use starknet::testing::set_caller_address; +use integer::u256; +use integer::u256_from_felt252; +use array::ArrayTrait; +use traits::Into; +use zeroable::Zeroable; + +const NAME: felt252 = 111; +const SYMBOL: felt252 = 222; +const URI: felt252 = 333; + +fn TOKEN_ID() -> u256 { + 7.into() +} + +fn ZERO() -> ContractAddress { + Zeroable::zero() +} +fn OWNER() -> ContractAddress { + contract_address_const::<10>() +} +fn RECIPIENT() -> ContractAddress { + contract_address_const::<20>() +} +fn SPENDER() -> ContractAddress { + contract_address_const::<30>() +} +fn OPERATOR() -> ContractAddress { + contract_address_const::<40>() +} +fn OTHER() -> ContractAddress { + contract_address_const::<50>() +} + +fn DATA(success: bool) -> Span { + let mut data = ArrayTrait::new(); + if success { + data.append(SUCCESS); + } else { + data.append(FAILURE); + } + data.span() +} + +/// +/// Setup +/// + +fn setup() { + ERC721::initializer(NAME, SYMBOL); + ERC721::_mint(OWNER(), TOKEN_ID()); +} + +fn setup_receiver() -> ContractAddress { + utils::deploy(ERC721Receiver::TEST_CLASS_HASH, ArrayTrait::new()) +} + +fn setup_account() -> ContractAddress { + let mut calldata = ArrayTrait::new(); + let public_key: felt252 = 1234678; + calldata.append(public_key); + utils::deploy(Account::TEST_CLASS_HASH, calldata) +} + +/// +/// Initializers +/// + +#[test] +#[available_gas(2000000)] +fn test_constructor() { + ERC721::constructor(NAME, SYMBOL); + + assert(ERC721::name() == NAME, 'Name should be NAME'); + assert(ERC721::symbol() == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC721::balance_of(OWNER()) == 0.into(), 'Balance should be zero'); + + assert(ERC721::supports_interface(erc721::interface::IERC721_ID), 'Missing interface ID'); + assert( + ERC721::supports_interface(erc721::interface::IERC721_METADATA_ID), 'missing interface ID' + ); + assert(ERC721::supports_interface(erc165::IERC165_ID), 'missing interface ID'); + assert(!ERC721::supports_interface(erc165::INVALID_ID), 'invalid interface ID'); +} + +#[test] +#[available_gas(2000000)] +fn test_initialize() { + ERC721::initializer(NAME, SYMBOL); + + assert(ERC721::name() == NAME, 'Name should be NAME'); + assert(ERC721::symbol() == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC721::balance_of(OWNER()) == 0.into(), 'Balance should be zero'); + + assert(ERC721::supports_interface(erc721::interface::IERC721_ID), 'Missing interface ID'); + assert( + ERC721::supports_interface(erc721::interface::IERC721_METADATA_ID), 'missing interface ID' + ); + assert(ERC721::supports_interface(erc165::IERC165_ID), 'missing interface ID'); + assert(!ERC721::supports_interface(erc165::INVALID_ID), 'invalid interface ID'); +} + +/// +/// Getters +/// + +#[test] +#[available_gas(2000000)] +fn test_balance_of() { + setup(); + assert(ERC721::balance_of(OWNER()) == 1.into(), 'Should return balance'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid account', ))] +fn test_balance_of_zero() { + ERC721::balance_of(ZERO()); +} + +#[test] +#[available_gas(2000000)] +fn test_owner_of() { + setup(); + assert(ERC721::owner_of(TOKEN_ID()) == OWNER(), 'Should return owner'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test_owner_of_non_minted() { + ERC721::owner_of(u256_from_felt252(7)); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test_token_uri_non_minted() { + ERC721::token_uri(u256_from_felt252(7)); +} + +#[test] +#[available_gas(2000000)] +fn test_get_approved() { + setup(); + let spender = SPENDER(); + let token_id = TOKEN_ID(); + + assert(ERC721::get_approved(token_id) == ZERO(), 'Should return non-approval'); + ERC721::_approve(spender, token_id); + assert(ERC721::get_approved(token_id) == spender, 'Should return approval'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test_get_approved_nonexistent() { + ERC721::get_approved(u256_from_felt252(7)); +} + +#[test] +#[available_gas(2000000)] +fn test__exists() { + let zero = ZERO(); + let token_id = TOKEN_ID(); + assert(!ERC721::_exists(token_id), 'Token should not exist'); + assert(ERC721::_owners::read(token_id) == zero, 'Invalid owner'); + + ERC721::_mint(RECIPIENT(), token_id); + + assert(ERC721::_exists(token_id), 'Token should exist'); + assert(ERC721::_owners::read(token_id) == RECIPIENT(), 'Invalid owner'); + + ERC721::_burn(token_id); + + assert(!ERC721::_exists(token_id), 'Token should not exist'); + assert(ERC721::_owners::read(token_id) == zero, 'Invalid owner'); +} + +/// +/// approve & _approve +/// + +#[test] +#[available_gas(2000000)] +fn test_approve_from_owner() { + setup(); + + set_caller_address(OWNER()); + ERC721::approve(SPENDER(), TOKEN_ID()); + assert(ERC721::get_approved(TOKEN_ID()) == SPENDER(), 'Spender not approved correctly'); +} + +#[test] +#[available_gas(2000000)] +fn test_approve_from_operator() { + setup(); + + set_caller_address(OWNER()); + ERC721::set_approval_for_all(OPERATOR(), true); + + set_caller_address(OPERATOR()); + ERC721::approve(SPENDER(), TOKEN_ID()); + assert(ERC721::get_approved(TOKEN_ID()) == SPENDER(), 'Spender not approved correctly'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: unauthorized caller', ))] +fn test_approve_from_unauthorized() { + setup(); + + set_caller_address(OTHER()); + ERC721::approve(SPENDER(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: approval to owner', ))] +fn test_approve_to_owner() { + setup(); + + set_caller_address(OWNER()); + ERC721::approve(OWNER(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test_approve_nonexistent() { + ERC721::approve(SPENDER(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +fn test__approve() { + setup(); + + ERC721::_approve(SPENDER(), TOKEN_ID()); + assert(ERC721::get_approved(TOKEN_ID()) == SPENDER(), 'Spender not approved correctly'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: approval to owner', ))] +fn test__approve_to_owner() { + setup(); + + ERC721::_approve(OWNER(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test__approve_nonexistent() { + ERC721::_approve(SPENDER(), TOKEN_ID()); +} + +/// +/// set_approval_for_all & _set_approval_for_all +/// + +#[test] +#[available_gas(2000000)] +fn test_set_approval_for_all() { + set_caller_address(OWNER()); + assert(!ERC721::is_approved_for_all(OWNER(), OPERATOR()), 'Invalid default value'); + + ERC721::set_approval_for_all(OPERATOR(), true); + assert(ERC721::is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly'); + + ERC721::set_approval_for_all(OPERATOR(), false); + assert(!ERC721::is_approved_for_all(OWNER(), OPERATOR()), 'Approval not revoked correctly'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: self approval', ))] +fn test_set_approval_for_all_owner_equal_operator_true() { + set_caller_address(OWNER()); + ERC721::set_approval_for_all(OWNER(), true); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: self approval', ))] +fn test_set_approval_for_all_owner_equal_operator_false() { + set_caller_address(OWNER()); + ERC721::set_approval_for_all(OWNER(), false); +} + +#[test] +#[available_gas(2000000)] +fn test__set_approval_for_all() { + assert(!ERC721::is_approved_for_all(OWNER(), OPERATOR()), 'Invalid default value'); + + ERC721::_set_approval_for_all(OWNER(), OPERATOR(), true); + assert(ERC721::is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly'); + + ERC721::_set_approval_for_all(OWNER(), OPERATOR(), false); + assert(!ERC721::is_approved_for_all(OWNER(), OPERATOR()), 'Operator not approved correctly'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: self approval', ))] +fn test__set_approval_for_all_owner_equal_operator_true() { + ERC721::_set_approval_for_all(OWNER(), OWNER(), true); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: self approval', ))] +fn test__set_approval_for_all_owner_equal_operator_false() { + ERC721::_set_approval_for_all(OWNER(), OWNER(), false); +} + +/// +/// transfer_from +/// + +#[test] +#[available_gas(2000000)] +fn test_transfer_from_owner() { + setup(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + let recipient = RECIPIENT(); + // set approval to check reset + ERC721::_approve(OTHER(), token_id); + + assert_state_before_transfer(token_id, owner, recipient); + assert(ERC721::get_approved(token_id) == OTHER(), 'Approval not implicitly reset'); + + set_caller_address(owner); + ERC721::transfer_from(owner, recipient, token_id); + + assert_state_after_transfer(token_id, owner, recipient); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test_transfer_from_nonexistent() { + ERC721::transfer_from(ZERO(), RECIPIENT(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid receiver', ))] +fn test_transfer_from_to_zero() { + setup(); + + set_caller_address(OWNER()); + ERC721::transfer_from(OWNER(), ZERO(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +fn test_transfer_from_to_owner() { + setup(); + + assert(ERC721::owner_of(TOKEN_ID()) == OWNER(), 'Ownership before'); + assert(ERC721::balance_of(OWNER()) == 1.into(), 'Balance of owner before'); + + set_caller_address(OWNER()); + ERC721::transfer_from(OWNER(), OWNER(), TOKEN_ID()); + + assert(ERC721::owner_of(TOKEN_ID()) == OWNER(), 'Ownership after'); + assert(ERC721::balance_of(OWNER()) == 1.into(), 'Balance of owner after'); +} + +#[test] +#[available_gas(2000000)] +fn test_transfer_from_approved() { + setup(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + let recipient = RECIPIENT(); + assert_state_before_transfer(token_id, owner, recipient); + + set_caller_address(owner); + ERC721::approve(OPERATOR(), token_id); + + set_caller_address(OPERATOR()); + ERC721::transfer_from(owner, recipient, token_id); + + assert_state_after_transfer(token_id, owner, recipient); +} + +#[test] +#[available_gas(2000000)] +fn test_transfer_from_approved_for_all() { + setup(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(token_id, owner, recipient); + + set_caller_address(owner); + ERC721::set_approval_for_all(OPERATOR(), true); + + set_caller_address(OPERATOR()); + ERC721::transfer_from(owner, recipient, token_id); + + assert_state_after_transfer(token_id, owner, recipient); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: unauthorized caller', ))] +fn test_transfer_from_unauthorized() { + setup(); + + set_caller_address(OTHER()); + ERC721::transfer_from(OWNER(), RECIPIENT(), TOKEN_ID()); +} + +// +// safe_transfer_from +// + +#[test] +#[available_gas(2000000)] +fn test_safe_transfer_from_to_account() { + setup(); + let account = setup_account(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + + assert_state_before_transfer(token_id, owner, account); + + set_caller_address(owner); + ERC721::safe_transfer_from(owner, account, token_id, DATA(true)); + + assert_state_after_transfer(token_id, owner, account); +} + +#[test] +#[available_gas(2000000)] +fn test_safe_transfer_from_to_receiver() { + setup(); + let receiver = setup_receiver(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + + assert_state_before_transfer(token_id, owner, receiver); + + set_caller_address(owner); + ERC721::safe_transfer_from(owner, receiver, token_id, DATA(true)); + + assert_state_after_transfer(token_id, owner, receiver); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: safe transfer failed', ))] +fn test_safe_transfer_from_to_receiver_failure() { + setup(); + let receiver = setup_receiver(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + + set_caller_address(owner); + ERC721::safe_transfer_from(owner, receiver, token_id, DATA(false)); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', ))] +fn test_safe_transfer_from_to_non_receiver() { + setup(); + let recipient = utils::deploy(ERC721NonReceiver::TEST_CLASS_HASH, ArrayTrait::new()); + let token_id = TOKEN_ID(); + let owner = OWNER(); + + set_caller_address(owner); + ERC721::safe_transfer_from(owner, recipient, token_id, DATA(true)); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test_safe_transfer_from_nonexistent() { + ERC721::safe_transfer_from(ZERO(), RECIPIENT(), TOKEN_ID(), DATA(true)); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid receiver', ))] +fn test_safe_transfer_from_to_zero() { + setup(); + + set_caller_address(OWNER()); + ERC721::safe_transfer_from(OWNER(), ZERO(), TOKEN_ID(), DATA(true)); +} + +#[test] +#[available_gas(2000000)] +fn test_safe_transfer_from_to_owner() { + let token_id = TOKEN_ID(); + let owner = setup_receiver(); + ERC721::initializer(NAME, SYMBOL); + ERC721::_mint(owner, token_id); + + assert(ERC721::owner_of(token_id) == owner, 'Ownership before'); + assert(ERC721::balance_of(owner) == 1.into(), 'Balance of owner before'); + + set_caller_address(owner); + ERC721::safe_transfer_from(owner, owner, token_id, DATA(true)); + + assert(ERC721::owner_of(token_id) == owner, 'Ownership after'); + assert(ERC721::balance_of(owner) == 1.into(), 'Balance of owner after'); +} + +#[test] +#[available_gas(2000000)] +fn test_safe_transfer_from_approved() { + setup(); + let receiver = setup_receiver(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + + assert_state_before_transfer(token_id, owner, receiver); + + set_caller_address(owner); + ERC721::approve(OPERATOR(), token_id); + + set_caller_address(OPERATOR()); + ERC721::safe_transfer_from(owner, receiver, token_id, DATA(true)); + + assert_state_after_transfer(token_id, owner, receiver); +} + +#[test] +#[available_gas(2000000)] +fn test_safe_transfer_from_approved_for_all() { + setup(); + let receiver = setup_receiver(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + + assert_state_before_transfer(token_id, owner, receiver); + + set_caller_address(owner); + ERC721::set_approval_for_all(OPERATOR(), true); + + set_caller_address(OPERATOR()); + ERC721::safe_transfer_from(owner, receiver, token_id, DATA(true)); + + assert_state_after_transfer(token_id, owner, receiver); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: unauthorized caller', ))] +fn test_safe_transfer_from_unauthorized() { + setup(); + set_caller_address(OTHER()); + ERC721::safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID(), DATA(true)); +} + +// +// __transfer +// + +#[test] +#[available_gas(2000000)] +fn test__transfer() { + setup(); + let token_id = TOKEN_ID(); + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(token_id, owner, recipient); + ERC721::_transfer(owner, recipient, token_id); + assert_state_after_transfer(token_id, owner, recipient); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test__transfer_nonexistent() { + ERC721::_transfer(ZERO(), RECIPIENT(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid receiver', ))] +fn test__transfer_to_zero() { + setup(); + + ERC721::_transfer(OWNER(), ZERO(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: wrong sender', ))] +fn test__transfer_from_invalid_owner() { + setup(); + + ERC721::_transfer(RECIPIENT(), OWNER(), TOKEN_ID()); +} + +/// +/// Mint +/// + +#[test] +#[available_gas(2000000)] +fn test__mint() { + let recipient = RECIPIENT(); + let token_id = TOKEN_ID(); + assert_state_before_mint(recipient); + ERC721::_mint(RECIPIENT(), TOKEN_ID()); + assert_state_after_mint(token_id, recipient); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid receiver', ))] +fn test__mint_to_zero() { + ERC721::_mint(ZERO(), TOKEN_ID()); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: token already minted', ))] +fn test__mint_already_exist() { + setup(); + + ERC721::_mint(RECIPIENT(), TOKEN_ID()); +} + +/// +/// _safe_mint +/// + +#[test] +#[available_gas(2000000)] +fn test__safe_mint_to_receiver() { + let recipient = setup_receiver(); + let token_id = TOKEN_ID(); + + assert_state_before_mint(recipient); + ERC721::_safe_mint(recipient, token_id, DATA(true)); + assert_state_after_mint(token_id, recipient); +} + +#[test] +#[available_gas(2000000)] +fn test__safe_mint_to_account() { + let account = setup_account(); + let token_id = TOKEN_ID(); + + assert_state_before_mint(account); + ERC721::_safe_mint(account, token_id, DATA(true)); + assert_state_after_mint(token_id, account); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', ))] +fn test__safe_mint_to_non_receiver() { + let recipient = utils::deploy(ERC721NonReceiver::TEST_CLASS_HASH, ArrayTrait::new()); + let token_id = TOKEN_ID(); + + assert_state_before_mint(recipient); + ERC721::_safe_mint(recipient, token_id, DATA(true)); + assert_state_after_mint(token_id, recipient); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: safe mint failed', ))] +fn test__safe_mint_to_receiver_failure() { + let recipient = setup_receiver(); + let token_id = TOKEN_ID(); + + assert_state_before_mint(recipient); + ERC721::_safe_mint(recipient, token_id, DATA(false)); + assert_state_after_mint(token_id, recipient); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid receiver', ))] +fn test__safe_mint_to_zero() { + ERC721::_safe_mint(ZERO(), TOKEN_ID(), DATA(true)); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: token already minted', ))] +fn test__safe_mint_already_exist() { + setup(); + ERC721::_safe_mint(RECIPIENT(), TOKEN_ID(), DATA(true)); +} + +/// +/// Burn +/// + +#[test] +#[available_gas(2000000)] +fn test__burn() { + setup(); + + ERC721::_approve(OTHER(), TOKEN_ID()); + + assert(ERC721::owner_of(TOKEN_ID()) == OWNER(), 'Ownership before'); + assert(ERC721::balance_of(OWNER()) == 1.into(), 'Balance of owner before'); + assert(ERC721::get_approved(TOKEN_ID()) == OTHER(), 'Approval before'); + + ERC721::_burn(TOKEN_ID()); + + assert(ERC721::_owners::read(TOKEN_ID()) == ZERO(), 'Ownership after'); + assert(ERC721::balance_of(OWNER()) == 0.into(), 'Balance of owner after'); + assert(ERC721::_token_approvals::read(TOKEN_ID()) == ZERO(), 'Approval after'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test__burn_nonexistent() { + ERC721::_burn(TOKEN_ID()); +} + +/// +/// _set_token_uri +/// + +#[test] +#[available_gas(2000000)] +fn test__set_token_uri() { + setup(); + + assert(ERC721::token_uri(TOKEN_ID()) == 0, 'URI should be 0'); + ERC721::_set_token_uri(TOKEN_ID(), URI); + assert(ERC721::token_uri(TOKEN_ID()) == URI, 'URI should be set'); +} + +#[test] +#[available_gas(2000000)] +#[should_panic(expected: ('ERC721: invalid token ID', ))] +fn test__set_token_uri_nonexistent() { + ERC721::_set_token_uri(TOKEN_ID(), URI); +} + +// +// Helpers +// + +fn assert_state_before_transfer( + token_id: u256, owner: ContractAddress, recipient: ContractAddress +) { + assert(ERC721::owner_of(token_id) == owner, 'Ownership before'); + assert(ERC721::balance_of(owner) == 1.into(), 'Balance of owner before'); + assert(ERC721::balance_of(recipient) == 0.into(), 'Balance of recipient before'); +} + +fn assert_state_after_transfer(token_id: u256, owner: ContractAddress, recipient: ContractAddress) { + assert(ERC721::owner_of(token_id) == recipient, 'Ownership after'); + assert(ERC721::balance_of(owner) == 0.into(), 'Balance of owner after'); + assert(ERC721::balance_of(recipient) == 1.into(), 'Balance of recipient after'); + assert(ERC721::get_approved(token_id) == ZERO(), 'Approval not implicitly reset'); +} + +fn assert_state_before_mint(recipient: ContractAddress) { + assert(ERC721::balance_of(recipient) == 0.into(), 'Balance of recipient before'); +} + +fn assert_state_after_mint(token_id: u256, recipient: ContractAddress) { + assert(ERC721::owner_of(token_id) == recipient, 'Ownership after'); + assert(ERC721::balance_of(recipient) == 1.into(), 'Balance of recipient after'); + assert(ERC721::get_approved(token_id) == ZERO(), 'Approval implicitly set'); +} diff --git a/src/openzeppelin/token.cairo b/src/openzeppelin/token.cairo index bfe4665e0..f9a848d01 100644 --- a/src/openzeppelin/token.cairo +++ b/src/openzeppelin/token.cairo @@ -1 +1,2 @@ mod erc20; +mod erc721; diff --git a/src/openzeppelin/token/erc721.cairo b/src/openzeppelin/token/erc721.cairo new file mode 100644 index 000000000..2dde68df8 --- /dev/null +++ b/src/openzeppelin/token/erc721.cairo @@ -0,0 +1,3 @@ +mod erc721; +use erc721::ERC721; +mod interface; diff --git a/src/openzeppelin/token/erc721/erc721.cairo b/src/openzeppelin/token/erc721/erc721.cairo new file mode 100644 index 000000000..e22466078 --- /dev/null +++ b/src/openzeppelin/token/erc721/erc721.cairo @@ -0,0 +1,462 @@ +use starknet::ContractAddress; + +#[abi] +trait ERC721ABI { + // case agnostic + #[view] + fn name() -> felt252; + #[view] + fn symbol() -> felt252; + #[external] + fn approve(to: ContractAddress, token_id: u256); + // snake_case + #[view] + fn balance_of(account: ContractAddress) -> u256; + #[view] + fn owner_of(token_id: u256) -> ContractAddress; + #[external] + fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256); + #[external] + fn safe_transfer_from( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span + ); + #[external] + fn set_approval_for_all(operator: ContractAddress, approved: bool); + #[view] + fn get_approved(token_id: u256) -> ContractAddress; + #[view] + fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool; + #[view] + fn token_uri(token_id: u256) -> felt252; + // camelCase + #[view] + fn balanceOf(account: ContractAddress) -> u256; + #[view] + fn ownerOf(tokenId: u256) -> ContractAddress; + #[external] + fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256); + #[external] + fn safeTransferFrom( + from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span + ); + #[external] + fn setApprovalForAll(operator: ContractAddress, approved: bool); + #[view] + fn getApproved(tokenId: u256) -> ContractAddress; + #[view] + fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool; + #[view] + fn tokenUri(tokenId: u256) -> felt252; +} + +#[contract] +mod ERC721 { + // OZ modules + use openzeppelin::account; + use openzeppelin::introspection::erc165; + use openzeppelin::token::erc721; + + // Dispatchers + use openzeppelin::introspection::erc165::IERC165Dispatcher; + use openzeppelin::introspection::erc165::IERC165DispatcherTrait; + use super::super::interface::IERC721ReceiverABIDispatcher; + use super::super::interface::IERC721ReceiverABIDispatcherTrait; + + // Other + use starknet::ContractAddress; + use starknet::get_caller_address; + use zeroable::Zeroable; + use option::OptionTrait; + use array::SpanTrait; + use traits::Into; + use openzeppelin::utils::serde::SpanSerde; + + struct Storage { + _name: felt252, + _symbol: felt252, + _owners: LegacyMap, + _balances: LegacyMap, + _token_approvals: LegacyMap, + _operator_approvals: LegacyMap<(ContractAddress, ContractAddress), bool>, + _token_uri: LegacyMap, + } + + #[event] + fn Transfer(from: ContractAddress, to: ContractAddress, token_id: u256) {} + + #[event] + fn Approval(owner: ContractAddress, approved: ContractAddress, token_id: u256) {} + + #[event] + fn ApprovalForAll(owner: ContractAddress, operator: ContractAddress, approved: bool) {} + + #[constructor] + fn constructor(name: felt252, symbol: felt252) { + initializer(name, symbol); + } + + impl ERC721Impl of erc721::interface::IERC721 { + fn name() -> felt252 { + _name::read() + } + + fn symbol() -> felt252 { + _symbol::read() + } + + fn token_uri(token_id: u256) -> felt252 { + assert(_exists(token_id), 'ERC721: invalid token ID'); + _token_uri::read(token_id) + } + + fn balance_of(account: ContractAddress) -> u256 { + assert(!account.is_zero(), 'ERC721: invalid account'); + _balances::read(account) + } + + fn owner_of(token_id: u256) -> ContractAddress { + _owner_of(token_id) + } + + fn get_approved(token_id: u256) -> ContractAddress { + assert(_exists(token_id), 'ERC721: invalid token ID'); + _token_approvals::read(token_id) + } + + fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool { + _operator_approvals::read((owner, operator)) + } + + fn approve(to: ContractAddress, token_id: u256) { + let owner = _owner_of(token_id); + + let caller = get_caller_address(); + assert( + owner == caller | is_approved_for_all(owner, caller), 'ERC721: unauthorized caller' + ); + _approve(to, token_id); + } + + fn set_approval_for_all(operator: ContractAddress, approved: bool) { + _set_approval_for_all(get_caller_address(), operator, approved) + } + + fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256) { + assert( + _is_approved_or_owner(get_caller_address(), token_id), 'ERC721: unauthorized caller' + ); + _transfer(from, to, token_id); + } + + fn safe_transfer_from( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span + ) { + assert( + _is_approved_or_owner(get_caller_address(), token_id), 'ERC721: unauthorized caller' + ); + _safe_transfer(from, to, token_id, data); + } + } + + impl ERC721CamelImpl of erc721::interface::IERC721Camel { + fn name() -> felt252 { + ERC721Impl::name() + } + + fn symbol() -> felt252 { + ERC721Impl::symbol() + } + + fn tokenUri(tokenId: u256) -> felt252 { + ERC721Impl::token_uri(tokenId) + } + + fn balanceOf(account: ContractAddress) -> u256 { + ERC721Impl::balance_of(account) + } + + fn ownerOf(tokenId: u256) -> ContractAddress { + ERC721Impl::owner_of(tokenId) + } + + fn approve(to: ContractAddress, tokenId: u256) { + ERC721Impl::approve(to, tokenId) + } + + fn getApproved(tokenId: u256) -> ContractAddress { + ERC721Impl::get_approved(tokenId) + } + + fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool { + ERC721Impl::is_approved_for_all(owner, operator) + } + + fn setApprovalForAll(operator: ContractAddress, approved: bool) { + ERC721Impl::set_approval_for_all(operator, approved) + } + + fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256) { + ERC721Impl::transfer_from(from, to, tokenId) + } + + fn safeTransferFrom( + from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span + ) { + ERC721Impl::safe_transfer_from(from, to, tokenId, data) + } + } + + // View + + #[view] + fn supports_interface(interface_id: u32) -> bool { + erc165::ERC165::supports_interface(interface_id) + } + + #[view] + fn supportsInterface(interfaceId: u32) -> bool { + erc165::ERC165::supports_interface(interfaceId) + } + + #[view] + fn name() -> felt252 { + ERC721Impl::name() + } + + #[view] + fn symbol() -> felt252 { + ERC721Impl::symbol() + } + + #[view] + fn token_uri(token_id: u256) -> felt252 { + ERC721Impl::token_uri(token_id) + } + + #[view] + fn tokenUri(tokenId: u256) -> felt252 { + ERC721CamelImpl::tokenUri(tokenId) + } + + #[view] + fn balance_of(account: ContractAddress) -> u256 { + ERC721Impl::balance_of(account) + } + + #[view] + fn balanceOf(account: ContractAddress) -> u256 { + ERC721CamelImpl::balanceOf(account) + } + + #[view] + fn owner_of(token_id: u256) -> ContractAddress { + ERC721Impl::owner_of(token_id) + } + + #[view] + fn ownerOf(tokenId: u256) -> ContractAddress { + ERC721CamelImpl::ownerOf(tokenId) + } + + #[view] + fn get_approved(token_id: u256) -> ContractAddress { + ERC721Impl::get_approved(token_id) + } + + #[view] + fn getApproved(tokenId: u256) -> ContractAddress { + ERC721CamelImpl::getApproved(tokenId) + } + + #[view] + fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool { + ERC721Impl::is_approved_for_all(owner, operator) + } + + #[view] + fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool { + ERC721CamelImpl::isApprovedForAll(owner, operator) + } + + // External + + #[external] + fn approve(to: ContractAddress, token_id: u256) { + ERC721Impl::approve(to, token_id) + } + + #[external] + fn set_approval_for_all(operator: ContractAddress, approved: bool) { + ERC721Impl::set_approval_for_all(operator, approved) + } + + #[external] + fn setApprovalForAll(operator: ContractAddress, approved: bool) { + ERC721CamelImpl::setApprovalForAll(operator, approved) + } + + #[external] + fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256) { + ERC721Impl::transfer_from(from, to, token_id) + } + + #[external] + fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256) { + ERC721CamelImpl::transferFrom(from, to, tokenId) + } + + #[external] + fn safe_transfer_from( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span + ) { + ERC721Impl::safe_transfer_from(from, to, token_id, data) + } + + #[external] + fn safeTransferFrom( + from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span + ) { + ERC721CamelImpl::safeTransferFrom(from, to, tokenId, data) + } + + // Internal + + #[internal] + fn initializer(name_: felt252, symbol_: felt252) { + _name::write(name_); + _symbol::write(symbol_); + erc165::ERC165::register_interface(erc721::interface::IERC721_ID); + erc165::ERC165::register_interface(erc721::interface::IERC721_METADATA_ID); + } + + #[internal] + fn _owner_of(token_id: u256) -> ContractAddress { + let owner = _owners::read(token_id); + match owner.is_zero() { + bool::False(()) => owner, + bool::True(()) => panic_with_felt252('ERC721: invalid token ID') + } + } + + #[internal] + fn _exists(token_id: u256) -> bool { + !_owners::read(token_id).is_zero() + } + + #[internal] + fn _is_approved_or_owner(spender: ContractAddress, token_id: u256) -> bool { + let owner = _owner_of(token_id); + owner == spender | is_approved_for_all(owner, spender) | spender == get_approved(token_id) + } + + #[internal] + fn _approve(to: ContractAddress, token_id: u256) { + let owner = _owner_of(token_id); + assert(owner != to, 'ERC721: approval to owner'); + _token_approvals::write(token_id, to); + Approval(owner, to, token_id); + } + + #[internal] + fn _set_approval_for_all(owner: ContractAddress, operator: ContractAddress, approved: bool) { + assert(owner != operator, 'ERC721: self approval'); + _operator_approvals::write((owner, operator), approved); + ApprovalForAll(owner, operator, approved); + } + + #[internal] + fn _mint(to: ContractAddress, token_id: u256) { + assert(!to.is_zero(), 'ERC721: invalid receiver'); + assert(!_exists(token_id), 'ERC721: token already minted'); + + // Update balances + _balances::write(to, _balances::read(to) + 1.into()); + + // Update token_id owner + _owners::write(token_id, to); + + // Emit event + Transfer(Zeroable::zero(), to, token_id); + } + + #[internal] + fn _transfer(from: ContractAddress, to: ContractAddress, token_id: u256) { + assert(!to.is_zero(), 'ERC721: invalid receiver'); + let owner = _owner_of(token_id); + assert(from == owner, 'ERC721: wrong sender'); + + // Implicit clear approvals, no need to emit an event + _token_approvals::write(token_id, Zeroable::zero()); + + // Update balances + _balances::write(from, _balances::read(from) - 1.into()); + _balances::write(to, _balances::read(to) + 1.into()); + + // Update token_id owner + _owners::write(token_id, to); + + // Emit event + Transfer(from, to, token_id); + } + + #[internal] + fn _burn(token_id: u256) { + let owner = _owner_of(token_id); + + // Implicit clear approvals, no need to emit an event + _token_approvals::write(token_id, Zeroable::zero()); + + // Update balances + _balances::write(owner, _balances::read(owner) - 1.into()); + + // Delete owner + _owners::write(token_id, Zeroable::zero()); + + // Emit event + Transfer(owner, Zeroable::zero(), token_id); + } + + #[internal] + fn _safe_mint(to: ContractAddress, token_id: u256, data: Span) { + _mint(to, token_id); + assert( + _check_on_erc721_received(Zeroable::zero(), to, token_id, data), + 'ERC721: safe mint failed' + ); + } + + #[internal] + fn _safe_transfer( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span + ) { + _transfer(from, to, token_id); + assert(_check_on_erc721_received(from, to, token_id, data), 'ERC721: safe transfer failed'); + } + + #[internal] + fn _set_token_uri(token_id: u256, token_uri: felt252) { + assert(_exists(token_id), 'ERC721: invalid token ID'); + _token_uri::write(token_id, token_uri) + } + + #[private] + fn _check_on_erc721_received( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span + ) -> bool { + if (IERC165Dispatcher { + contract_address: to + }.supports_interface(erc721::interface::IERC721_RECEIVER_ID)) { + // todo add casing fallback mechanism + IERC721ReceiverABIDispatcher { + contract_address: to + } + .on_erc721_received( + get_caller_address(), from, token_id, data + ) == erc721::interface::IERC721_RECEIVER_ID + } else { + IERC165Dispatcher { + contract_address: to + }.supports_interface(account::interface::IACCOUNT_ID) + } + } +} diff --git a/src/openzeppelin/token/erc721/interface.cairo b/src/openzeppelin/token/erc721/interface.cairo new file mode 100644 index 000000000..a8b21f60e --- /dev/null +++ b/src/openzeppelin/token/erc721/interface.cairo @@ -0,0 +1,71 @@ +use openzeppelin::utils::serde::SpanSerde; +use starknet::ContractAddress; +use array::SpanTrait; + +const IERC721_ID: u32 = 0x80ac58cd_u32; +const IERC721_METADATA_ID: u32 = 0x5b5e139f_u32; +const IERC721_RECEIVER_ID: u32 = 0x150b7a02_u32; + +#[abi] +trait IERC721 { + fn balance_of(account: ContractAddress) -> u256; + fn owner_of(token_id: u256) -> ContractAddress; + fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256); + fn safe_transfer_from( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span + ); + fn approve(to: ContractAddress, token_id: u256); + fn set_approval_for_all(operator: ContractAddress, approved: bool); + fn get_approved(token_id: u256) -> ContractAddress; + fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool; + // IERC721Metadata + fn name() -> felt252; + fn symbol() -> felt252; + fn token_uri(token_id: u256) -> felt252; +} + +#[abi] +trait IERC721Camel { + fn balanceOf(account: ContractAddress) -> u256; + fn ownerOf(tokenId: u256) -> ContractAddress; + fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256); + fn safeTransferFrom( + from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span + ); + fn approve(to: ContractAddress, tokenId: u256); + fn setApprovalForAll(operator: ContractAddress, approved: bool); + fn getApproved(tokenId: u256) -> ContractAddress; + fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool; + // IERC721Metadata + fn name() -> felt252; + fn symbol() -> felt252; + fn tokenUri(tokenId: u256) -> felt252; +} + +// +// ERC721Receiver +// + +#[abi] +trait IERC721ReceiverABI { + fn on_erc721_received( + operator: ContractAddress, from: ContractAddress, token_id: u256, data: Span + ) -> u32; + fn onERC721Received( + operator: ContractAddress, from: ContractAddress, tokenId: u256, data: Span + ) -> u32; +} + +#[abi] +trait IERC721Receiver { + fn on_erc721_received( + operator: ContractAddress, from: ContractAddress, token_id: u256, data: Span + ) -> u32; +} + +#[abi] +trait IERC721ReceiverCamel { + fn onERC721Received( + operator: ContractAddress, from: ContractAddress, tokenId: u256, data: Span + ) -> u32; +} diff --git a/src/openzeppelin/utils.cairo b/src/openzeppelin/utils.cairo index c8a47ceb8..8dab269b1 100644 --- a/src/openzeppelin/utils.cairo +++ b/src/openzeppelin/utils.cairo @@ -3,6 +3,7 @@ use array::SpanTrait; use box::BoxTrait; use option::OptionTrait; mod constants; +mod serde; #[inline(always)] fn check_gas() { diff --git a/src/openzeppelin/utils/serde.cairo b/src/openzeppelin/utils/serde.cairo new file mode 100644 index 000000000..68c3720f0 --- /dev/null +++ b/src/openzeppelin/utils/serde.cairo @@ -0,0 +1,19 @@ +use array::ArrayTrait; +use array::SpanTrait; +use serde::Serde; +use serde::serialize_array_helper; +use serde::deserialize_array_helper; + +impl SpanSerde< + T, impl TSerde: Serde, impl TCopy: Copy, impl TDrop: Drop +> of Serde> { + fn serialize(self: @Span, ref output: Array) { + (*self).len().serialize(ref output); + serialize_array_helper(*self, ref output); + } + fn deserialize(ref serialized: Span) -> Option> { + let length = *serialized.pop_front()?; + let mut arr = ArrayTrait::new(); + Option::Some(deserialize_array_helper(ref serialized, arr, length)?.span()) + } +}