diff --git a/.gitignore b/.gitignore index 9f97022..784d2a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -target/ \ No newline at end of file +target/ +.snfoundry_cache \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index 179f2a8..fbe9b81 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ scarb 2.6.5 +starknet-foundry 0.27.0 diff --git a/Scarb.lock b/Scarb.lock index 9272408..1bd04fc 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -6,9 +6,15 @@ name = "cairo_erc_2981" version = "2.0.0" dependencies = [ "openzeppelin", + "snforge_std", ] [[package]] name = "openzeppelin" version = "0.14.0" source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.14.0#f091c4f51ddeb10297db984acae965328c5a4e5b" + +[[package]] +name = "snforge_std" +version = "0.27.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.27.0#2d99b7c00678ef0363881ee0273550c44a9263de" diff --git a/Scarb.toml b/Scarb.toml index 5b8ebca..0ff83ae 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -8,6 +8,12 @@ version = "2.0.0" starknet = "2.6.4" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.14.0" } +[dev-dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.27.0" } + +[scripts] +test = "snforge test" + [[target.starknet-contract]] sierra = true casm = true \ No newline at end of file diff --git a/src/components/erc2981.cairo b/src/components/erc2981.cairo index 3285faf..b529f65 100644 --- a/src/components/erc2981.cairo +++ b/src/components/erc2981.cairo @@ -1,21 +1,22 @@ //! Component implementing IERC2981. - #[starknet::component] mod ERC2981Component { // Starknet deps use starknet::{ContractAddress}; // OZ deps - use openzeppelin::introspection::{ - src5::{ - SRC5Component, SRC5Component::InternalTrait as SRC5InternalTrait, - SRC5Component::SRC5Impl - }, - interface::{ISRC5Dispatcher, ISRC5DispatcherTrait} + use openzeppelin::{ + introspection::{ + src5::{ + SRC5Component, SRC5Component::InternalTrait as SRC5InternalTrait, + SRC5Component::SRC5Impl + }, + interface::{ISRC5Dispatcher, ISRC5DispatcherTrait} + } }; // Local deps - use cairo_erc_2981::interfaces::erc2981::{IERC2981, IERC2981_ID}; + use cairo_erc_2981::interfaces::erc2981::{IERC2981, IERC2981Camel, IERC2981_ID}; #[storage] struct Storage { @@ -31,12 +32,12 @@ mod ERC2981Component { #[derive(Drop, starknet::Event)] enum Event {} - #[embeddable_as(ERC2981)] - impl ERC2981Impl< + #[embeddable_as(ERC2981Impl)] + impl ERC2981< TContractState, +HasComponent, +SRC5Component::HasComponent, - +Drop + +Drop, > of IERC2981> { /// Return the default royalty. /// @@ -125,6 +126,10 @@ mod ERC2981Component { // [Check] Fee is lower or equal to 1 assert(fee_numerator <= fee_denominator, 'Invalid fee rate'); + // [Assert] Caller is owner + // let mut ownable_comp = get_dep_component!(@self, Owner); + // ownable_comp.assert_only_owner(); + // [Effect] Store values self.ERC2981_receiver.write(receiver); self.ERC2981_fee_numerator.write(fee_numerator); @@ -160,6 +165,10 @@ mod ERC2981Component { // [Check] Fee is lower or equal to 1 assert(fee_numerator <= fee_denominator, 'Invalid fee rate'); + // [Assert] Caller is owner + // let mut ownable_comp = get_dep_component!(@self, Owner); + // ownable_comp.assert_only_owner(); + // [Effect] Store values self.ERC2981_token_receiver.write(token_id, receiver); self.ERC2981_token_fee_numerator.write(token_id, fee_numerator); @@ -167,6 +176,49 @@ mod ERC2981Component { } } + #[embeddable_as(ERC2981CamelImpl)] + impl ERC2981CamelOnly< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop, + > of IERC2981Camel> { + fn defaultRoyalty(self: @ComponentState) -> (ContractAddress, u256, u256) { + self.default_royalty() + } + + fn tokenRoyalty( + self: @ComponentState, tokenId: u256 + ) -> (ContractAddress, u256, u256) { + self.token_royalty(tokenId) + } + + fn royaltyInfo( + self: @ComponentState, tokenId: u256, salePrice: u256 + ) -> (ContractAddress, u256) { + self.royalty_info(tokenId, salePrice) + } + + fn setDefaultRoyalty( + ref self: ComponentState, + receiver: ContractAddress, + feeNumerator: u256, + feeDenominator: u256 + ) { + self.set_default_royalty(receiver, feeNumerator, feeDenominator) + } + + fn setTokenRoyalty( + ref self: ComponentState, + tokenId: u256, + receiver: ContractAddress, + feeNumerator: u256, + feeDenominator: u256 + ) { + self.set_token_royalty(tokenId, receiver, feeNumerator, feeDenominator) + } + } + #[generate_trait] pub impl InternalImpl< TContractState, @@ -185,7 +237,7 @@ mod ERC2981Component { ref self: ComponentState, receiver: ContractAddress, fee_numerator: u256, - fee_denominator: u256 + fee_denominator: u256, ) { // [Effect] Register interfaces let mut src5_component = get_dep_component_mut!(ref self, SRC5); @@ -235,9 +287,10 @@ mod ERC2981Component { #[cfg(test)] mod Test { // starknet deps - use cairo_erc_2981::interfaces::erc2981::IERC2981; + use starknet::ContractAddress; + use cairo_erc_2981::components::erc2981::ERC2981Component::HasComponent; + use cairo_erc_2981::interfaces::erc2981::{IERC2981}; use cairo_erc_2981::components::erc2981::ERC2981Component::InternalTrait; - use starknet::{contract_address_const}; // Local deps use super::ERC2981Component; @@ -253,21 +306,20 @@ mod Test { type ERC2981ComponentState = ERC2981Component::ComponentState; - fn STATE() -> ERC2981ComponentState { ERC2981Component::component_state_for_testing() } fn ZERO() -> starknet::ContractAddress { - contract_address_const::<0>() + starknet::contract_address_const::<0>() } fn RECEIVER() -> starknet::ContractAddress { - contract_address_const::<'RECEIVER'>() + starknet::contract_address_const::<'RECEIVER'>() } fn NEW_RECEIVER() -> starknet::ContractAddress { - contract_address_const::<'NEW_RECEIVER'>() + starknet::contract_address_const::<'NEW_RECEIVER'>() } #[test] @@ -275,6 +327,7 @@ mod Test { fn test_initialization() { // [Setup] let mut state = STATE(); + state.initializer(RECEIVER(), FEE_NUMERATOR, FEE_DENOMINATOR); // [Assert] Default royalty @@ -284,6 +337,7 @@ mod Test { assert(fee_denominator == FEE_DENOMINATOR, 'Invalid fee denominator'); } + #[test] #[available_gas(105_000)] #[should_panic(expected: ('Invalid receiver',))] @@ -326,7 +380,7 @@ mod Test { assert( royalty_amount == SALE_PRICE * FEE_NUMERATOR / FEE_DENOMINATOR, 'Invalid royalty - amount' + amount' ); } @@ -370,7 +424,7 @@ mod Test { assert( royalty_amount == SALE_PRICE * FEE_NUMERATOR / FEE_DENOMINATOR, 'Invalid royalty - amount' + amount' ); } diff --git a/src/lib.cairo b/src/lib.cairo index e1b2fed..24ef299 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -13,9 +13,8 @@ mod presets { mod mocks { mod erc2981; } -// #[cfg(test)] -// mod tests { -// mod test_erc721_royalty; -// } - +#[cfg(test)] +mod tests { + mod test_erc721_royalty; +} diff --git a/src/mocks/erc2981.cairo b/src/mocks/erc2981.cairo index 2a0f525..292b818 100644 --- a/src/mocks/erc2981.cairo +++ b/src/mocks/erc2981.cairo @@ -8,17 +8,25 @@ mod MockERC2981 { component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event); component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); #[abi(embed_v0)] - impl ERC2981Impl = ERC2981Component::ERC2981; + impl ERC2981Impl = ERC2981Component::ERC2981Impl; impl ERC2981InternalImpl = ERC2981Component::InternalImpl; + // Ownable Mixin + #[abi(embed_v0)] + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + #[storage] struct Storage { #[substorage(v0)] erc2981: ERC2981Component::Storage, #[substorage(v0)] src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, } #[event] @@ -28,5 +36,7 @@ mod MockERC2981 { ERC2981Event: ERC2981Component::Event, #[flat] SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, } } diff --git a/src/presets/erc721_royalty.cairo b/src/presets/erc721_royalty.cairo index f867e5d..6ba4b3c 100644 --- a/src/presets/erc721_royalty.cairo +++ b/src/presets/erc721_royalty.cairo @@ -5,8 +5,10 @@ mod ERC721Royalty { // OZ deps use openzeppelin::{ - access::ownable::OwnableComponent, introspection::src5::SRC5Component, - token::erc721::{ERC721Component, ERC721HooksEmptyImpl} + access::ownable::{ + OwnableComponent, OwnableComponent::{InternalTrait as OwnableInternalTrait} + }, + introspection::src5::SRC5Component, token::erc721::{ERC721Component, ERC721HooksEmptyImpl} }; // Local deps @@ -30,8 +32,7 @@ mod ERC721Royalty { impl ERC721InternalImpl = ERC721Component::InternalImpl; // ERC2981 - #[abi(embed_v0)] - impl ERC2981Impl = ERC2981Component::ERC2981; + impl ERC2981Impl = ERC2981Component::ERC2981Impl; impl ERC2981InternalImpl = ERC2981Component::InternalImpl; #[storage] @@ -74,38 +75,40 @@ mod ERC721Royalty { } #[abi(embed_v0)] - impl ERC2981CamelImpl of IERC2981Camel { - fn defaultRoyalty(self: @ContractState) -> (ContractAddress, u256, u256) { + impl ERC721RoyaltyImpl of IERC2981 { + fn default_royalty(self: @ContractState) -> (ContractAddress, u256, u256) { self.erc2981.default_royalty() } - fn tokenRoyalty(self: @ContractState, tokenId: u256) -> (ContractAddress, u256, u256) { - self.erc2981.token_royalty(tokenId) + fn token_royalty(self: @ContractState, token_id: u256) -> (ContractAddress, u256, u256) { + self.erc2981.token_royalty(token_id) } - fn royaltyInfo( - self: @ContractState, tokenId: u256, salePrice: u256 + fn royalty_info( + self: @ContractState, token_id: u256, sale_price: u256 ) -> (ContractAddress, u256) { - self.erc2981.royalty_info(tokenId, salePrice) + self.erc2981.royalty_info(token_id, sale_price) } - fn setDefaultRoyalty( + fn set_default_royalty( ref self: ContractState, receiver: ContractAddress, - feeNumerator: u256, - feeDenominator: u256 + fee_numerator: u256, + fee_denominator: u256 ) { - self.erc2981.set_default_royalty(receiver, feeNumerator, feeDenominator) + self.ownable.assert_only_owner(); + self.erc2981.set_default_royalty(receiver, fee_numerator, fee_denominator); } - fn setTokenRoyalty( + fn set_token_royalty( ref self: ContractState, - tokenId: u256, + token_id: u256, receiver: ContractAddress, - feeNumerator: u256, - feeDenominator: u256 + fee_numerator: u256, + fee_denominator: u256 ) { - self.erc2981.set_token_royalty(tokenId, receiver, feeNumerator, feeDenominator) + self.ownable.assert_only_owner(); + self.erc2981.set_token_royalty(token_id, receiver, fee_numerator, fee_denominator); } } diff --git a/src/tests/test_erc721_royalty.cairo b/src/tests/test_erc721_royalty.cairo index 6e77236..ea500ca 100644 --- a/src/tests/test_erc721_royalty.cairo +++ b/src/tests/test_erc721_royalty.cairo @@ -1,29 +1,26 @@ #[cfg(test)] mod Test { - // Starknet deps - - use starknet::ContractAddress; - use starknet::deploy_syscall; - use starknet::testing::set_contract_address; + // Core deps + use core::serde::Serde; - // External deps + // Starknet-Foundry deps + use snforge_std::{ + declare, ContractClassTrait, start_cheat_caller_address, stop_cheat_caller_address + }; - use openzeppelin::account::account::Account; + // Starknet deps + use starknet::{ContractAddress, deploy_syscall}; // Dispatchers - - use cairo_erc_2981::components::erc2981::interface::{ - IERC2981Dispatcher, IERC2981DispatcherTrait - }; + use cairo_erc_2981::interfaces::erc2981::{IERC2981Dispatcher, IERC2981DispatcherTrait}; // Contracts - use cairo_erc_2981::presets::erc721_royalty::ERC721Royalty; // Constants - - const NAME: felt252 = 'NAME'; - const SYMBOL: felt252 = 'SYMBOL'; + const RECEIVER: felt252 = 'RECEIVER'; + const NEW_RECEIVER: felt252 = 'NEW_RECEIVER'; + const OWNER: felt252 = 'OWNER'; const TOKEN_ID: u256 = 1; const FEE_NUMERATOR: u256 = 5; const FEE_DENOMINATOR: u256 = 100; @@ -31,74 +28,41 @@ mod Test { const NEW_FEE_DENOMINATOR: u256 = 50; // Setup - - #[derive(Drop)] - struct Signers { - owner: ContractAddress, - receiver: ContractAddress, - new_receiver: ContractAddress, - } - - #[derive(Drop)] - struct Contracts { - preset: ContractAddress, - } - - fn deploy_account(public_key: felt252) -> ContractAddress { - let mut calldata = array![public_key]; - let (address, _) = deploy_syscall( - Account::TEST_CLASS_HASH.try_into().expect('Account declare failed'), - 0, - calldata.span(), - false - ) - .expect('Account deploy failed'); - address - } - - fn deploy_preset(receiver: ContractAddress, owner: ContractAddress) -> ContractAddress { - let mut calldata = array![ - NAME, - SYMBOL, - receiver.into(), - FEE_NUMERATOR.low.into(), - FEE_NUMERATOR.high.into(), - FEE_DENOMINATOR.low.into(), - FEE_DENOMINATOR.high.into(), - owner.into() - ]; - let (address, _) = deploy_syscall( - ERC721Royalty::TEST_CLASS_HASH.try_into().expect('Preset declare failed'), - 0, - calldata.span(), - false - ) - .expect('Preset deploy failed'); - address - } - - fn setup() -> (Signers, Contracts) { - let signers = Signers { - owner: deploy_account('OWNER'), - receiver: deploy_account('RECEIVER'), - new_receiver: deploy_account('TOKEN_RECEIVER') - }; - let preset_address = deploy_preset(signers.receiver, signers.owner); - (signers, Contracts { preset: preset_address }) + fn setup(receiver: ContractAddress, owner: ContractAddress) -> ContractAddress { + let name: ByteArray = "NAME"; + let symbol: ByteArray = "SYMBOL"; + let base_uri: ByteArray = "ipfs://abcdefghi/"; + + let mut calldata: Array = array![]; + name.serialize(ref calldata); + symbol.serialize(ref calldata); + base_uri.serialize(ref calldata); + receiver.serialize(ref calldata); + FEE_NUMERATOR.low.serialize(ref calldata); + FEE_NUMERATOR.high.serialize(ref calldata); + FEE_DENOMINATOR.low.serialize(ref calldata); + FEE_DENOMINATOR.high.serialize(ref calldata); + owner.serialize(ref calldata); + + let contract = declare("ERC721Royalty").unwrap(); + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + + contract_address } // Tests - #[test] #[available_gas(1_250_000)] fn test_initialization() { // [Setup] - let (signers, contracts) = setup(); - let erc2981 = IERC2981Dispatcher { contract_address: contracts.preset }; + let preset_contract_address = setup( + RECEIVER.try_into().unwrap(), OWNER.try_into().unwrap() + ); + let preset = IERC2981Dispatcher { contract_address: preset_contract_address }; // [Assert] Provide minter rights to anyone - let (receiver, fee_numerator, fee_denominator) = erc2981.default_royalty(); - assert(receiver == signers.receiver, 'Invalid receiver'); + let (receiver, fee_numerator, fee_denominator) = preset.default_royalty(); + assert(receiver == RECEIVER.try_into().unwrap(), 'Invalid receiver'); assert(fee_numerator == FEE_NUMERATOR.into(), 'Invalid fee numerator'); assert(fee_denominator == FEE_DENOMINATOR.into(), 'Invalid fee denominator'); } @@ -107,16 +71,22 @@ mod Test { #[available_gas(1_600_000)] fn test_set_default_royalty() { // [Setup] - let (signers, contracts) = setup(); - let erc2981 = IERC2981Dispatcher { contract_address: contracts.preset }; + let preset_contract_address = setup( + RECEIVER.try_into().unwrap(), OWNER.try_into().unwrap() + ); + let preset = IERC2981Dispatcher { contract_address: preset_contract_address }; // [Effect] Set default royalty - set_contract_address(signers.owner); - erc2981.set_default_royalty(signers.new_receiver, NEW_FEE_NUMERATOR, NEW_FEE_DENOMINATOR); + start_cheat_caller_address(preset_contract_address, OWNER.try_into().unwrap()); + preset + .set_default_royalty( + NEW_RECEIVER.try_into().unwrap(), NEW_FEE_NUMERATOR, NEW_FEE_DENOMINATOR + ); + stop_cheat_caller_address(preset_contract_address); // [Assert] Default royalty - let (receiver, fee_numerator, fee_denominator) = erc2981.default_royalty(); - assert(receiver == signers.new_receiver, 'Invalid receiver'); + let (receiver, fee_numerator, fee_denominator) = preset.default_royalty(); + assert(receiver == NEW_RECEIVER.try_into().unwrap(), 'Invalid receiver'); assert(fee_numerator == NEW_FEE_NUMERATOR.into(), 'Invalid fee numerator'); assert(fee_denominator == NEW_FEE_DENOMINATOR.into(), 'Invalid fee denominator'); } @@ -126,31 +96,40 @@ mod Test { #[should_panic] fn test_set_default_royalty_revert_not_owner() { // [Setup] - let (signers, contracts) = setup(); - let erc2981 = IERC2981Dispatcher { contract_address: contracts.preset }; + let preset_contract_address = setup( + RECEIVER.try_into().unwrap(), OWNER.try_into().unwrap() + ); + let preset = IERC2981Dispatcher { contract_address: preset_contract_address }; // [Revert] Set default royalty - set_contract_address(signers.new_receiver); - erc2981.set_default_royalty(signers.new_receiver, NEW_FEE_NUMERATOR, NEW_FEE_DENOMINATOR); + start_cheat_caller_address(preset_contract_address, NEW_RECEIVER.try_into().unwrap()); + preset + .set_default_royalty( + NEW_RECEIVER.try_into().unwrap(), NEW_FEE_NUMERATOR, NEW_FEE_DENOMINATOR + ); + stop_cheat_caller_address(preset_contract_address); } #[test] #[available_gas(1_600_000)] fn test_set_token_royalty() { // [Setup] - let (signers, contracts) = setup(); - let erc2981 = IERC2981Dispatcher { contract_address: contracts.preset }; + let preset_contract_address = setup( + RECEIVER.try_into().unwrap(), OWNER.try_into().unwrap() + ); + let preset = IERC2981Dispatcher { contract_address: preset_contract_address }; // [Effect] Set default royalty - set_contract_address(signers.owner); - erc2981 + start_cheat_caller_address(preset_contract_address, OWNER.try_into().unwrap()); + preset .set_token_royalty( - TOKEN_ID, signers.new_receiver, NEW_FEE_NUMERATOR, NEW_FEE_DENOMINATOR + TOKEN_ID, NEW_RECEIVER.try_into().unwrap(), NEW_FEE_NUMERATOR, NEW_FEE_DENOMINATOR ); + stop_cheat_caller_address(preset_contract_address); // [Assert] Token royalty - let (receiver, fee_numerator, fee_denominator) = erc2981.token_royalty(TOKEN_ID); - assert(receiver == signers.new_receiver, 'Invalid receiver'); + let (receiver, fee_numerator, fee_denominator) = preset.token_royalty(TOKEN_ID); + assert(receiver == NEW_RECEIVER.try_into().unwrap(), 'Invalid receiver'); assert(fee_numerator == NEW_FEE_NUMERATOR.into(), 'Invalid fee numerator'); assert(fee_denominator == NEW_FEE_DENOMINATOR.into(), 'Invalid fee denominator'); } @@ -160,14 +139,17 @@ mod Test { #[should_panic] fn test_set_token_royalty_revert_not_owner() { // [Setup] - let (signers, contracts) = setup(); - let erc2981 = IERC2981Dispatcher { contract_address: contracts.preset }; + let preset_contract_address = setup( + RECEIVER.try_into().unwrap(), OWNER.try_into().unwrap() + ); + let preset = IERC2981Dispatcher { contract_address: preset_contract_address }; // [Revert] Set default royalty - set_contract_address(signers.new_receiver); - erc2981 + start_cheat_caller_address(preset_contract_address, NEW_RECEIVER.try_into().unwrap()); + preset .set_token_royalty( - TOKEN_ID, signers.new_receiver, NEW_FEE_NUMERATOR, NEW_FEE_DENOMINATOR + TOKEN_ID, NEW_RECEIVER.try_into().unwrap(), NEW_FEE_NUMERATOR, NEW_FEE_DENOMINATOR ); + stop_cheat_caller_address(preset_contract_address); } }