diff --git a/src/interfaces/minter.cairo b/src/interfaces/minter.cairo index 010767f6..352de8c4 100644 --- a/src/interfaces/minter.cairo +++ b/src/interfaces/minter.cairo @@ -64,6 +64,9 @@ namespace ICarbonableMinter: func set_unit_price(unit_price : Uint256): end + func decrease_reserved_supply_for_mint(slots : Uint256): + end + func airdrop(to : felt, quantity : felt) -> (success : felt): end diff --git a/src/mint/library.cairo b/src/mint/library.cairo index 0cde4416..55e84a23 100644 --- a/src/mint/library.cairo +++ b/src/mint/library.cairo @@ -223,6 +223,23 @@ namespace CarbonableMinter: return () end + func decrease_reserved_supply_for_mint{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr + }(slots : Uint256): + alloc_locals + + # Access control check + Ownable.assert_only_owner() + let (reserved_supply_for_mint) = reserved_supply_for_mint_.read() + let (enough_slots) = uint256_le(slots, reserved_supply_for_mint) + with_attr error_message("CarbonableMinter: not enough reserved slots"): + assert enough_slots = TRUE + end + let (new_reserved_supply_for_mint) = SafeUint256.sub_le(reserved_supply_for_mint, slots) + reserved_supply_for_mint_.write(new_reserved_supply_for_mint) + return () + end + func airdrop{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}( to : felt, quantity : felt ) -> (success : felt): diff --git a/src/mint/minter.cairo b/src/mint/minter.cairo index 61cb545f..dfb846c5 100644 --- a/src/mint/minter.cairo +++ b/src/mint/minter.cairo @@ -145,6 +145,13 @@ func set_unit_price{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_chec return CarbonableMinter.set_unit_price(unit_price) end +@external +func decrease_reserved_supply_for_mint{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(slots : Uint256): + return CarbonableMinter.decrease_reserved_supply_for_mint(slots) +end + @external func airdrop{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}( to : felt, quantity : felt diff --git a/tests/integrations/library.cairo b/tests/integrations/library.cairo index 069e6cb3..ac6764b1 100644 --- a/tests/integrations/library.cairo +++ b/tests/integrations/library.cairo @@ -185,6 +185,15 @@ namespace carbonable_minter_instance: # Externals + func decrease_reserved_supply_for_mint{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr, carbonable_minter : felt + }(slots : Uint256, caller : felt): + %{ stop_prank = start_prank(caller_address=ids.caller, target_contract_address=ids.carbonable_minter) %} + ICarbonableMinter.decrease_reserved_supply_for_mint(carbonable_minter, slots) + %{ stop_prank() %} + return () + end + func withdraw{ syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr, carbonable_minter : felt }(caller : felt) -> (success : felt): @@ -352,6 +361,25 @@ namespace admin_instance: return () end + func decrease_reserved_supply_for_mint{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr + }(slots : felt): + alloc_locals + let (carbonable_minter) = carbonable_minter_instance.deployed() + let (caller) = admin_instance.get_address() + let slots_uint256 = Uint256(slots, 0) + with carbonable_minter: + let (initial_supply) = carbonable_minter_instance.reserved_supply_for_mint() + carbonable_minter_instance.decrease_reserved_supply_for_mint( + slots=slots_uint256, caller=caller + ) + let (returned_supply) = carbonable_minter_instance.reserved_supply_for_mint() + let (expected_supply) = SafeUint256.sub_le(initial_supply, slots_uint256) + assert returned_supply = expected_supply + end + return () + end + func transferOwnership{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}( newOwner : felt ): diff --git a/tests/integrations/test_nominal_case.cairo b/tests/integrations/test_nominal_case.cairo index 94c52dbe..f7f25f09 100644 --- a/tests/integrations/test_nominal_case.cairo +++ b/tests/integrations/test_nominal_case.cairo @@ -142,7 +142,10 @@ func test_e2e_airdrop{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_ch # - has enough funds: YES # User: ADMIN # - aidrop 5 nft to ANYONE - # - aidrop 4 nft to ANYONE + # - aidrop 3 nft to ANYONE + # - decrease reserved supply by one + # User: ADMIN + # - wants to buy 2 NFTs (2 public) alloc_locals let (anyone_address) = anyone.get_address() @@ -154,7 +157,9 @@ func test_e2e_airdrop{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_ch anyone.public_buy(quantity=2) %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available reserved NFTs") %} admin.airdrop(to=anyone_address, quantity=5) - admin.airdrop(to=anyone_address, quantity=4) + admin.airdrop(to=anyone_address, quantity=3) + admin.decrease_reserved_supply_for_mint(slots=1) + anyone.public_buy(quantity=1) admin.withdraw() return () diff --git a/tests/units/test_minter.cairo b/tests/units/test_minter.cairo index 2bcbe58d..16eab5d8 100644 --- a/tests/units/test_minter.cairo +++ b/tests/units/test_minter.cairo @@ -8,6 +8,8 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin from starkware.cairo.common.uint256 import Uint256 from starkware.cairo.common.bool import TRUE, FALSE +from openzeppelin.security.safemath import SafeUint256 + from src.mint.library import CarbonableMinter const PROJECT_NFT_ADDRESS = 0x056d4ffea4ca664ffe1256af4b029998014471a87dec8036747a927ab3320b46 @@ -387,14 +389,12 @@ func test_airdrop_nominal_case{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, # Wants to aidrop 5 NFTs # Whitelisted sale: OPEN # Public sale: CLOSED - # Is user whitelisted: NO # current NFT totalSupply: 5 - # current NFT reserved supply: 0 + # current NFT reserved supply: 5 %{ stop=start_prank(ids.context.signers.admin) %} - let quantity = 5 %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [5, 0]) %} %{ mock_call(ids.context.mocks.project_nft_address, "mint", []) %} - CarbonableMinter.airdrop(to=context.signers.anyone_1, quantity=quantity) + CarbonableMinter.airdrop(to=context.signers.anyone_1, quantity=5) %{ stop() %} return () end @@ -437,10 +437,9 @@ func test_airdrop_revert_not_enough_nfts_available{ # current NFT reserved supply: 1 # has enough funds: YES %{ stop=start_prank(ids.context.signers.admin) %} - let quantity = 5 %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [6, 0]) %} %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} - CarbonableMinter.airdrop(to=context.signers.anyone_1, quantity=quantity) + CarbonableMinter.airdrop(to=context.signers.anyone_1, quantity=5) CarbonableMinter.airdrop(to=context.signers.anyone_1, quantity=1) %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available reserved NFTs") %} CarbonableMinter.airdrop(to=context.signers.anyone_1, quantity=1) @@ -448,6 +447,83 @@ func test_airdrop_revert_not_enough_nfts_available{ return () end +@external +func test_decrease_reserved_supply_nominal_case{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + alloc_locals + let unit_price = Uint256(10, 0) + let max_supply = Uint256(10, 0) + let reserved_supply = Uint256(5, 0) + let (local context : TestContext) = test_internal.prepare( + 1, FALSE, 5, unit_price, max_supply, reserved_supply + ) + + # User: admin + # Wants to decrease the reserved supply by 2 + # Whitelisted sale: OPEN + # Public sale: CLOSED + # current NFT reserved supply: 5 + %{ stop=start_prank(ids.context.signers.admin) %} + let slots = Uint256(2, 0) + let (expected_slots) = SafeUint256.sub_le(reserved_supply, slots) + CarbonableMinter.decrease_reserved_supply_for_mint(slots=slots) + let (returned_supply) = CarbonableMinter.reserved_supply_for_mint() + assert returned_supply = expected_slots + %{ stop() %} + return () +end + +@external +func test_decrease_reserved_supply_revert_not_owner{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + alloc_locals + let unit_price = Uint256(10, 0) + let max_supply = Uint256(10, 0) + let reserved_supply = Uint256(5, 0) + let (local context : TestContext) = test_internal.prepare( + 1, FALSE, 5, unit_price, max_supply, reserved_supply + ) + + # User: anyone_1 + # Wants to decrease the reserved supply by 2 + # Whitelisted sale: OPEN + # Public sale: CLOSED + # current NFT reserved supply: 5 + %{ stop=start_prank(ids.context.signers.anyone_1) %} + let slots = Uint256(2, 0) + %{ expect_revert("TRANSACTION_FAILED", "Ownable: caller is not the owner") %} + CarbonableMinter.decrease_reserved_supply_for_mint(slots=slots) + %{ stop() %} + return () +end + +@external +func test_decrease_reserved_supply_revert_over_decreased{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + alloc_locals + let unit_price = Uint256(10, 0) + let max_supply = Uint256(10, 0) + let reserved_supply = Uint256(5, 0) + let (local context : TestContext) = test_internal.prepare( + 1, FALSE, 5, unit_price, max_supply, reserved_supply + ) + + # User: admin + # Wants to decrease the reserved supply by 6 + # Whitelisted sale: OPEN + # Public sale: CLOSED + # current NFT reserved supply: 5 + %{ stop=start_prank(ids.context.signers.admin) %} + let slots = Uint256(6, 0) + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough reserved slots") %} + CarbonableMinter.decrease_reserved_supply_for_mint(slots=slots) + %{ stop() %} + return () +end + @external func test_withdraw_nominal_case{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}( ):