diff --git a/README.md b/README.md index 377dae95..1f72cf7e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ See also starknet [documentation](https://www.cairo-lang.org/docs/hello_starknet ### 💋 Format code ```bash -cairo-format -i src/**/*.cairo tests/**/*.cairo +cairo-format -i src/**/*.cairo tests/**/**/*.cairo ``` ## 🚀 Deployment diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..cdb68d1a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,41 @@ +import yaml +from pathlib import Path + + +class Object(): + pass + + +def update(ref: dict, new: dict) -> dict: + """Update ref recursively with new.""" + for key, value in new.items(): + if isinstance(value, dict): + ref[key] = update(ref.get(key, {}), value) + else: + ref[key] = value + return ref + + +def objectify(parent: object, attributes: dict) -> object: + """Set attributes recursively to parent object.""" + for key, value in attributes.items(): + if isinstance(value, dict): + value = objectify(Object(), value) + setattr(parent, key, value) + return parent + + +def load(path: str, context: object): + """Read config files and setup context.""" + # load config + path = Path(path) + config = {} + for parent in path.parents: + config_path = parent / path.name + if not config_path.exists(): + continue + with open(config_path, 'r') as file_instance: + update(config, yaml.safe_load(file_instance)) + + # set up context + context = objectify(context, config) diff --git a/tests/config.yml b/tests/config.yml new file mode 100644 index 00000000..47da4816 --- /dev/null +++ b/tests/config.yml @@ -0,0 +1,3 @@ +signers: + admin: 1000 + anyone: 1001 \ No newline at end of file diff --git a/tests/features/minter.feature b/tests/features/minter.feature new file mode 100644 index 00000000..288713d1 --- /dev/null +++ b/tests/features/minter.feature @@ -0,0 +1,64 @@ +Feature: Minter + + Feature Description: Minter contract provides functionnalities to allow a user to buy project NFTs. + All corresponding actions must work as expected. + + Background: + Given a deployed user contracts + And an admin with address 1000 + And an anyone with address 1001 + Given a deployed project nft contact + And owned by admin + Given a deployed payment token contact + And owned by admin + And a total supply set to 1,000,000 + And anyone owns the whole supply + Given a deployed minter contact + And owned by admin + And a whitelist sale open + And a public sale close + And a max buy per tx set to 5 + And an unit price set to 10 + And a max supply set to 10 + And a reserved supply set to 4 + Given a whitelist merkle tree + And an allocation of 5 whitelist slots to anyone + + Scenario: Whitelisted + When anyone approves minter for 5 token equivalent nfts + And anyone makes 5 whitelist buy + And admin open the public sale + And anyone approves minter for 1 token equivalent nft + And anyone makes 1 public buy + And admin withdraw minter contract balance + Then no failed transactions expected + + Scenario: Not whitelisted + When admin set up a new whitelist merkle tree excluding anyone + And anyone approves minter for 1 token equivalent nft + And anyone makes 1 whitelist buy + Then 'caller address is not whitelisted' failed transaction happens + When admin open the public sale + And anyone approves minter for 5 token equivalent nfts + And anyone makes 5 public buy + And admin withdraw minter contract balance + Then no failed transactions expected + + Scenario: Airdrop + When anyone approves minter for 5 token equivalent nfts + And anyone makes 5 whitelist buy + And admin open the public sale + And anyone approves minter for 2 token equivalent nfts + And anyone makes 2 public buy + Then 'not enough available NFTs' failed transaction happens + When admin airdrops 5 nfts to anyone + Then 'not enough available reserved NFTs' failed transaction happens + When admin airdrops 3 nfts to anyone + And admin decreases reserved supply by 1 + And anyone makes 1 public buy + And admin withdraw minter contract balance + Then no failed transactions expected + + Scenario: Over-airdropped + When admin airdrops 11 nfts to anyone + Then 'not enough available NFTs' failed transaction happens \ No newline at end of file diff --git a/tests/features/yielder.feature b/tests/features/yielder.feature new file mode 100644 index 00000000..44b2af35 --- /dev/null +++ b/tests/features/yielder.feature @@ -0,0 +1,21 @@ +Feature: Yielder + + Feature Description: Yield manager contract provides functionnalities to distribute reward tokens to stackers. + All corresponding actions must work as expected. + + Background: + Given a deployed user contracts + And an admin with address 1000 + And an anyone with address 1001 + Given a deployed project nft contact + And owned by admin + Given a deployed reward token contact + And owned by admin + And a total supply set to 1,000,000 + And anyone owns the whole supply + Given a deployed carbonable token contact + And owned by admin + And a total supply set to 1,000,000 + And anyone owns the whole supply + Given a deployed yielder contract + And owned by admin \ No newline at end of file diff --git a/tests/integrations/config.yml b/tests/integrations/config.yml new file mode 100644 index 00000000..eb48e2eb --- /dev/null +++ b/tests/integrations/config.yml @@ -0,0 +1,9 @@ +project: + name: Project + symbol: NFT + +token: + name: Token + symbol: TOK + decimals: 6 + initial_supply: 1000000 \ No newline at end of file diff --git a/tests/integrations/minter/config.yml b/tests/integrations/minter/config.yml new file mode 100644 index 00000000..f00b7f36 --- /dev/null +++ b/tests/integrations/minter/config.yml @@ -0,0 +1,13 @@ +minter: + public_sale_open: 0 + max_buy_per_tx: 5 + unit_price: 10 + max_supply_for_mint: 10 + reserved_supply_for_mint: 4 + +whitelist: + slots: 5 + merkle_root: 3236969588476960619958150604131083087415975923122021901088942336874683133579 + merkle_proof: + - 1489335374474017495857579265074565262713421005832572026644103123081435719307 + merkle_proof_len: 1 \ No newline at end of file diff --git a/tests/integrations/library.cairo b/tests/integrations/minter/library.cairo similarity index 90% rename from tests/integrations/library.cairo rename to tests/integrations/minter/library.cairo index ac6764b1..08b569ca 100644 --- a/tests/integrations/library.cairo +++ b/tests/integrations/minter/library.cairo @@ -17,6 +17,67 @@ from openzeppelin.security.safemath import SafeUint256 from interfaces.minter import ICarbonableMinter from interfaces.CarbonableProjectNFT import IERC721, IERC721_Enumerable, ICarbonableProjectNFT +func setup{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + alloc_locals + local carbonable_minter + local merkle_root + %{ + # Load config + import sys + sys.path.append('.') + from tests import load + load("./tests/integrations/minter/config.yml", context) + + # ERC-721 deployment + context.project_nft_contract = deploy_contract( + "./src/nft/project/CarbonableProjectNFT.cairo", + { + "name": context.project.name, + "symbol": context.project.symbol, + "owner": context.signers.admin, + }, + ).contract_address + + # ERC-20 deployment + context.payment_token_contract = deploy_contract( + "./tests/mocks/token/erc20.cairo", + { + "name": context.token.name, + "symbol": context.token.symbol, + "decimals": context.token.decimals, + "initial_supply": context.token.initial_supply, + "recipient": context.signers.anyone + }, + ).contract_address + + # Minter deployment + context.carbonable_minter_contract = deploy_contract( + "./src/mint/minter.cairo", + { + "owner": context.signers.admin, + "project_nft_address": context.project_nft_contract, + "payment_token_address": context.payment_token_contract, + "public_sale_open": context.minter.public_sale_open, + "max_buy_per_tx": context.minter.max_buy_per_tx, + "unit_price": context.minter.unit_price, + "max_supply_for_mint": context.minter.max_supply_for_mint, + "reserved_supply_for_mint": context.minter.reserved_supply_for_mint, + }, + ).contract_address + + # Externalize required variables + ids.carbonable_minter = context.carbonable_minter_contract + ids.merkle_root = context.whitelist.merkle_root + %} + + # Transfer project nft ownershop from admin to minter + admin_instance.transferOwnership(carbonable_minter) + # Set merkle tree root to minter contract + admin_instance.set_whitelist_merkle_root(merkle_root) + + return () +end + namespace project_nft_instance: # Internals @@ -276,7 +337,7 @@ namespace admin_instance: func get_address() -> (address : felt): tempvar admin - %{ ids.admin = context.ADMIN %} + %{ ids.admin = context.signers.admin %} return (admin) end @@ -444,27 +505,27 @@ namespace anyone_instance: func get_address() -> (address : felt): tempvar anyone - %{ ids.anyone = context.ANYONE %} + %{ ids.anyone = context.signers.anyone %} return (anyone) end func get_slots() -> (slots : felt): tempvar slots - %{ ids.slots = context.SLOTS %} + %{ ids.slots = context.whitelist.slots %} return (slots) end func get_proof_len() -> (proof_len : felt): tempvar proof_len - %{ ids.proof_len = context.PROOF_LEN %} + %{ ids.proof_len = context.whitelist.merkle_proof_len %} return (proof_len) end func get_proof() -> (proof : felt*): alloc_locals - let (proof : felt*) = alloc() + let (local proof : felt*) = alloc() %{ - for index, node in enumerate(context.PROOF): + for index, node in enumerate(context.whitelist.merkle_proof): memory[ids.proof + index] = node %} return (proof) diff --git a/tests/integrations/minter/test_nominal_cases.cairo b/tests/integrations/minter/test_nominal_cases.cairo new file mode 100644 index 00000000..df19f920 --- /dev/null +++ b/tests/integrations/minter/test_nominal_cases.cairo @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_nominal_case.cairo) + +%lang starknet + +# Starkware dependencies +from starkware.cairo.common.bool import TRUE, FALSE +from starkware.cairo.common.cairo_builtins import HashBuiltin + +# Project dependencies +from tests.integrations.minter.library import ( + setup, + admin_instance as admin, + anyone_instance as anyone, +) + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # Given a deployed user contracts + # And an admin with address 1000 + # And an anyone with address 1001 + # Given a deployed project nft contact + # And owned by admin + # Given a deployed payment token contact + # And owned by admin + # And a total supply set to 1,000,000 + # And anyone owns the whole supply + # Given a deployed minter contact + # And owned by admin + # And a whitelist sale open + # And a public sale close + # And a max buy per tx set to 5 + # And an unit price set to 10 + # And a max supply set to 10 + # And a reserved supply set to 4 + # Given a whitelist merkle tree + # And an allocation of 5 whitelist slots to anyone + return setup() +end + +@view +func test_e2e_whitelisted{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # When anyone approves minter for 5 token equivalent nfts + # And anyone makes 5 whitelist buy + # And admin open the public sale + # And anyone approves minter for 1 token equivalent nft + # And anyone makes 1 public buy + # And admin withdraw minter contract balance + # Then no failed transactions expected + + anyone.approve(quantity=5) + anyone.whitelist_buy(quantity=5) + admin.set_public_sale_open(TRUE) + anyone.approve(quantity=1) + anyone.public_buy(quantity=1) + admin.withdraw() + + return () +end + +@view +func test_e2e_not_whitelisted{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # When admin set up a new whitelist merkle tree excluding anyone + # And anyone approves minter for 1 token equivalent nft + # And anyone makes 1 whitelist buy + # Then 'caller address is not whitelisted' failed transaction happens + # When admin open the public sale + # And anyone approves minter for 5 token equivalent nfts + # And anyone makes 5 public buy + # And admin withdraw minter contract balance + # Then no failed transactions expected + + admin.set_whitelist_merkle_root(123) + anyone.approve(quantity=1) + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: caller address is not whitelisted") %} + anyone.whitelist_buy(quantity=1) + admin.set_public_sale_open(TRUE) + anyone.approve(quantity=5) + anyone.public_buy(quantity=5) + admin.withdraw() + + return () +end + +@view +func test_e2e_airdrop{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # When anyone approves minter for 5 token equivalent nfts + # And anyone makes 5 whitelist buy + # And admin open the public sale + # And anyone approves minter for 2 token equivalent nfts + # And anyone makes 2 public buy + # Then 'not enough available NFTs' failed transaction happens + # When admin airdrops 5 nfts to anyone + # Then 'not enough available reserved NFTs' failed transaction happens + # When admin airdrops 3 nfts to anyone + # And admin decreases reserved supply by 1 + # And anyone makes 1 public buy + # And admin withdraw minter contract balance + # Then no failed transactions expected + alloc_locals + let (anyone_address) = anyone.get_address() + + anyone.approve(quantity=5) + anyone.whitelist_buy(quantity=5) + admin.set_public_sale_open(TRUE) + anyone.approve(quantity=2) + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} + 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=3) + admin.decrease_reserved_supply_for_mint(slots=1) + anyone.public_buy(quantity=1) + admin.withdraw() + + return () +end + +@view +func test_e2e_over_airdropped{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # When admin airdrops 11 nfts to anyone + # Then 'not enough available NFTs' failed transaction happens + alloc_locals + let (anyone_address) = anyone.get_address() + + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} + admin.airdrop(to=anyone_address, quantity=11) + + return () +end diff --git a/tests/integrations/test_nominal_case.cairo b/tests/integrations/test_nominal_case.cairo deleted file mode 100644 index f7f25f09..00000000 --- a/tests/integrations/test_nominal_case.cairo +++ /dev/null @@ -1,182 +0,0 @@ -# SPDX-License-Identifier: MIT -# Carbonable smart contracts written in Cairo v0.1.0 (test_nominal_case.cairo) - -%lang starknet - -# Starkware dependencies -from starkware.cairo.common.cairo_builtins import HashBuiltin -from starkware.cairo.common.bool import TRUE, FALSE - -# Project dependencies -from tests.integrations.library import ( - carbonable_minter_instance, - project_nft_instance, - payment_token_instance, - admin_instance as admin, - anyone_instance as anyone, -) - -@view -func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): - alloc_locals - tempvar carbonable_minter - tempvar whitelist_merkle_root = 3236969588476960619958150604131083087415975923122021901088942336874683133579 - %{ - # --- INITIAL SETTINGS --- - # User addresses - context.ADMIN = 1000 - context.ANYONE = 1001 - # CarbonableProjectNFT - context.NFT_NAME = 'Carbonable ERC-721 Test' - context.NFT_SYMBOL = 'CET' - # Payment token - context.TOKEN_NAME = 'StableCoinToken' - context.TOKEN_SYMBOL = 'SCT' - context.TOKEN_DECIMALS = 6 - context.TOKEN_INITIAL_SUPPLY = 1000000 - # CarbonableMint - context.PUBLIC_SALE_OPEN = ids.FALSE - context.MAX_BUY_PER_TX = 5 - context.UNIT_PRICE = 10 - context.MAX_SUPPLY_FOR_MINT = 10 - context.RESERVED_SUPPLY_FOR_MINT = 4 - # Whitelist ANYONE - context.SLOTS = 5 - context.PROOF = [ - 1489335374474017495857579265074565262713421005832572026644103123081435719307, - ] - context.PROOF_LEN = len(context.PROOF) - - # ERC-721 deployment - context.project_nft_contract = deploy_contract( - "./src/nft/project/CarbonableProjectNFT.cairo", - { - "name": context.NFT_NAME, - "symbol": context.NFT_SYMBOL, - "owner": context.ADMIN, - }, - ).contract_address - - # ERC-20 deployment - context.payment_token_contract = deploy_contract( - "./tests/mocks/token/erc20.cairo", - { - "name": context.TOKEN_NAME, - "symbol": context.TOKEN_SYMBOL, - "decimals": context.TOKEN_DECIMALS, - "initial_supply": context.TOKEN_INITIAL_SUPPLY, - "recipient": context.ANYONE - }, - ).contract_address - - # Minter deployment - context.carbonable_minter_contract = deploy_contract( - "./src/mint/minter.cairo", - { - "owner": context.ADMIN, - "project_nft_address": context.project_nft_contract, - "payment_token_address": context.payment_token_contract, - "public_sale_open": context.PUBLIC_SALE_OPEN, - "max_buy_per_tx": context.MAX_BUY_PER_TX, - "unit_price": context.UNIT_PRICE, - "max_supply_for_mint": context.MAX_SUPPLY_FOR_MINT, - "reserved_supply_for_mint": context.RESERVED_SUPPLY_FOR_MINT, - }, - ).contract_address - ids.carbonable_minter = context.carbonable_minter_contract - %} - - # Transfer project nft ownershop from admin to minter - admin.transferOwnership(carbonable_minter) - admin.set_whitelist_merkle_root(whitelist_merkle_root) - - return () -end - -@view -func test_e2e_whitelisted{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): - # STORY - # --- - # User: ANYONE - # - wants to buy 6 NFTs (5 whitelist, 1 public) - # - whitelisted: TRUE - # - has enough funds: YES - let (anyone_address) = anyone.get_address() - - anyone.approve(quantity=5) - anyone.whitelist_buy(quantity=5) - admin.set_public_sale_open(TRUE) - anyone.approve(quantity=1) - anyone.public_buy(quantity=1) - admin.withdraw() - - return () -end - -@view -func test_e2e_not_whitelisted{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): - # STORY - # --- - # User: ANYONE - # - wants to buy 6 NFTs (1 whitelist, 5 public) - # - whitelisted: FALSE - # - has enough funds: YES - - admin.set_whitelist_merkle_root(123) - anyone.approve(quantity=1) - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: caller address is not whitelisted") %} - anyone.whitelist_buy(quantity=1) - admin.set_public_sale_open(TRUE) - anyone.approve(quantity=5) - anyone.public_buy(quantity=5) - admin.withdraw() - - return () -end - -@view -func test_e2e_airdrop{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): - # User: ANYONE - # - wants to buy 6 NFTs (5 whitelist, 2 public) - # - whitelisted: TRUE - # - has enough funds: YES - # User: ADMIN - # - aidrop 5 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() - - anyone.approve(quantity=5) - anyone.whitelist_buy(quantity=5) - admin.set_public_sale_open(TRUE) - anyone.approve(quantity=2) - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} - 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=3) - admin.decrease_reserved_supply_for_mint(slots=1) - anyone.public_buy(quantity=1) - admin.withdraw() - - return () -end - -@view -func test_e2e_over_airdrop{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): - # User: ANYONE - # - wants to buy 1 NFT1 (1 whitelist) - # - whitelisted: TRUE - # - has enough funds: YES - # User: ADMIN - # - aidrop 11 nft to ANYONE - alloc_locals - let (anyone_address) = anyone.get_address() - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} - admin.airdrop(to=anyone_address, quantity=11) - - return () -end diff --git a/tests/integrations/yield/config.yml b/tests/integrations/yield/config.yml new file mode 100644 index 00000000..636543f4 --- /dev/null +++ b/tests/integrations/yield/config.yml @@ -0,0 +1,5 @@ +carbonable_token: + name: Carbonable + symbol: CARBZ + decimals: 6 + initial_supply: 1000000 \ No newline at end of file diff --git a/tests/integrations/yield/library.cairo b/tests/integrations/yield/library.cairo index e69de29b..b697e41c 100644 --- a/tests/integrations/yield/library.cairo +++ b/tests/integrations/yield/library.cairo @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (library.cairo) + +%lang starknet + +# Starkware dependencies +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.bool import TRUE, FALSE + +func setup{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + %{ + # Load config + import sys + sys.path.append('.') + from tests import load + load("./tests/integrations/yield/config.yml", context) + + # ERC-721 deployment + context.project_nft_contract = deploy_contract( + "./src/nft/project/CarbonableProjectNFT.cairo", + { + "name": context.project.name, + "symbol": context.project.symbol, + "owner": context.signers.admin, + }, + ).contract_address + + # Reward token ERC-20 deployment + context.reward_token_contract = deploy_contract( + "./tests/mocks/token/erc20.cairo", + { + "name": context.token.name, + "symbol": context.token.symbol, + "decimals": context.token.decimals, + "initial_supply": context.token.initial_supply, + "recipient": context.signers.anyone + }, + ).contract_address + + # Carbonable token ERC-20 deployment + context.carbonable_token_contract = deploy_contract( + "./tests/mocks/token/erc20.cairo", + { + "name": context.carbonable_token.name, + "symbol": context.carbonable_token.symbol, + "decimals": context.carbonable_token.decimals, + "initial_supply": context.carbonable_token.initial_supply, + "recipient": context.signers.anyone + }, + ).contract_address + + # Yield Manager deployment + context.yield_manager_contract = deploy_contract( + "./src/yield/yield_manager.cairo", + { + "owner": context.signers.admin, + "project_nft_address": context.project_nft_contract, + "carbonable_token_address": context.carbonable_token_contract, + "reward_token_address": context.reward_token_contract, + }, + ).contract_address + %} + + return () +end diff --git a/tests/integrations/yield/test_nominal_case.cairo b/tests/integrations/yield/test_nominal_case.cairo deleted file mode 100644 index f7c20b71..00000000 --- a/tests/integrations/yield/test_nominal_case.cairo +++ /dev/null @@ -1,95 +0,0 @@ -# SPDX-License-Identifier: MIT -# Carbonable smart contracts written in Cairo v0.1.0 (test_nominal_case.cairo) - -%lang starknet - -# Starkware dependencies -from starkware.cairo.common.cairo_builtins import HashBuiltin -from starkware.cairo.common.bool import TRUE, FALSE - -# Project dependencies - -@view -func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): - alloc_locals - tempvar yield_manager - %{ - # --- INITIAL SETTINGS --- - # User addresses - context.ADMIN = 1000 - context.ANYONE = 1001 - - # CarbonableProjectNFT - context.NFT_NAME = 'Carbonable ERC-721 Test' - context.NFT_SYMBOL = 'CET' - - # Reward token - context.TOKEN_NAME = 'StableCoinToken' - context.TOKEN_SYMBOL = 'SCT' - context.TOKEN_DECIMALS = 6 - context.TOKEN_INITIAL_SUPPLY = 1000000 - - # Carbonable token - context.CARBONABLE_TOKEN_NAME = 'Carbonable' - context.CARBONABLE_TOKEN_SYMBOL = 'CARBZ' - context.CARBONABLE_TOKEN_DECIMALS = 6 - context.CARBONABLE_TOKEN_INITIAL_SUPPLY = 1000000 - - # ERC-721 deployment - context.project_nft_contract = deploy_contract( - "./src/nft/project/CarbonableProjectNFT.cairo", - { - "name": context.NFT_NAME, - "symbol": context.NFT_SYMBOL, - "owner": context.ADMIN, - }, - ).contract_address - - # Reward token ERC-20 deployment - context.reward_token_contract = deploy_contract( - "./tests/mocks/token/erc20.cairo", - { - "name": context.TOKEN_NAME, - "symbol": context.TOKEN_SYMBOL, - "decimals": context.TOKEN_DECIMALS, - "initial_supply": context.TOKEN_INITIAL_SUPPLY, - "recipient": context.ANYONE - }, - ).contract_address - - # Carbonable token ERC-20 deployment - context.carbonable_token_contract = deploy_contract( - "./tests/mocks/token/erc20.cairo", - { - "name": context.CARBONABLE_TOKEN_NAME, - "symbol": context.CARBONABLE_TOKEN_SYMBOL, - "decimals": context.CARBONABLE_TOKEN_DECIMALS, - "initial_supply": context.CARBONABLE_TOKEN_INITIAL_SUPPLY, - "recipient": context.ANYONE - }, - ).contract_address - - # Yield Manager deployment - context.yield_manager_contract = deploy_contract( - "./src/yield/yield_manager.cairo", - { - "owner": context.ADMIN, - "project_nft_address": context.project_nft_contract, - "carbonable_token_address": context.carbonable_token_contract, - "reward_token_address": context.reward_token_contract, - }, - ).contract_address - ids.yield_manager = context.yield_manager_contract - %} - - return () -end - -@view -func test_e2e{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): - # STORY - # --- - # - - return () -end diff --git a/tests/integrations/yield/test_nominal_cases.cairo b/tests/integrations/yield/test_nominal_cases.cairo new file mode 100644 index 00000000..373f424f --- /dev/null +++ b/tests/integrations/yield/test_nominal_cases.cairo @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_nominal_case.cairo) + +%lang starknet + +# Starkware dependencies +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.bool import TRUE, FALSE + +# Project dependencies +from tests.integrations.yield.library import setup + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # Given a deployed user contracts + # And an admin with address 1000 + # And an anyone with address 1001 + # Given a deployed project nft contact + # And owned by admin + # Given a deployed reward token contact + # And owned by admin + # And a total supply set to 1,000,000 + # And anyone owns the whole supply + # Given a deployed carbonable token contact + # And owned by admin + # And a total supply set to 1,000,000 + # And anyone owns the whole supply + # Given a deployed yielder contract + # And owned by admin + return setup() +end + +@view +func test_e2e{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # When + + return () +end diff --git a/tests/units/config.yml b/tests/units/config.yml new file mode 100644 index 00000000..5bcccde8 --- /dev/null +++ b/tests/units/config.yml @@ -0,0 +1,2 @@ +mocks: + project_nft_address: 0x056d4ffea4ca664ffe1256af4b029998014471a87dec8036747a927ab3320b46 \ No newline at end of file diff --git a/tests/units/minter/config.yml b/tests/units/minter/config.yml new file mode 100644 index 00000000..21d62d7e --- /dev/null +++ b/tests/units/minter/config.yml @@ -0,0 +1,9 @@ +mocks: + payment_token_address: 0x073314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b82 + +whitelist: + slots: 5 + merkle_root: 3236969588476960619958150604131083087415975923122021901088942336874683133579 + merkle_proof: + - 1489335374474017495857579265074565262713421005832572026644103123081435719307 + merkle_proof_len: 1 \ No newline at end of file diff --git a/tests/units/minter/library.cairo b/tests/units/minter/library.cairo new file mode 100644 index 00000000..e34f2bf7 --- /dev/null +++ b/tests/units/minter/library.cairo @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (library.cairo) + +%lang starknet + +# Starkware dependencies +from starkware.cairo.common.alloc import alloc +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256 + +# Project dependencies +from src.mint.library import CarbonableMinter + +# Structs +struct Signers: + member admin : felt + member anyone : felt +end + +struct Mocks: + member project_nft_address : felt + member payment_token_address : felt +end + +struct Whitelist: + member slots : felt + member merkle_root : felt + member merkle_proof : felt* + member merkle_proof_len : felt +end + +struct TestContext: + member signers : Signers + member mocks : Mocks + member whitelist : Whitelist +end + +# Functions +func setup{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + %{ + # Load config + import sys + sys.path.append('.') + from tests import load + load("./tests/units/minter/config.yml", context) + %} + + return () +end + +func prepare{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}( + public_sale_open : felt, + max_buy_per_tx : felt, + unit_price : Uint256, + max_supply_for_mint : Uint256, + reserved_supply_for_mint : Uint256, +) -> (test_context : TestContext): + alloc_locals + + # Extract context variables + local admin + local anyone + local project_nft_address + local payment_token_address + local slots + local merkle_root + local merkle_proof_len + let (local merkle_proof : felt*) = alloc() + %{ + ids.admin = context.signers.admin + ids.anyone = context.signers.anyone + ids.project_nft_address = context.mocks.project_nft_address + ids.payment_token_address = context.mocks.payment_token_address + ids.slots = context.whitelist.slots + ids.merkle_root = context.whitelist.merkle_root + ids.merkle_proof_len = context.whitelist.merkle_proof_len + for index, node in enumerate(context.whitelist.merkle_proof): + memory[ids.merkle_proof + index] = node + %} + + # Instantiate minter + CarbonableMinter.constructor( + owner=admin, + project_nft_address=project_nft_address, + payment_token_address=payment_token_address, + public_sale_open=public_sale_open, + max_buy_per_tx=max_buy_per_tx, + unit_price=unit_price, + max_supply_for_mint=max_supply_for_mint, + reserved_supply_for_mint=reserved_supply_for_mint, + ) + + # Instantiate context, useful to avoid many hints in tests + local signers : Signers = Signers(admin=admin, anyone=anyone) + + local mocks : Mocks = Mocks( + project_nft_address=project_nft_address, + payment_token_address=payment_token_address, + ) + + local whitelist : Whitelist = Whitelist( + slots=slots, + merkle_root=merkle_root, + merkle_proof=merkle_proof, + merkle_proof_len=merkle_proof_len, + ) + + local context : TestContext = TestContext(signers=signers, mocks=mocks, whitelist=whitelist) + + return (context) +end diff --git a/tests/units/minter/test_airdrop.cairo b/tests/units/minter/test_airdrop.cairo new file mode 100644 index 00000000..d285981d --- /dev/null +++ b/tests/units/minter/test_airdrop.cairo @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_minter.cairo) + +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256 +from starkware.cairo.common.bool import TRUE, FALSE + +from tests.units.minter.library import setup, prepare, CarbonableMinter + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + return setup() +end + +@external +func test_airdrop_nominal_case{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # User: admin + # Wants to aidrop 5 NFTs + # Whitelisted sale: OPEN + # Public sale: CLOSED + # current NFT totalSupply: 5 + # current NFT reserved supply: 5 + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(5, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.admin) %} + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [5, 0]) %} + %{ mock_call(context.mocks.project_nft_address, "mint", []) %} + CarbonableMinter.airdrop(to=context.signers.anyone, quantity=5) + %{ stop() %} + return () +end + +@external +func test_airdrop_revert_if_not_owner{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + %{ expect_revert("TRANSACTION_FAILED", "Ownable: caller is not the owner") %} + CarbonableMinter.airdrop(to=context.signers.anyone, quantity=1) + %{ stop() %} + return () +end + +@external +func test_airdrop_revert_not_enough_nfts_available{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: admin + # Wants to airdrop 5 NFTs then 1 NFT then 1 NFT + # Whitelisted sale: CLOSED + # Public sale: OPEN + # current NFT totalSupply: 6 + # current NFT reserved supply: 1 + # has enough funds: YES + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=TRUE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(1, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.admin) %} + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [6, 0]) %} + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} + CarbonableMinter.airdrop(to=context.signers.anyone, quantity=5) + CarbonableMinter.airdrop(to=context.signers.anyone, quantity=1) + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available reserved NFTs") %} + CarbonableMinter.airdrop(to=context.signers.anyone, quantity=1) + %{ stop() %} + return () +end diff --git a/tests/units/minter/test_decrease_reserved_supply.cairo b/tests/units/minter/test_decrease_reserved_supply.cairo new file mode 100644 index 00000000..913107c1 --- /dev/null +++ b/tests/units/minter/test_decrease_reserved_supply.cairo @@ -0,0 +1,109 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_minter.cairo) + +%lang starknet + +# Starkware dependencies +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256 +from starkware.cairo.common.bool import TRUE, FALSE + +# OZ dependencies +from openzeppelin.security.safemath import SafeUint256 + +# Project dependencies +from tests.units.minter.library import setup, prepare, CarbonableMinter + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + return setup() +end + +@external +func test_decrease_reserved_supply_nominal_case{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: admin + # Wants to decrease the reserved supply by 2 + # Whitelisted sale: OPEN + # Public sale: CLOSED + # current NFT reserved supply: 5 + alloc_locals + + # prepare minter instance + let reserved_supply = Uint256(5, 0) + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=reserved_supply, + ) + + # run scenario + %{ stop=start_prank(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 +}(): + # User: anyone + # Wants to decrease the reserved supply by 2 + # Whitelisted sale: OPEN + # Public sale: CLOSED + # current NFT reserved supply: 5 + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(5, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + %{ expect_revert("TRANSACTION_FAILED", "Ownable: caller is not the owner") %} + CarbonableMinter.decrease_reserved_supply_for_mint(slots=Uint256(2, 0)) + %{ stop() %} + return () +end + +@external +func test_decrease_reserved_supply_revert_over_decreased{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: admin + # Wants to decrease the reserved supply by 6 + # Whitelisted sale: OPEN + # Public sale: CLOSED + # current NFT reserved supply: 5 + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(5, 0), + ) + + # run scenario + %{ stop=start_prank(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 diff --git a/tests/units/minter/test_initialization.cairo b/tests/units/minter/test_initialization.cairo new file mode 100644 index 00000000..29e51fdc --- /dev/null +++ b/tests/units/minter/test_initialization.cairo @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_minter.cairo) + +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256 +from starkware.cairo.common.bool import TRUE, FALSE + +from tests.units.minter.library import setup, prepare, CarbonableMinter + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + return setup() +end + +@external +func test_initialization{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + alloc_locals + + # prepare minter instance + let public_sale_open = FALSE + let max_buy_per_tx = 5 + let unit_price = Uint256(10, 0) + let max_supply_for_mint = Uint256(10, 0) + let reserved_supply_for_mint = Uint256(5, 0) + let (local context) = prepare( + public_sale_open=public_sale_open, + max_buy_per_tx=max_buy_per_tx, + unit_price=unit_price, + max_supply_for_mint=max_supply_for_mint, + reserved_supply_for_mint=reserved_supply_for_mint, + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + let (returned_project_nft_address) = CarbonableMinter.project_nft_address() + assert returned_project_nft_address = context.mocks.project_nft_address + + let (payment_token_address) = CarbonableMinter.payment_token_address() + assert payment_token_address = context.mocks.payment_token_address + + let (whitelisted_sale_open) = CarbonableMinter.whitelisted_sale_open() + let (whitelist_merkle_root) = CarbonableMinter.whitelist_merkle_root() + assert whitelisted_sale_open = whitelist_merkle_root + + let (returned_public_sale_open) = CarbonableMinter.public_sale_open() + assert returned_public_sale_open = public_sale_open + + let (returned_max_buy_per_tx) = CarbonableMinter.max_buy_per_tx() + assert returned_max_buy_per_tx = max_buy_per_tx + + let (returned_unit_price) = CarbonableMinter.unit_price() + assert returned_unit_price = unit_price + + let (returned_reserved_supply_for_mint) = CarbonableMinter.reserved_supply_for_mint() + assert returned_reserved_supply_for_mint = reserved_supply_for_mint + %{ stop() %} + + return () +end diff --git a/tests/units/minter/test_public_buy.cairo b/tests/units/minter/test_public_buy.cairo new file mode 100644 index 00000000..429fe1ad --- /dev/null +++ b/tests/units/minter/test_public_buy.cairo @@ -0,0 +1,181 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_minter.cairo) + +%lang starknet + +from starkware.cairo.common.alloc import alloc +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 tests.units.minter.library import setup, prepare, CarbonableMinter + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + return setup() +end + +@external +func test_buy_nominal_case{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # User: anyone + # Wants to buy 2 NFTs + # Whitelisted sale: CLOSED + # Public sale: OPEN + # current NFT totalSupply: 5 + # current NFT reserved supply: 0 + # has enough funds: YES + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=TRUE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [5, 0]) %} + %{ mock_call(context.mocks.project_nft_address, "mint", []) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [1]) %} + let (success) = CarbonableMinter.public_buy(2) + assert success = TRUE + %{ stop() %} + return () +end + +@external +func test_buy_revert_not_enough_nfts_available{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: anyone + # Wants to buy 2 NFTs + # Whitelisted sale: CLOSED + # Public sale: OPEN + # current NFT totalSupply: 10 + # current NFT reserved supply: 0 + # has enough funds: YES + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=TRUE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + let quantity = 2 + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [10, 0]) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [1]) %} + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} + CarbonableMinter.public_buy(quantity) + %{ stop() %} + return () +end + +@external +func test_buy_revert_not_enough_free_nfts{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: anyone + # Wants to buy 2 NFTs + # Whitelisted sale: CLOSED + # Public sale: OPEN + # current NFT totalSupply: 0 + # current NFT reserved supply: 9 + # has enough funds: YES + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=TRUE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(9, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + let quantity = 2 + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [0, 0]) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [1]) %} + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} + CarbonableMinter.public_buy(quantity) + %{ stop() %} + return () +end + +@external +func test_buy_revert_transfer_failed{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: anyone + # Wants to buy 2 NFTs + # Whitelisted sale: CLOSED + # Public sale: OPEN + # current NFT totalSupply: 5 + # current NFT reserved supply: 0 + # has enough funds: NO + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=TRUE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + let quantity = 2 + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [5, 0]) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [0]) %} + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: transfer failed") %} + CarbonableMinter.public_buy(quantity) + %{ stop() %} + return () +end + +@external +func test_buy_revert_mint_not_open{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: anyone + # Wants to buy 2 NFTs + # Whitelisted sale: CLOSED + # Public sale: CLOSED + # current NFT totalSupply: 5 + # current NFT reserved supply: 0 + # has enough funds: YES + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + let quantity = 2 + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [5, 0]) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [1]) %} + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: public sale is not open") %} + CarbonableMinter.public_buy(quantity) + %{ stop() %} + return () +end diff --git a/tests/units/minter/test_set_merkle_tree.cairo b/tests/units/minter/test_set_merkle_tree.cairo new file mode 100644 index 00000000..24e15f9d --- /dev/null +++ b/tests/units/minter/test_set_merkle_tree.cairo @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_minter.cairo) + +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256 +from starkware.cairo.common.bool import TRUE, FALSE + +from tests.units.minter.library import setup, prepare, CarbonableMinter + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + return setup() +end + +@external +func test_set_whitelist_merkle_root_nominal_case{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # admin sets whitelist merkle root + %{ stop=start_prank(context.signers.admin) %} + CarbonableMinter.set_whitelist_merkle_root(context.whitelist.merkle_root) + %{ stop() %} + + # run scenario + %{ stop=start_prank(context.signers.admin) %} + CarbonableMinter.set_whitelist_merkle_root(context.whitelist.merkle_root) + let (returned_slots) = CarbonableMinter.whitelisted_slots( + account=context.signers.anyone, + slots=context.whitelist.slots, + proof_len=context.whitelist.merkle_proof_len, + proof=context.whitelist.merkle_proof, + ) + assert returned_slots = context.whitelist.slots + %{ stop() %} + return () +end + +@external +func test_set_merkle_tree_revert_if_not_owner{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + %{ expect_revert("TRANSACTION_FAILED", "Ownable: caller is not the owner") %} + CarbonableMinter.set_whitelist_merkle_root(123) + %{ stop() %} + return () +end diff --git a/tests/units/minter/test_whitelist_buy.cairo b/tests/units/minter/test_whitelist_buy.cairo new file mode 100644 index 00000000..5febbf2b --- /dev/null +++ b/tests/units/minter/test_whitelist_buy.cairo @@ -0,0 +1,223 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_minter.cairo) + +%lang starknet + +from starkware.cairo.common.alloc import alloc +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 tests.units.minter.library import setup, prepare, CarbonableMinter + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # Given a deployed user contracts + # And an admin with address 1000 + # And an anyone with address 1001 + # Given a deployed project nft contact + # Given a deployed payment token contact + # Given a deployed minter contact + # And owned by admin + # And a whitelist sale close + # And a public sale close + # And a max buy per tx set to 5 + # And an unit price set to 10 + # And a max supply set to 10 + # And a reserved supply set to 0 + return setup() +end + +@external +func test_whitelist_buy_nominal_case{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # Given a deployed project nft contact + # And a total supply of 5 nfts + # And the mint function succeeds + # Given a deployed payment token contact + # And the transferFrom function succeeds + # Given a whitelist merkle tree + # And an allocation of 5 whitelist slots to anyone + # When admin set up the whitelist merkle tree + # And anyone makes 2 whitelist buy + # Then no failed transactions expected + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # admin sets whitelist merkle root + %{ stop=start_prank(context.signers.admin) %} + CarbonableMinter.set_whitelist_merkle_root(context.whitelist.merkle_root) + %{ stop() %} + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [5, 0]) %} + %{ mock_call(context.mocks.project_nft_address, "mint", []) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [1]) %} + let (success) = CarbonableMinter.whitelist_buy( + slots=context.whitelist.slots, + proof_len=context.whitelist.merkle_proof_len, + proof=context.whitelist.merkle_proof, + quantity=2, + ) + assert success = TRUE + %{ stop() %} + return () +end + +@external +func test_buy_user_whitelisted_but_not_enough_slots{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: anyone + # Wants to buy 6 NFTs + # Whitelisted sale: OPEN + # Public sale: CLOSED + # Is user whitelisted: YES (but not enough slots) + # current NFT totalSupply: 5 + # current NFT reserved supply: 0 + # has enough funds: YES + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # admin sets whitelist merkle root + %{ stop=start_prank(context.signers.admin) %} + CarbonableMinter.set_whitelist_merkle_root(context.whitelist.merkle_root) + %{ stop() %} + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [3, 0]) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [1]) %} + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough whitelisted slots available") %} + CarbonableMinter.whitelist_buy( + slots=context.whitelist.slots, + proof_len=context.whitelist.merkle_proof_len, + proof=context.whitelist.merkle_proof, + quantity=6, + ) + %{ stop() %} + return () +end + +@external +func test_buy_user_whitelisted_but_not_enough_slots_after_claim{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: anyone + # Wants to buy 5 NFTs and then 1 NFT + # Whitelisted sale: OPEN + # Public sale: CLOSED + # Is user whitelisted: YES (but not enough slots for second buy) + # current NFT totalSupply: 3 + # current NFT reserved supply: 0 + # has enough funds: YES + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # admin sets whitelist merkle root + %{ stop=start_prank(context.signers.admin) %} + CarbonableMinter.set_whitelist_merkle_root(context.whitelist.merkle_root) + %{ stop() %} + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + + # Mock + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [3, 0]) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [1]) %} + %{ mock_call(context.mocks.project_nft_address, "mint", []) %} + + # First buy + # Call whitelist_buy + CarbonableMinter.whitelist_buy( + slots=context.whitelist.slots, + proof_len=context.whitelist.merkle_proof_len, + proof=context.whitelist.merkle_proof, + quantity=5, + ) + + # Expect error + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough whitelisted slots available") %} + + # Second buy + # Call whitelist_buy + CarbonableMinter.whitelist_buy( + slots=context.whitelist.slots, + proof_len=context.whitelist.merkle_proof_len, + proof=context.whitelist.merkle_proof, + quantity=1, + ) + %{ stop() %} + return () +end + +@external +func test_buy_revert_not_whitelisted{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # User: anyone + # Wants to buy 2 NFTs + # Whitelisted sale: OPEN + # Public sale: CLOSED + # Is user whitelisted: NO + # current NFT totalSupply: 5 + # current NFT reserved supply: 0 + # has enough funds: YES + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # admin sets whitelist merkle root + %{ stop=start_prank(context.signers.admin) %} + CarbonableMinter.set_whitelist_merkle_root(123) + %{ stop() %} + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + %{ mock_call(context.mocks.project_nft_address, "totalSupply", [5, 0]) %} + %{ mock_call(context.mocks.payment_token_address, "transferFrom", [1]) %} + %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: caller address is not whitelisted") %} + CarbonableMinter.whitelist_buy( + slots=context.whitelist.slots, + proof_len=context.whitelist.merkle_proof_len, + proof=context.whitelist.merkle_proof, + quantity=2, + ) + %{ stop() %} + return () +end diff --git a/tests/units/minter/test_withdraw.cairo b/tests/units/minter/test_withdraw.cairo new file mode 100644 index 00000000..91d5bfdc --- /dev/null +++ b/tests/units/minter/test_withdraw.cairo @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_minter.cairo) + +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256 +from starkware.cairo.common.bool import TRUE, FALSE + +from tests.units.minter.library import setup, prepare, CarbonableMinter + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + # Given a deployed user contracts + # And an admin with address 1000 + # And an anyone with address 1001 + # Given a deployed project nft contact + # Given a deployed payment token contact + # Given a deployed minter contact + # And owned by admin + # And a whitelist sale close + # And a public sale close + # And a max buy per tx set to 5 + # And an unit price set to 10 + # And a max supply set to 10 + # And a reserved supply set to 0 + return setup() +end + +@external +func test_withdraw_nominal_case{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}( + ): + # Given a deployed payment token contact + # And the transfer function succeeds + # Given a deployed minter contact + # And a balance of payment token at 5 + # When admin withdraws funds + # Then no failed transactions expected + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.admin) %} + %{ mock_call(context.mocks.payment_token_address, "balanceOf", [5, 0]) %} + %{ mock_call(context.mocks.payment_token_address, "transfer", [1]) %} + let (success) = CarbonableMinter.withdraw() + assert success = TRUE + %{ stop() %} + return () +end + +@external +func test_withdraw_revert_not_owner{ + syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr +}(): + # When anyone withdraws funds + # Then 'caller is not the owner' failed transaction happens + alloc_locals + + # prepare minter instance + let (local context) = prepare( + public_sale_open=FALSE, + max_buy_per_tx=5, + unit_price=Uint256(10, 0), + max_supply_for_mint=Uint256(10, 0), + reserved_supply_for_mint=Uint256(0, 0), + ) + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + %{ expect_revert("TRANSACTION_FAILED", "Ownable: caller is not the owner") %} + CarbonableMinter.withdraw() + %{ stop() %} + return () +end diff --git a/tests/units/test_minter.cairo b/tests/units/test_minter.cairo deleted file mode 100644 index 16eab5d8..00000000 --- a/tests/units/test_minter.cairo +++ /dev/null @@ -1,616 +0,0 @@ -# SPDX-License-Identifier: MIT -# Carbonable smart contracts written in Cairo v0.1.0 (test_minter.cairo) - -%lang starknet - -from starkware.cairo.common.alloc import alloc -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 -const PAYMENT_TOKEN_ADDRESS = 0x073314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b82 -const ADMIN = 1000 -const ANYONE_1 = 1001 -const ANYONE_2 = 1002 -const ANYONE_3 = 1003 - -const MERKLE_ROOT = 3236969588476960619958150604131083087415975923122021901088942336874683133579 -const PROOF = 1489335374474017495857579265074565262713421005832572026644103123081435719307 - -# ------- -# STRUCTS -# ------- - -struct Signers: - member admin : felt - member anyone_1 : felt - member anyone_2 : felt - member anyone_3 : felt -end - -struct Mocks: - member payment_token_address : felt - member project_nft_address : felt -end - -struct TestContext: - member signers : Signers - member mocks : Mocks - - member whitelist_merkle_root : felt - member public_sale_open : felt - member max_buy_per_tx : felt - member unit_price : Uint256 - member max_supply_for_mint : Uint256 - member reserved_supply_for_mint : Uint256 -end - -@external -func test_buy_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(0, 0) - let (local context : TestContext) = test_internal.prepare( - 1, TRUE, 5, unit_price, max_supply, reserved_supply - ) - - # User: anyone_1 - # Wants to buy 2 NFTs - # Whitelisted sale: CLOSED - # Public sale: OPEN - # current NFT totalSupply: 5 - # current NFT reserved supply: 0 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.anyone_1) %} - let quantity = 2 - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [5, 0]) %} - %{ mock_call(ids.context.mocks.project_nft_address, "mint", []) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [1]) %} - let (success) = CarbonableMinter.public_buy(quantity) - assert success = TRUE - %{ stop() %} - return () -end - -@external -func test_buy_revert_not_enough_nfts_available{ - 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(0, 0) - let (local context : TestContext) = test_internal.prepare( - 1, TRUE, 5, unit_price, max_supply, reserved_supply - ) - - # User: anyone_1 - # Wants to buy 2 NFTs - # Whitelisted sale: CLOSED - # Public sale: OPEN - # current NFT totalSupply: 10 - # current NFT reserved supply: 0 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.anyone_1) %} - let quantity = 2 - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [10, 0]) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [1]) %} - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} - CarbonableMinter.public_buy(quantity) - %{ stop() %} - return () -end - -@external -func test_buy_revert_not_enough_free_nfts{ - 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(9, 0) - let (local context : TestContext) = test_internal.prepare( - 1, TRUE, 5, unit_price, max_supply, reserved_supply - ) - - # User: anyone_1 - # Wants to buy 2 NFTs - # Whitelisted sale: CLOSED - # Public sale: OPEN - # current NFT totalSupply: 0 - # current NFT reserved supply: 9 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.anyone_1) %} - let quantity = 2 - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [0, 0]) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [1]) %} - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough available NFTs") %} - CarbonableMinter.public_buy(quantity) - %{ stop() %} - return () -end - -@external -func test_buy_revert_transfer_failed{ - 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(0, 0) - let (local context : TestContext) = test_internal.prepare( - 1, TRUE, 5, unit_price, max_supply, reserved_supply - ) - - # User: anyone_1 - # Wants to buy 2 NFTs - # Whitelisted sale: CLOSED - # Public sale: OPEN - # current NFT totalSupply: 5 - # current NFT reserved supply: 0 - # has enough funds: NO - %{ stop=start_prank(ids.context.signers.anyone_1) %} - let quantity = 2 - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [5, 0]) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [0]) %} - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: transfer failed") %} - CarbonableMinter.public_buy(quantity) - %{ stop() %} - return () -end - -@external -func test_buy_revert_mint_not_open{ - 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(0, 0) - let (local context : TestContext) = test_internal.prepare( - 0, FALSE, 5, unit_price, max_supply, reserved_supply - ) - - # User: anyone_1 - # Wants to buy 2 NFTs - # Whitelisted sale: CLOSED - # Public sale: CLOSED - # current NFT totalSupply: 5 - # current NFT reserved supply: 0 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.anyone_1) %} - let quantity = 2 - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [5, 0]) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [1]) %} - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: public sale is not open") %} - CarbonableMinter.public_buy(quantity) - %{ stop() %} - return () -end - -@external -func test_buy_revert_not_whitelisted{ - 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(0, 0) - let (local context : TestContext) = test_internal.prepare( - MERKLE_ROOT, FALSE, 5, unit_price, max_supply, reserved_supply - ) - let (local proof : felt*) = alloc() - assert [proof] = 1 - - # User: anyone_1 - # Wants to buy 2 NFTs - # Whitelisted sale: OPEN - # Public sale: CLOSED - # Is user whitelisted: NO - # current NFT totalSupply: 5 - # current NFT reserved supply: 0 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.anyone_1) %} - let quantity = 2 - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [5, 0]) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [1]) %} - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: caller address is not whitelisted") %} - CarbonableMinter.whitelist_buy(slots=5, proof_len=1, proof=proof, quantity=quantity) - %{ stop() %} - return () -end - -@external -func test_set_whitelist_merkle_root_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(0, 0) - let (local context : TestContext) = test_internal.prepare( - MERKLE_ROOT, FALSE, 5, unit_price, max_supply, reserved_supply - ) - let (local proof : felt*) = alloc() - assert [proof] = PROOF - - %{ stop=start_prank(ids.context.signers.admin) %} - CarbonableMinter.set_whitelist_merkle_root(context.whitelist_merkle_root) - let (slots) = CarbonableMinter.whitelisted_slots( - account=context.signers.anyone_1, slots=5, proof_len=1, proof=proof - ) - assert slots = 5 - %{ stop() %} - return () -end - -@external -func test_set_merkle_tree_revert_if_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(0, 0) - let (local context : TestContext) = test_internal.prepare( - MERKLE_ROOT, FALSE, 5, unit_price, max_supply, reserved_supply - ) - %{ stop=start_prank(ids.context.signers.anyone_1) %} - %{ expect_revert("TRANSACTION_FAILED", "Ownable: caller is not the owner") %} - CarbonableMinter.set_whitelist_merkle_root(123) - %{ stop() %} - return () -end - -@external -func test_buy_user_whitelisted{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(0, 0) - let (local context : TestContext) = test_internal.prepare( - MERKLE_ROOT, FALSE, 5, unit_price, max_supply, reserved_supply - ) - let (local proof : felt*) = alloc() - assert [proof] = PROOF - - # User: anyone_1 - # Wants to buy 2 NFTs - # Whitelisted sale: OPEN - # Public sale: CLOSED - # Is user whitelisted: YES - # current NFT totalSupply: 5 - # current NFT reserved supply: 0 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.anyone_1) %} - let quantity = 2 - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [5, 0]) %} - %{ mock_call(ids.context.mocks.project_nft_address, "mint", []) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [1]) %} - let (success) = CarbonableMinter.whitelist_buy( - slots=5, proof_len=1, proof=proof, quantity=quantity - ) - assert success = TRUE - %{ stop() %} - return () -end - -@external -func test_buy_user_whitelisted_but_not_enough_slots{ - 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(0, 0) - let (local context : TestContext) = test_internal.prepare( - MERKLE_ROOT, FALSE, 5, unit_price, max_supply, reserved_supply - ) - let (local proof : felt*) = alloc() - assert [proof] = PROOF - - # User: anyone_1 - # Wants to buy 6 NFTs - # Whitelisted sale: OPEN - # Public sale: CLOSED - # Is user whitelisted: YES (but not enough slots) - # current NFT totalSupply: 5 - # current NFT reserved supply: 0 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.anyone_1) %} - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [3, 0]) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [1]) %} - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough whitelisted slots available") %} - CarbonableMinter.whitelist_buy(slots=5, proof_len=1, proof=proof, quantity=6) - %{ stop() %} - return () -end - -@external -func test_buy_user_whitelisted_but_not_enough_slots_after_claim{ - 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(0, 0) - let (local context : TestContext) = test_internal.prepare( - MERKLE_ROOT, FALSE, 5, unit_price, max_supply, reserved_supply - ) - let (local proof : felt*) = alloc() - assert [proof] = PROOF - - # User: anyone_1 - # Wants to buy 5 NFTs and then 1 NFT - # Whitelisted sale: OPEN - # Public sale: CLOSED - # Is user whitelisted: YES (but not enough slots for second buy) - # current NFT totalSupply: 3 - # current NFT reserved supply: 0 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.anyone_1) %} - - # Mock - %{ mock_call(ids.context.mocks.project_nft_address, "totalSupply", [3, 0]) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transferFrom", [1]) %} - %{ mock_call(ids.context.mocks.project_nft_address, "mint", []) %} - - # First buy - # Call whitelist_buy - CarbonableMinter.whitelist_buy(slots=5, proof_len=1, proof=proof, quantity=5) - - # Expect error - %{ expect_revert("TRANSACTION_FAILED", "CarbonableMinter: not enough whitelisted slots available") %} - - # Second buy - # Call whitelist_buy - CarbonableMinter.whitelist_buy(slots=5, proof_len=1, proof=proof, quantity=1) - %{ stop() %} - return () -end - -@external -func test_airdrop_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 aidrop 5 NFTs - # Whitelisted sale: OPEN - # Public sale: CLOSED - # current NFT totalSupply: 5 - # current NFT reserved supply: 5 - %{ stop=start_prank(ids.context.signers.admin) %} - %{ 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=5) - %{ stop() %} - return () -end - -@external -func test_airdrop_revert_if_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(0, 0) - let (local context : TestContext) = test_internal.prepare( - 1, FALSE, 5, unit_price, max_supply, reserved_supply - ) - %{ stop=start_prank(ids.context.signers.anyone_1) %} - %{ expect_revert("TRANSACTION_FAILED", "Ownable: caller is not the owner") %} - CarbonableMinter.airdrop(to=context.signers.anyone_1, quantity=1) - %{ stop() %} - return () -end - -@external -func test_airdrop_revert_not_enough_nfts_available{ - 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(1, 0) - let (local context : TestContext) = test_internal.prepare( - 1, TRUE, 5, unit_price, max_supply, reserved_supply - ) - - # User: admin - # Wants to airdrop 5 NFTs then 1 NFT then 1 NFT - # Whitelisted sale: CLOSED - # Public sale: OPEN - # current NFT totalSupply: 6 - # current NFT reserved supply: 1 - # has enough funds: YES - %{ stop=start_prank(ids.context.signers.admin) %} - %{ 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=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) - %{ stop() %} - 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}( - ): - alloc_locals - let unit_price = Uint256(10, 0) - let max_supply = Uint256(10, 0) - let reserved_supply = Uint256(0, 0) - let (local context : TestContext) = test_internal.prepare( - 1, TRUE, 5, unit_price, max_supply, reserved_supply - ) - - # User: admin - # Withdraw contract balance - %{ stop=start_prank(ids.context.signers.admin) %} - %{ mock_call(ids.context.mocks.payment_token_address, "balanceOf", [5, 0]) %} - %{ mock_call(ids.context.mocks.payment_token_address, "transfer", [1]) %} - let (success) = CarbonableMinter.withdraw() - assert success = TRUE - %{ stop() %} - return () -end - -@external -func test_withdraw_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(0, 0) - let (local context : TestContext) = test_internal.prepare( - 1, TRUE, 5, unit_price, max_supply, reserved_supply - ) - - # User: anyone_1 - # Withdraw contract balance - %{ stop=start_prank(ids.context.signers.anyone_1) %} - %{ expect_revert("TRANSACTION_FAILED", "Ownable: caller is not the owner") %} - CarbonableMinter.withdraw() - %{ stop() %} - return () -end - -namespace test_internal: - func prepare{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}( - whitelist_merkle_root : felt, - public_sale_open : felt, - max_buy_per_tx : felt, - unit_price : Uint256, - max_supply_for_mint : Uint256, - reserved_supply_for_mint : Uint256, - ) -> (test_context : TestContext): - alloc_locals - local signers : Signers = Signers(admin=ADMIN, anyone_1=ANYONE_1, anyone_2=ANYONE_2, anyone_3=ANYONE_3) - - local mocks : Mocks = Mocks( - payment_token_address=PAYMENT_TOKEN_ADDRESS, - project_nft_address=PROJECT_NFT_ADDRESS, - ) - - local context : TestContext = TestContext( - signers=signers, - mocks=mocks, - whitelist_merkle_root=whitelist_merkle_root, - public_sale_open=public_sale_open, - max_buy_per_tx=max_buy_per_tx, - unit_price=unit_price, - max_supply_for_mint=max_supply_for_mint, - reserved_supply_for_mint=reserved_supply_for_mint, - ) - - CarbonableMinter.constructor( - signers.admin, - mocks.project_nft_address, - mocks.payment_token_address, - context.public_sale_open, - context.max_buy_per_tx, - context.unit_price, - context.max_supply_for_mint, - context.reserved_supply_for_mint, - ) - - # Admin adds whitelist_merkle_root including anyone_1 with 5 slots - %{ stop=start_prank(ids.context.signers.admin) %} - CarbonableMinter.set_whitelist_merkle_root(context.whitelist_merkle_root) - %{ stop() %} - - return (test_context=context) - end -end diff --git a/tests/units/test_yield_manager.cairo b/tests/units/test_yield_manager.cairo deleted file mode 100644 index 61d118c4..00000000 --- a/tests/units/test_yield_manager.cairo +++ /dev/null @@ -1,87 +0,0 @@ -# SPDX-License-Identifier: MIT -# Carbonable smart contracts written in Cairo v0.1.0 (test_yield_manager.cairo) - -%lang starknet - -from starkware.cairo.common.alloc import alloc -from starkware.cairo.common.cairo_builtins import HashBuiltin -from starkware.cairo.common.uint256 import Uint256 -from starkware.cairo.common.bool import TRUE, FALSE - -from src.yield.library import YieldManager - -const PROJECT_NFT_ADDRESS = 0x056d4ffea4ca664ffe1256af4b029998014471a87dec8036747a927ab3320b46 -const reward_token_address = 0x073314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b82 -const CARBONABLE_TOKEN_ADDRESS = 0x043eB0d3D84CC1f850D4b6dEe9630cAc35Af99980BAA30577A76adEa72475598 - -const ADMIN = 1000 -const ANYONE_1 = 1001 -const ANYONE_2 = 1002 -const ANYONE_3 = 1003 - -# ------- -# STRUCTS -# ------- - -struct Signers: - member admin : felt - member anyone_1 : felt - member anyone_2 : felt - member anyone_3 : felt -end - -struct Mocks: - member reward_token_address : felt - member project_nft_address : felt - member carbonable_token_address : felt -end - -struct TestContext: - member signers : Signers - member mocks : Mocks -end - -@external -func test_initialization{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): - alloc_locals - - let (local context : TestContext) = test_internal.prepare() - - %{ stop=start_prank(ids.context.signers.anyone_1) %} - let (reward_token_address) = YieldManager.reward_token_address() - assert reward_token_address = context.mocks.reward_token_address - let (project_nft_address) = YieldManager.project_nft_address() - assert project_nft_address = context.mocks.project_nft_address - let (carbonable_token_address) = YieldManager.carbonable_token_address() - assert carbonable_token_address = context.mocks.carbonable_token_address - %{ stop() %} - return () -end - -namespace test_internal: - func prepare{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> ( - test_context : TestContext - ): - alloc_locals - local signers : Signers = Signers(admin=ADMIN, anyone_1=ANYONE_1, anyone_2=ANYONE_2, anyone_3=ANYONE_3) - - local mocks : Mocks = Mocks( - reward_token_address=reward_token_address, - project_nft_address=PROJECT_NFT_ADDRESS, - carbonable_token_address=CARBONABLE_TOKEN_ADDRESS, - ) - - local context : TestContext = TestContext( - signers=signers, - mocks=mocks, - ) - - YieldManager.constructor( - signers.admin, - mocks.project_nft_address, - mocks.carbonable_token_address, - mocks.reward_token_address, - ) - return (test_context=context) - end -end diff --git a/tests/units/yielder/config.yml b/tests/units/yielder/config.yml new file mode 100644 index 00000000..99212eb0 --- /dev/null +++ b/tests/units/yielder/config.yml @@ -0,0 +1,3 @@ +mocks: + carbonable_token_address: 0x043eB0d3D84CC1f850D4b6dEe9630cAc35Af99980BAA30577A76adEa72475598 + reward_token_address: 0x073314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b82 \ No newline at end of file diff --git a/tests/units/yielder/library.cairo b/tests/units/yielder/library.cairo new file mode 100644 index 00000000..1c4c46c2 --- /dev/null +++ b/tests/units/yielder/library.cairo @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (library.cairo) + +%lang starknet + +# Starkware dependencies +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256 + +# Project dependencies +from src.yield.library import YieldManager + +# Structs +struct Signers: + member admin : felt + member anyone : felt +end + +struct Mocks: + member project_nft_address : felt + member carbonable_token_address : felt + member reward_token_address : felt +end + +struct TestContext: + member signers : Signers + member mocks : Mocks +end + +# Functions +func setup{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + %{ + # Load config + import sys + sys.path.append('.') + from tests import load + load("./tests/units/yielder/config.yml", context) + %} + + return () +end + +func prepare{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> ( + test_context : TestContext +): + alloc_locals + + # Extract context variables + local admin + local anyone + local project_nft_address + local carbonable_token_address + local reward_token_address + %{ + ids.admin = context.signers.admin + ids.anyone = context.signers.anyone + ids.project_nft_address = context.mocks.project_nft_address + ids.carbonable_token_address = context.mocks.carbonable_token_address + ids.reward_token_address = context.mocks.reward_token_address + %} + + # Instantiate yielder + YieldManager.constructor( + owner=admin, + project_nft_address=project_nft_address, + carbonable_token_address=carbonable_token_address, + reward_token_address=reward_token_address, + ) + + # Instantiate context, useful to avoid many hints in tests + local signers : Signers = Signers(admin=admin, anyone=anyone) + + local mocks : Mocks = Mocks( + project_nft_address=project_nft_address, + carbonable_token_address=carbonable_token_address, + reward_token_address=reward_token_address, + ) + + local context : TestContext = TestContext(signers=signers, mocks=mocks) + return (test_context=context) +end diff --git a/tests/units/yielder/test_initialization.cairo b/tests/units/yielder/test_initialization.cairo new file mode 100644 index 00000000..2dee2f25 --- /dev/null +++ b/tests/units/yielder/test_initialization.cairo @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: MIT +# Carbonable smart contracts written in Cairo v0.1.0 (test_yielder.cairo) + +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.bool import TRUE, FALSE + +from tests.units.yielder.library import setup, prepare, YieldManager + +@view +func __setup__{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + return setup() +end + +@external +func test_initialization{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): + alloc_locals + + # prepare yielder instance + let (local context) = prepare() + + # run scenario + %{ stop=start_prank(context.signers.anyone) %} + let (project_nft_address) = YieldManager.project_nft_address() + assert project_nft_address = context.mocks.project_nft_address + + let (carbonable_token_address) = YieldManager.carbonable_token_address() + assert carbonable_token_address = context.mocks.carbonable_token_address + + let (reward_token_address) = YieldManager.reward_token_address() + assert reward_token_address = context.mocks.reward_token_address + %{ stop() %} + + return () +end