diff --git a/packages/test_common/src/mocks/erc1155.cairo b/packages/test_common/src/mocks/erc1155.cairo index 8c206e95e..6c98dd7b2 100644 --- a/packages/test_common/src/mocks/erc1155.cairo +++ b/packages/test_common/src/mocks/erc1155.cairo @@ -102,6 +102,103 @@ pub mod SnakeERC1155Mock { } } +/// Similar to `SnakeERC1155Mock`, but emits events for `before_update` and `after_update` hooks. +/// This is used to test that the hooks are called with the correct arguments. +#[starknet::contract] +pub mod SnakeERC1155MockWithHooks { + 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 ERC1155MetadataURIImpl = + ERC1155Component::ERC1155MetadataURIImpl; + impl ERC1155InternalImpl = ERC1155Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc1155: ERC1155Component::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + ERC1155Event: ERC1155Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + BeforeUpdate: BeforeUpdate, + AfterUpdate: AfterUpdate + } + + /// Event used to test that `before_update` hook is called. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct BeforeUpdate { + pub from: ContractAddress, + pub to: ContractAddress, + pub token_ids: Span, + pub values: Span + } + + /// Event used to test that `after_update` hook is called. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct AfterUpdate { + pub from: ContractAddress, + pub to: ContractAddress, + pub token_ids: Span, + pub values: Span + } + + #[constructor] + fn constructor( + ref self: ContractState, + base_uri: ByteArray, + recipient: ContractAddress, + token_id: u256, + value: u256 + ) { + self.erc1155.initializer(base_uri); + self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); + } + + impl ERC1155HooksImpl of ERC1155Component::ERC1155HooksTrait { + fn before_update( + ref self: ERC1155Component::ComponentState, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.emit(BeforeUpdate { from, to, token_ids, values }); + } + + fn after_update( + ref self: ERC1155Component::ComponentState, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.emit(AfterUpdate { from, to, token_ids, values }); + } + } +} + #[starknet::contract] pub mod DualCaseERC1155ReceiverMock { use openzeppelin_introspection::src5::SRC5Component; diff --git a/packages/test_common/src/mocks/erc20.cairo b/packages/test_common/src/mocks/erc20.cairo index a77d3275c..818dc0917 100644 --- a/packages/test_common/src/mocks/erc20.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -78,6 +78,87 @@ pub mod SnakeERC20Mock { } } +/// Similar to `SnakeERC20Mock`, but emits events for `before_update` and `after_update` hooks. +/// This is used to test that the hooks are called with the correct arguments. +#[starknet::contract] +pub mod SnakeERC20MockWithHooks { + use openzeppelin_token::erc20::ERC20Component; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + impl InternalImpl = ERC20Component::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + BeforeUpdate: BeforeUpdate, + AfterUpdate: AfterUpdate + } + + /// Event used to test that `before_update` hook is called. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct BeforeUpdate { + pub from: ContractAddress, + pub recipient: ContractAddress, + pub amount: u256 + } + + /// Event used to test that `after_update` hook is called. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct AfterUpdate { + pub from: ContractAddress, + pub recipient: ContractAddress, + pub amount: u256 + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + initial_supply: u256, + recipient: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + } + + impl ERC20HooksImpl of ERC20Component::ERC20HooksTrait { + fn before_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.emit(BeforeUpdate { from, recipient, amount }); + } + + fn after_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.emit(AfterUpdate { from, recipient, amount }); + } + } +} + #[starknet::contract] pub mod DualCaseERC20PermitMock { use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; diff --git a/packages/test_common/src/mocks/erc721.cairo b/packages/test_common/src/mocks/erc721.cairo index 3d9be74a2..054b45258 100644 --- a/packages/test_common/src/mocks/erc721.cairo +++ b/packages/test_common/src/mocks/erc721.cairo @@ -107,6 +107,99 @@ pub mod SnakeERC721Mock { } } +/// Similar as `SnakeERC721Mock`, but emits events for `before_update` and `after_update` hooks. +/// This is used to test that the hooks are called with the correct arguments. +#[starknet::contract] +pub mod SnakeERC721MockWithHooks { + 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; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc721: ERC721Component::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + BeforeUpdate: BeforeUpdate, + AfterUpdate: AfterUpdate + } + + /// Event used to test that `before_update` hook is called. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct BeforeUpdate { + pub to: ContractAddress, + pub token_id: u256, + pub auth: ContractAddress + } + + /// Event used to test that `after_update` hook is called. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct AfterUpdate { + pub to: ContractAddress, + pub token_id: u256, + pub auth: ContractAddress + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + base_uri: ByteArray, + recipient: ContractAddress, + token_id: u256 + ) { + self.erc721.initializer(name, symbol, base_uri); + self.erc721.mint(recipient, token_id); + } + + impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait { + fn before_update( + ref self: ERC721Component::ComponentState, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.emit(BeforeUpdate { to, token_id, auth }); + } + + fn after_update( + ref self: ERC721Component::ComponentState, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) { + let mut contract_state = self.get_contract_mut(); + contract_state.emit(AfterUpdate { to, token_id, auth }); + } + } +} + #[starknet::contract] pub mod DualCaseERC721ReceiverMock { use openzeppelin_introspection::src5::SRC5Component; diff --git a/packages/token/src/tests/erc1155/test_erc1155.cairo b/packages/token/src/tests/erc1155/test_erc1155.cairo index f1bad9234..4e1bc6a3a 100644 --- a/packages/token/src/tests/erc1155/test_erc1155.cairo +++ b/packages/token/src/tests/erc1155/test_erc1155.cairo @@ -10,12 +10,14 @@ use openzeppelin_test_common::erc1155::{ use openzeppelin_test_common::erc1155::{ setup_account, deploy_another_account_at, setup_src5, setup_receiver }; -use openzeppelin_test_common::mocks::erc1155::DualCaseERC1155Mock; +use openzeppelin_test_common::mocks::erc1155::{DualCaseERC1155Mock, SnakeERC1155MockWithHooks}; use openzeppelin_testing::constants::{ EMPTY_DATA, ZERO, OWNER, RECIPIENT, OPERATOR, OTHER, TOKEN_ID, TOKEN_ID_2, TOKEN_VALUE, TOKEN_VALUE_2 }; -use snforge_std::{spy_events, test_address, start_cheat_caller_address}; +use openzeppelin_testing::events::EventSpyExt; + +use snforge_std::{EventSpy, spy_events, test_address, start_cheat_caller_address}; use starknet::ContractAddress; use starknet::storage::StoragePointerReadAccess; @@ -24,6 +26,8 @@ use starknet::storage::StoragePointerReadAccess; // type ComponentState = ERC1155Component::ComponentState; +type ComponentStateWithHooks = + ERC1155Component::ComponentState; fn CONTRACT_STATE() -> DualCaseERC1155Mock::ContractState { DualCaseERC1155Mock::contract_state_for_testing() @@ -31,6 +35,9 @@ fn CONTRACT_STATE() -> DualCaseERC1155Mock::ContractState { fn COMPONENT_STATE() -> ComponentState { ERC1155Component::component_state_for_testing() } +fn COMPONENT_STATE_WITH_HOOKS() -> ComponentStateWithHooks { + ERC1155Component::component_state_for_testing() +} fn setup() -> (ComponentState, ContractAddress) { let mut state = COMPONENT_STATE(); @@ -45,6 +52,19 @@ fn setup() -> (ComponentState, ContractAddress) { (state, owner) } +fn setup_with_hooks() -> (ComponentStateWithHooks, ContractAddress) { + let mut state = COMPONENT_STATE_WITH_HOOKS(); + state.initializer("URI"); + + let owner = setup_account(); + let token_ids = array![TOKEN_ID, TOKEN_ID_2].span(); + let values = array![TOKEN_VALUE, TOKEN_VALUE_2].span(); + + state.batch_mint_with_acceptance_check(owner, token_ids, values, array![].span()); + + (state, owner) +} + // // Initializers // @@ -811,6 +831,34 @@ fn test_update_insufficient_balance() { state.update(owner, recipient, token_ids, values); } +#[test] +fn test_update_calls_before_update_hook() { + let (mut state, owner) = setup_with_hooks(); + let recipient = RECIPIENT(); + let token_ids = array![TOKEN_ID].span(); + let values = array![TOKEN_VALUE].span(); + + let mut spy = spy_events(); + let contract_address = test_address(); + + state.update(owner, recipient, token_ids, values); + spy.assert_event_before_update(contract_address, owner, recipient, token_ids, values); +} + +#[test] +fn test_update_calls_after_update_hook() { + let (mut state, owner) = setup_with_hooks(); + let recipient = RECIPIENT(); + let token_ids = array![TOKEN_ID].span(); + let values = array![TOKEN_VALUE].span(); + + let mut spy = spy_events(); + let contract_address = test_address(); + + state.update(owner, recipient, token_ids, values); + spy.assert_event_after_update(contract_address, owner, recipient, token_ids, values); +} + // // update_with_acceptance_check @@ -1328,3 +1376,33 @@ fn assert_state_after_transfer_from_zero_batch( } } +#[generate_trait] +impl ERC1155HooksSpyHelpersImpl of ERC1155HooksSpyHelpers { + fn assert_event_before_update( + ref self: EventSpy, + contract: ContractAddress, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span + ) { + let expected = SnakeERC1155MockWithHooks::Event::BeforeUpdate( + SnakeERC1155MockWithHooks::BeforeUpdate { from, to, token_ids, values } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_event_after_update( + ref self: EventSpy, + contract: ContractAddress, + from: ContractAddress, + to: ContractAddress, + token_ids: Span, + values: Span + ) { + let expected = SnakeERC1155MockWithHooks::Event::AfterUpdate( + SnakeERC1155MockWithHooks::AfterUpdate { from, to, token_ids, values } + ); + self.assert_emitted_single(contract, expected); + } +} diff --git a/packages/token/src/tests/erc20/test_erc20.cairo b/packages/token/src/tests/erc20/test_erc20.cairo index 1d8c4451a..1d1deeb8a 100644 --- a/packages/token/src/tests/erc20/test_erc20.cairo +++ b/packages/token/src/tests/erc20/test_erc20.cairo @@ -3,11 +3,12 @@ use crate::erc20::ERC20Component::{ERC20CamelOnlyImpl, ERC20Impl}; use crate::erc20::ERC20Component::{ERC20MetadataImpl, InternalImpl}; use crate::erc20::ERC20Component; use openzeppelin_test_common::erc20::ERC20SpyHelpers; -use openzeppelin_test_common::mocks::erc20::DualCaseERC20Mock; +use openzeppelin_test_common::mocks::erc20::{DualCaseERC20Mock, SnakeERC20MockWithHooks}; use openzeppelin_testing::constants::{ ZERO, OWNER, SPENDER, RECIPIENT, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE }; -use snforge_std::{spy_events, test_address, start_cheat_caller_address}; +use openzeppelin_testing::events::EventSpyExt; +use snforge_std::{EventSpy, spy_events, test_address, start_cheat_caller_address}; use starknet::ContractAddress; // @@ -15,11 +16,17 @@ use starknet::ContractAddress; // type ComponentState = ERC20Component::ComponentState; +type ComponentStateWithHooks = + ERC20Component::ComponentState; fn COMPONENT_STATE() -> ComponentState { ERC20Component::component_state_for_testing() } +fn COMPONENT_STATE_WITH_HOOKS() -> ComponentStateWithHooks { + ERC20Component::component_state_for_testing() +} + fn setup() -> ComponentState { let mut state = COMPONENT_STATE(); state.initializer(NAME(), SYMBOL()); @@ -27,6 +34,13 @@ fn setup() -> ComponentState { state } +fn setup_with_hooks() -> ComponentStateWithHooks { + let mut state = COMPONENT_STATE_WITH_HOOKS(); + state.initializer(NAME(), SYMBOL()); + state.mint(OWNER(), SUPPLY); + state +} + // // initializer & constructor // @@ -552,6 +566,30 @@ fn test_update_from_zero_to_zero() { spy.assert_only_event_transfer(contract_address, ZERO(), ZERO(), VALUE); } +#[test] +fn test_update_calls_before_update_hook() { + let mut state = setup_with_hooks(); + + let mut spy = spy_events(); + let contract_address = test_address(); + + state.update(OWNER(), RECIPIENT(), VALUE); + + spy.assert_event_before_update(contract_address, OWNER(), RECIPIENT(), VALUE); +} + +#[test] +fn test_update_calls_after_update_hook() { + let mut state = setup_with_hooks(); + + let mut spy = spy_events(); + let contract_address = test_address(); + + state.update(OWNER(), RECIPIENT(), VALUE); + + spy.assert_event_after_update(contract_address, OWNER(), RECIPIENT(), VALUE); +} + // // Helpers // @@ -611,3 +649,32 @@ fn assert_state_after_burn(account: ContractAddress, amount: u256) { assert_eq!(current_supply, initial_supply - amount); assert_eq!(state.balance_of(account), initial_supply - amount); } + +#[generate_trait] +impl ERC20HooksSpyHelpersImpl of ERC20HooksSpyHelpers { + fn assert_event_before_update( + ref self: EventSpy, + contract: ContractAddress, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let expected = SnakeERC20MockWithHooks::Event::BeforeUpdate( + SnakeERC20MockWithHooks::BeforeUpdate { from, recipient, amount } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_event_after_update( + ref self: EventSpy, + contract: ContractAddress, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let expected = SnakeERC20MockWithHooks::Event::AfterUpdate( + SnakeERC20MockWithHooks::AfterUpdate { from, recipient, amount } + ); + self.assert_emitted_single(contract, expected); + } +} diff --git a/packages/token/src/tests/erc721/test_erc721.cairo b/packages/token/src/tests/erc721/test_erc721.cairo index 03e9826f2..51882720a 100644 --- a/packages/token/src/tests/erc721/test_erc721.cairo +++ b/packages/token/src/tests/erc721/test_erc721.cairo @@ -5,14 +5,14 @@ use crate::erc721::ERC721Component; use crate::erc721; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_test_common::erc721::ERC721SpyHelpers; -use openzeppelin_test_common::mocks::erc721::DualCaseERC721Mock; +use openzeppelin_test_common::mocks::erc721::{DualCaseERC721Mock, SnakeERC721MockWithHooks}; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{ DATA, ZERO, OWNER, CALLER, RECIPIENT, SPENDER, OPERATOR, OTHER, NAME, SYMBOL, TOKEN_ID, TOKEN_ID_2, PUBKEY, BASE_URI, BASE_URI_2 }; use openzeppelin_testing::events::EventSpyExt; -use snforge_std::{spy_events, test_address, start_cheat_caller_address}; +use snforge_std::{EventSpy, spy_events, test_address, start_cheat_caller_address}; use starknet::ContractAddress; use starknet::storage::StorageMapReadAccess; @@ -21,6 +21,8 @@ use starknet::storage::StorageMapReadAccess; // type ComponentState = ERC721Component::ComponentState; +type ComponentStateWithHooks = + ERC721Component::ComponentState; fn CONTRACT_STATE() -> DualCaseERC721Mock::ContractState { DualCaseERC721Mock::contract_state_for_testing() @@ -28,6 +30,9 @@ fn CONTRACT_STATE() -> DualCaseERC721Mock::ContractState { fn COMPONENT_STATE() -> ComponentState { ERC721Component::component_state_for_testing() } +fn COMPONENT_STATE_WITH_HOOKS() -> ComponentStateWithHooks { + ERC721Component::component_state_for_testing() +} fn setup() -> ComponentState { let mut state = COMPONENT_STATE(); @@ -36,6 +41,13 @@ fn setup() -> ComponentState { state } +fn setup_with_hooks() -> ComponentStateWithHooks { + let mut state = COMPONENT_STATE_WITH_HOOKS(); + state.initializer(NAME(), SYMBOL(), BASE_URI()); + state.mint(OWNER(), TOKEN_ID); + state +} + fn setup_receiver() -> ContractAddress { utils::declare_and_deploy("DualCaseERC721ReceiverMock", array![]) } @@ -1385,6 +1397,30 @@ fn test_update_mint_auth_not_zero() { state.update(RECIPIENT(), TOKEN_ID_2, CALLER()); } +#[test] +fn test_update_calls_before_update_hook() { + let mut state = setup_with_hooks(); + + let mut spy = spy_events(); + let contract_address = test_address(); + + state.update(RECIPIENT(), TOKEN_ID, OWNER()); + + spy.assert_event_before_update(contract_address, RECIPIENT(), TOKEN_ID, OWNER()); +} + +#[test] +fn test_update_calls_after_update_hook() { + let mut state = setup_with_hooks(); + + let mut spy = spy_events(); + let contract_address = test_address(); + + state.update(RECIPIENT(), TOKEN_ID, OWNER()); + + spy.assert_event_after_update(contract_address, RECIPIENT(), TOKEN_ID, OWNER()) +} + // // Helpers // @@ -1417,3 +1453,32 @@ fn assert_state_after_mint(recipient: ContractAddress, token_id: u256) { assert_eq!(state.balance_of(recipient), 1); assert!(state.get_approved(token_id).is_zero()); } + +#[generate_trait] +impl ERC721HooksSpyHelpersImpl of ERC721HooksSpyHelpers { + fn assert_event_before_update( + ref self: EventSpy, + contract: ContractAddress, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) { + let expected = SnakeERC721MockWithHooks::Event::BeforeUpdate( + SnakeERC721MockWithHooks::BeforeUpdate { to, token_id, auth } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_event_after_update( + ref self: EventSpy, + contract: ContractAddress, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) { + let expected = SnakeERC721MockWithHooks::Event::AfterUpdate( + SnakeERC721MockWithHooks::AfterUpdate { to, token_id, auth } + ); + self.assert_emitted_single(contract, expected); + } +}