diff --git a/src/components/offsetter/interface.cairo b/src/components/offsetter/interface.cairo index ee1217e..a86dfae 100644 --- a/src/components/offsetter/interface.cairo +++ b/src/components/offsetter/interface.cairo @@ -14,10 +14,26 @@ trait IOffsetHandler { ref self: TContractState, token_ids: Span, cc_amounts: Span ); - fn claim( + ///Verify and validate the proof on the Merkle tree side to confirm the offset. + fn confirm_for_merkle_tree( + ref self: TContractState, + from: ContractAddress, + amount: u128, + timestamp: u128, + id: u128, + proof: Array:: + ) -> bool; + + ///Verify on the business logic side, confirm on the Merkle tree side, and perform the offset action. + fn confirm_offset( ref self: TContractState, amount: u128, timestamp: u128, id: u128, proof: Array:: ); + fn get_allocation_id(self: @TContractState, from: ContractAddress) -> u256; + + + fn get_retirement(self: @TContractState, token_id: u256, from: ContractAddress) -> u256; + /// Get the pending retirement of a vintage for the caller address. fn get_pending_retirement( self: @TContractState, address: ContractAddress, token_id: u256 diff --git a/src/components/offsetter/offset_handler.cairo b/src/components/offsetter/offset_handler.cairo index 8a0d4ae..eae2528 100644 --- a/src/components/offsetter/offset_handler.cairo +++ b/src/components/offsetter/offset_handler.cairo @@ -154,34 +154,57 @@ mod OffsetComponent { }; } - fn claim( + fn confirm_for_merkle_tree( ref self: ComponentState, + from: ContractAddress, amount: u128, timestamp: u128, id: u128, proof: Array:: - ) { + ) -> bool { let mut merkle_tree: MerkleTree = MerkleTreeImpl::new(); - let claimee = get_caller_address(); - - // [Verify not already claimed] - let claimed = self.check_claimed(claimee, timestamp, amount, id); - assert(!claimed, 'Already claimed'); // [Verify the proof] let amount_felt: felt252 = amount.into(); - let claimee_felt: felt252 = claimee.into(); + let from_felt: felt252 = from.into(); let timestamp_felt: felt252 = timestamp.into(); let id_felt: felt252 = id.into(); - let intermediate_hash = LegacyHash::hash(claimee_felt, amount_felt); + let intermediate_hash = LegacyHash::hash(from_felt, amount_felt); let intermediate_hash = LegacyHash::hash(intermediate_hash, timestamp_felt); let leaf = LegacyHash::hash(intermediate_hash, id_felt); let root_computed = merkle_tree.compute_root(leaf, proof.span()); let stored_root = self.Offsetter_merkle_root.read(); - assert(root_computed == stored_root, 'Invalid proof'); + if root_computed != stored_root { + assert(root_computed == stored_root, 'Invalid proof'); + return false; + } + + return true; + } + + fn confirm_offset( + ref self: ComponentState, + amount: u128, + timestamp: u128, + id: u128, + proof: Array:: + ) { + let claimee = get_caller_address(); + + // [Verify not already claimed] + let claimed = self.check_claimed(claimee, timestamp, amount, id); + assert(!claimed, 'Already claimed'); + + // [Verify if the merkle tree claim is possible] + assert( + self.confirm_for_merkle_tree(claimee, amount, timestamp, id, proof), 'Invalid proof' + ); + + //If everything is correct, we offset the carbon credits + self._offset_carbon_credit(claimee, 1, amount.into()); // [Mark as claimed] let allocation = Allocation { @@ -198,6 +221,17 @@ mod OffsetComponent { ); } + fn get_allocation_id(self: @ComponentState, from: ContractAddress) -> u256 { + self.Offsetter_allocation_id.read(from) + } + + fn get_retirement( + self: @ComponentState, token_id: u256, from: ContractAddress + ) -> u256 { + self.Offsetter_carbon_retired.read((token_id, from)) + } + + fn get_pending_retirement( self: @ComponentState, address: ContractAddress, token_id: u256 ) -> u256 { diff --git a/tests/test_merkle_tree.cairo b/tests/test_merkle_tree.cairo index 23ae33f..7b05006 100644 --- a/tests/test_merkle_tree.cairo +++ b/tests/test_merkle_tree.cairo @@ -66,33 +66,7 @@ fn test_bob_claims_single_allocation() { start_cheat_caller_address(offsetter_address, bob_address); assert_eq!(contract.get_merkle_root(), root); - assert!(!contract.check_claimed(bob_address, timestamp, amount, id)); - - start_cheat_caller_address(offsetter_address, bob_address); - contract.claim(amount, timestamp, id, proof); - - assert!(contract.check_claimed(bob_address, timestamp, amount, id)); -} - -#[test] -#[should_panic(expected: 'Already claimed')] -fn test_bob_claims_twice() { - /// Test that trying to claim the same allocation twice results in a panic. - let owner_address = contract_address_const::<'OWNER'>(); - let (root, bob_address, amount, timestamp, id, proof) = get_bob_first_wave_allocation(); - let project_address = default_setup_and_deploy(); - let offsetter_address = deploy_offsetter(project_address); - let contract = IOffsetHandlerDispatcher { contract_address: offsetter_address }; - - start_cheat_caller_address(offsetter_address, owner_address); - contract.set_merkle_root(root); - assert!(!contract.check_claimed(bob_address, timestamp, amount, id)); - - start_cheat_caller_address(offsetter_address, bob_address); - contract.claim(amount, timestamp, id, proof.clone()); - assert!(contract.check_claimed(bob_address, timestamp, amount, id)); - - contract.claim(amount, timestamp, id, proof); + assert!(contract.confirm_for_merkle_tree(bob_address, amount, timestamp, id, proof)); } #[test] @@ -109,8 +83,7 @@ fn test_claim_with_invalid_address() { let invalid_address = contract_address_const::<'DUMMY'>(); assert!(!contract.check_claimed(invalid_address, timestamp, amount, id)); - start_cheat_caller_address(offsetter_address, invalid_address); - contract.claim(amount, timestamp, id, proof); + assert!(!contract.confirm_for_merkle_tree(invalid_address, amount, timestamp, id, proof)); } #[test] @@ -127,8 +100,7 @@ fn test_claim_with_invalid_amount() { let invalid_amount = 0; assert!(!contract.check_claimed(bob_address, timestamp, invalid_amount, id)); - start_cheat_caller_address(offsetter_address, bob_address); - contract.claim(invalid_amount, timestamp, id, proof); + assert!(!contract.confirm_for_merkle_tree(bob_address, invalid_amount, timestamp, id, proof)); } #[test] @@ -145,8 +117,7 @@ fn test_claim_with_invalid_timestamp() { let invalid_timestamp = 0; assert!(!contract.check_claimed(bob_address, invalid_timestamp, amount, id)); - start_cheat_caller_address(offsetter_address, bob_address); - contract.claim(amount, invalid_timestamp, id, proof); + assert!(!contract.confirm_for_merkle_tree(bob_address, amount, invalid_timestamp, id, proof)); } #[test] @@ -163,32 +134,7 @@ fn test_claim_with_invalid_proof() { let invalid_proof: Array = array![0x123, 0x1]; assert!(!contract.check_claimed(bob_address, timestamp, amount, id)); - start_cheat_caller_address(offsetter_address, bob_address); - contract.claim(amount, timestamp, id, invalid_proof); -} - -#[test] -fn test_event_emission_on_claim() { - let (root, bob_address, amount, timestamp, id, proof) = get_bob_first_wave_allocation(); - let owner_address = contract_address_const::<'OWNER'>(); - let project_address = default_setup_and_deploy(); - let offsetter_address = deploy_offsetter(project_address); - let contract = IOffsetHandlerDispatcher { contract_address: offsetter_address }; - - start_cheat_caller_address(offsetter_address, owner_address); - contract.set_merkle_root(root); - assert!(!contract.check_claimed(bob_address, timestamp, amount, id)); - - let mut spy = spy_events(); - start_cheat_caller_address(offsetter_address, bob_address); - contract.claim(amount, timestamp, id, proof); - - let expected_event = OffsetComponent::Event::AllocationClaimed( - OffsetComponent::AllocationClaimed { claimee: bob_address, amount, timestamp, id } - ); - spy.assert_emitted(@array![(offsetter_address, expected_event)]); - - assert!(contract.check_claimed(bob_address, timestamp, amount, id)); + assert!(!contract.confirm_for_merkle_tree(bob_address, amount, timestamp, id, invalid_proof)); } #[test] @@ -208,14 +154,12 @@ fn test_claim_after_root_update() { contract.set_merkle_root(new_root); assert!(!contract.check_claimed(bob_address, timestamp, amount, id)); - start_cheat_caller_address(offsetter_address, bob_address); - contract.claim(amount, timestamp, id, new_proof); - assert!(contract.check_claimed(bob_address, timestamp, amount, id)); + assert!(contract.confirm_for_merkle_tree(bob_address, amount, timestamp, id, new_proof)); } #[test] fn test_alice_claims_in_second_wave() { - /// Test that Bob can claim his allocation from the first wave and Alice can claim her allocation from the second wave. + /// Test that Bob can confirm his allocation from the first wave and Alice can confirm her allocation from the second wave. let (root, bob_address, amount, timestamp, id, proof) = get_bob_first_wave_allocation(); let owner_address = contract_address_const::<'OWNER'>(); let project_address = default_setup_and_deploy(); @@ -226,15 +170,7 @@ fn test_alice_claims_in_second_wave() { contract.set_merkle_root(root); assert!(!contract.check_claimed(bob_address, timestamp, amount, id)); - let mut spy = spy_events(); - start_cheat_caller_address(offsetter_address, bob_address); - contract.claim(amount, timestamp, id, proof); - - let expected_event = OffsetComponent::Event::AllocationClaimed( - OffsetComponent::AllocationClaimed { claimee: bob_address, amount, timestamp, id } - ); - spy.assert_emitted(@array![(offsetter_address, expected_event)]); - assert!(contract.check_claimed(bob_address, timestamp, amount, id)); + assert!(contract.confirm_for_merkle_tree(bob_address, amount, timestamp, id, proof)); let (new_root, alice_address, amount, timestamp, id, proof) = get_alice_second_wave_allocation(); @@ -242,14 +178,12 @@ fn test_alice_claims_in_second_wave() { contract.set_merkle_root(new_root); assert!(!contract.check_claimed(alice_address, timestamp, amount, id)); - start_cheat_caller_address(offsetter_address, alice_address); - contract.claim(amount, timestamp, id, proof); - assert!(contract.check_claimed(alice_address, timestamp, amount, id)); + assert!(contract.confirm_for_merkle_tree(alice_address, amount, timestamp, id, proof)); } #[test] fn test_john_claims_multiple_allocations() { - /// Test that John can claim two of his three allocations from the first wave, and the remaining one from the second wave. + /// Test that John can confirm_for_merkle_tree two of his three allocations from the first wave, and the remaining one from the second wave. let ( root, new_root, @@ -284,18 +218,11 @@ fn test_john_claims_multiple_allocations() { assert!(!contract.check_claimed(john_address, timestamp2, amount2, id_2)); assert!(!contract.check_claimed(john_address, timestamp3, amount3, id_3)); - start_cheat_caller_address(offsetter_address, john_address); - contract.claim(amount1, timestamp1, id_1, proof1); - contract.claim(amount2, timestamp2, id_2, proof2); - assert!(contract.check_claimed(john_address, timestamp1, amount1, id_1)); - assert!(contract.check_claimed(john_address, timestamp2, amount2, id_2)); - assert!(!contract.check_claimed(john_address, timestamp3, amount3, id_3)); + assert!(contract.confirm_for_merkle_tree(john_address, amount1, timestamp1, id_1, proof1)); + assert!(contract.confirm_for_merkle_tree(john_address, amount2, timestamp2, id_2, proof2)); start_cheat_caller_address(offsetter_address, owner_address); contract.set_merkle_root(new_root); - start_cheat_caller_address(offsetter_address, john_address); - contract.claim(amount4, timestamp4, id_4, proof4); - assert!(contract.check_claimed(john_address, timestamp4, amount4, id_4)); - assert!(!contract.check_claimed(john_address, timestamp3, amount3, id_3)); + assert!(contract.confirm_for_merkle_tree(john_address, amount4, timestamp4, id_4, proof4)); } diff --git a/tests/test_offsetter.cairo b/tests/test_offsetter.cairo index 8a66891..3aac557 100644 --- a/tests/test_offsetter.cairo +++ b/tests/test_offsetter.cairo @@ -22,7 +22,7 @@ use carbon_v3::components::erc1155::interface::{IERC1155Dispatcher, IERC1155Disp use carbon_v3::components::offsetter::interface::{ IOffsetHandlerDispatcher, IOffsetHandlerDispatcherTrait }; - +use carbon_v3::components::offsetter::OffsetComponent; // Contracts use carbon_v3::contracts::project::{ @@ -57,6 +57,12 @@ struct Contracts { } +/// Utils to import mock data +use super::tests_lib::{ + MERKLE_ROOT_FIRST_WAVE, MERKLE_ROOT_SECOND_WAVE, get_bob_first_wave_allocation, + get_bob_second_wave_allocation, get_alice_second_wave_allocation, get_john_multiple_allocations +}; + // // Tests // @@ -506,3 +512,557 @@ fn test_get_carbon_retired_no_retired() { assert(carbon_retired == 0.into(), 'Error about carbon retired'); } + +/// confirm_offset + +#[test] +fn test_confirm_offset() { + /// Test a simple confirm offset scenario where Bob claims his retirement from the first wave. + let owner_address = contract_address_const::<'OWNER'>(); + let project_address = default_setup_and_deploy(); + let offsetter_address = deploy_offsetter(project_address); + + let (root, bob_address, amount, timestamp, id, proof) = get_bob_first_wave_allocation(); + + let erc20_address = deploy_erc20(); + let minter_address = deploy_minter(project_address, erc20_address); + let token_id: u256 = 1; + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, owner_address); + + let project = IProjectDispatcher { contract_address: project_address }; + project.grant_minter_role(minter_address); + project.grant_offsetter_role(offsetter_address); + stop_cheat_caller_address(project_address); + + let vintages = IVintageDispatcher { contract_address: project_address }; + let initial_total_supply = vintages.get_initial_project_cc_supply(); + let cc_to_mint = initial_total_supply / 10; // 10% of the total supply + + buy_utils(owner_address, bob_address, minter_address, cc_to_mint); + let initial_balance = project.balance_of(bob_address, token_id); + + let amount_to_offset: u256 = amount.into(); + + start_cheat_caller_address(project_address, owner_address); + vintages.update_vintage_status(token_id, CarbonVintageType::Audited.into()); + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + + let offsetter = IOffsetHandlerDispatcher { contract_address: offsetter_address }; + + start_cheat_caller_address(offsetter_address, owner_address); + offsetter.set_merkle_root(root); + + start_cheat_caller_address(project_address, bob_address); + project.set_approval_for_all(offsetter_address, true); + stop_cheat_caller_address(erc20_address); + + let mut spy = spy_events(); + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + offsetter.retire_carbon_credits(token_id, amount_to_offset); + + let expected_event = helper_expected_transfer_event( + project_address, + offsetter_address, + bob_address, + offsetter_address, + array![token_id].span(), + amount_to_offset + ); + + spy.assert_emitted(@array![(project_address, expected_event)]); + + let carbon_pending = offsetter.get_pending_retirement(bob_address, token_id); + assert(carbon_pending == amount_to_offset, 'Carbon pending is wrong'); + let final_balance = project.balance_of(bob_address, token_id); + assert(final_balance == initial_balance - amount_to_offset, 'Balance is wrong'); + + let current_retirement = offsetter.get_retirement(token_id, bob_address); + let new_retirement = current_retirement + amount.clone().into(); + + assert!(!offsetter.check_claimed(bob_address, timestamp, amount, id)); + offsetter.confirm_offset(amount, timestamp, id, proof); + assert!(offsetter.check_claimed(bob_address, timestamp, amount, id)); + + assert!(offsetter.get_retirement(token_id, bob_address) == new_retirement) +} + +#[test] +#[should_panic(expected: 'Already claimed')] +fn test_bob_confirms_twice() { + /// Test that Bob trying to confirm the same offset twice, results in a panic. + let owner_address = contract_address_const::<'OWNER'>(); + let project_address = default_setup_and_deploy(); + let offsetter_address = deploy_offsetter(project_address); + + let (root, bob_address, amount, timestamp, id, proof) = get_bob_first_wave_allocation(); + + let erc20_address = deploy_erc20(); + let minter_address = deploy_minter(project_address, erc20_address); + let token_id: u256 = 1; + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, owner_address); + + let project = IProjectDispatcher { contract_address: project_address }; + project.grant_minter_role(minter_address); + project.grant_offsetter_role(offsetter_address); + stop_cheat_caller_address(project_address); + + let vintages = IVintageDispatcher { contract_address: project_address }; + let initial_total_supply = vintages.get_initial_project_cc_supply(); + let cc_to_mint = initial_total_supply / 10; // 10% of the total supply + + buy_utils(owner_address, bob_address, minter_address, cc_to_mint); + let initial_balance = project.balance_of(bob_address, token_id); + + let amount_to_offset: u256 = amount.into(); + + start_cheat_caller_address(project_address, owner_address); + vintages.update_vintage_status(token_id, CarbonVintageType::Audited.into()); + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + + let offsetter = IOffsetHandlerDispatcher { contract_address: offsetter_address }; + + start_cheat_caller_address(offsetter_address, owner_address); + offsetter.set_merkle_root(root); + + start_cheat_caller_address(project_address, bob_address); + project.set_approval_for_all(offsetter_address, true); + stop_cheat_caller_address(erc20_address); + + let mut spy = spy_events(); + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + offsetter.retire_carbon_credits(token_id, amount_to_offset); + + let expected_event = helper_expected_transfer_event( + project_address, + offsetter_address, + bob_address, + offsetter_address, + array![token_id].span(), + amount_to_offset + ); + + spy.assert_emitted(@array![(project_address, expected_event)]); + + let carbon_pending = offsetter.get_pending_retirement(bob_address, token_id); + assert(carbon_pending == amount_to_offset, 'Carbon pending is wrong'); + let final_balance = project.balance_of(bob_address, token_id); + assert(final_balance == initial_balance - amount_to_offset, 'Balance is wrong'); + + offsetter.confirm_offset(amount, timestamp, id, proof.clone()); + assert!(offsetter.check_claimed(bob_address, timestamp, amount, id)); + + offsetter.confirm_offset(amount, timestamp, id, proof); +} + + +#[test] +fn test_events_emission_on_claim_confirmation() { + let owner_address = contract_address_const::<'OWNER'>(); + let project_address = default_setup_and_deploy(); + let offsetter_address = deploy_offsetter(project_address); + + let (root, bob_address, amount, timestamp, id, proof) = get_bob_first_wave_allocation(); + + let erc20_address = deploy_erc20(); + let minter_address = deploy_minter(project_address, erc20_address); + let token_id: u256 = 1; + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, owner_address); + + let project = IProjectDispatcher { contract_address: project_address }; + project.grant_minter_role(minter_address); + project.grant_offsetter_role(offsetter_address); + stop_cheat_caller_address(project_address); + + let vintages = IVintageDispatcher { contract_address: project_address }; + let initial_total_supply = vintages.get_initial_project_cc_supply(); + let cc_to_mint = initial_total_supply / 10; // 10% of the total supply + + buy_utils(owner_address, bob_address, minter_address, cc_to_mint); + let initial_balance = project.balance_of(bob_address, token_id); + + let amount_to_offset: u256 = amount.into(); + + start_cheat_caller_address(project_address, owner_address); + vintages.update_vintage_status(token_id, CarbonVintageType::Audited.into()); + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + + let offsetter = IOffsetHandlerDispatcher { contract_address: offsetter_address }; + + start_cheat_caller_address(offsetter_address, owner_address); + offsetter.set_merkle_root(root); + + start_cheat_caller_address(project_address, bob_address); + project.set_approval_for_all(offsetter_address, true); + stop_cheat_caller_address(erc20_address); + + let mut spy = spy_events(); + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + offsetter.retire_carbon_credits(token_id, amount_to_offset); + + let expected_event = helper_expected_transfer_event( + project_address, + offsetter_address, + bob_address, + offsetter_address, + array![token_id].span(), + amount_to_offset + ); + + spy.assert_emitted(@array![(project_address, expected_event)]); + + let carbon_pending = offsetter.get_pending_retirement(bob_address, token_id); + assert(carbon_pending == amount_to_offset, 'Carbon pending is wrong'); + let final_balance = project.balance_of(bob_address, token_id); + assert(final_balance == initial_balance - amount_to_offset, 'Balance is wrong'); + + let current_retirement = offsetter.get_retirement(token_id, bob_address); + let new_retirement = current_retirement + amount.clone().into(); + + let mut spy = spy_events(); + offsetter.confirm_offset(amount, timestamp, id, proof); + + let first_expected_event = OffsetComponent::Event::Retired( + OffsetComponent::Retired { + from: bob_address, + project: project_address, + token_id: token_id, + old_amount: current_retirement, + new_amount: new_retirement + } + ); + + let second_expected_event = OffsetComponent::Event::AllocationClaimed( + OffsetComponent::AllocationClaimed { claimee: bob_address, amount, timestamp, id } + ); + + spy + .assert_emitted( + @array![ + (offsetter_address, first_expected_event), + (offsetter_address, second_expected_event) + ] + ); + + assert!(offsetter.check_claimed(bob_address, timestamp, amount, id)); +} + + +#[test] +#[should_panic(expected: 'Invalid proof')] +fn test_claim_confirmation_with_invalid_amount() { + let owner_address = contract_address_const::<'OWNER'>(); + let project_address = default_setup_and_deploy(); + let offsetter_address = deploy_offsetter(project_address); + + let (root, bob_address, amount, timestamp, id, proof) = get_bob_first_wave_allocation(); + + let erc20_address = deploy_erc20(); + let minter_address = deploy_minter(project_address, erc20_address); + let token_id: u256 = 1; + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, owner_address); + + let project = IProjectDispatcher { contract_address: project_address }; + project.grant_minter_role(minter_address); + project.grant_offsetter_role(offsetter_address); + stop_cheat_caller_address(project_address); + + let vintages = IVintageDispatcher { contract_address: project_address }; + let initial_total_supply = vintages.get_initial_project_cc_supply(); + let cc_to_mint = initial_total_supply / 10; // 10% of the total supply + + buy_utils(owner_address, bob_address, minter_address, cc_to_mint); + let initial_balance = project.balance_of(bob_address, token_id); + + let amount_to_offset: u256 = amount.into(); + + start_cheat_caller_address(project_address, owner_address); + vintages.update_vintage_status(token_id, CarbonVintageType::Audited.into()); + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + + let offsetter = IOffsetHandlerDispatcher { contract_address: offsetter_address }; + + start_cheat_caller_address(offsetter_address, owner_address); + offsetter.set_merkle_root(root); + + start_cheat_caller_address(project_address, bob_address); + project.set_approval_for_all(offsetter_address, true); + stop_cheat_caller_address(erc20_address); + + let mut spy = spy_events(); + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + offsetter.retire_carbon_credits(token_id, amount_to_offset); + + let expected_event = helper_expected_transfer_event( + project_address, + offsetter_address, + bob_address, + offsetter_address, + array![token_id].span(), + amount_to_offset + ); + + spy.assert_emitted(@array![(project_address, expected_event)]); + + let carbon_pending = offsetter.get_pending_retirement(bob_address, token_id); + assert(carbon_pending == amount_to_offset, 'Carbon pending is wrong'); + let final_balance = project.balance_of(bob_address, token_id); + assert(final_balance == initial_balance - amount_to_offset, 'Balance is wrong'); + + let invalid_amount = 0; + + offsetter.confirm_offset(invalid_amount, timestamp, id, proof); +} + +#[test] +fn test_alice_confirms_in_second_wave() { + /// Test that Bob can confirm his offset from the first wave and Alice can confirm her offset from the second wave. + let owner_address = contract_address_const::<'OWNER'>(); + let project_address = default_setup_and_deploy(); + let offsetter_address = deploy_offsetter(project_address); + + let (root, bob_address, amount, timestamp, id, proof) = get_bob_first_wave_allocation(); + + let erc20_address = deploy_erc20(); + let minter_address = deploy_minter(project_address, erc20_address); + let token_id: u256 = 1; + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, owner_address); + + let project = IProjectDispatcher { contract_address: project_address }; + project.grant_minter_role(minter_address); + project.grant_offsetter_role(offsetter_address); + stop_cheat_caller_address(project_address); + + let vintages = IVintageDispatcher { contract_address: project_address }; + let initial_total_supply = vintages.get_initial_project_cc_supply(); + let cc_to_mint = initial_total_supply / 10; // 10% of the total supply + + buy_utils(owner_address, bob_address, minter_address, cc_to_mint); + let initial_balance = project.balance_of(bob_address, token_id); + + let amount_to_offset: u256 = amount.into(); + + start_cheat_caller_address(project_address, owner_address); + vintages.update_vintage_status(token_id, CarbonVintageType::Audited.into()); + + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + + let offsetter = IOffsetHandlerDispatcher { contract_address: offsetter_address }; + + start_cheat_caller_address(offsetter_address, owner_address); + offsetter.set_merkle_root(root); + + start_cheat_caller_address(project_address, bob_address); + project.set_approval_for_all(offsetter_address, true); + stop_cheat_caller_address(erc20_address); + + let mut spy = spy_events(); + start_cheat_caller_address(offsetter_address, bob_address); + start_cheat_caller_address(project_address, offsetter_address); + offsetter.retire_carbon_credits(token_id, amount_to_offset); + + let expected_event = helper_expected_transfer_event( + project_address, + offsetter_address, + bob_address, + offsetter_address, + array![token_id].span(), + amount_to_offset + ); + + spy.assert_emitted(@array![(project_address, expected_event)]); + + let carbon_pending = offsetter.get_pending_retirement(bob_address, token_id); + assert(carbon_pending == amount_to_offset, 'Carbon pending is wrong'); + let final_balance = project.balance_of(bob_address, token_id); + assert(final_balance == initial_balance - amount_to_offset, 'Balance is wrong'); + + let current_retirement = offsetter.get_retirement(token_id, bob_address); + let new_retirement = current_retirement + amount.clone().into(); + + assert!(!offsetter.check_claimed(bob_address, timestamp, amount, id)); + offsetter.confirm_offset(amount, timestamp, id, proof); + assert!(offsetter.check_claimed(bob_address, timestamp, amount, id)); + + assert!(offsetter.get_retirement(token_id, bob_address) == new_retirement); + + stop_cheat_caller_address(erc20_address); + stop_cheat_caller_address(project_address); + + let (new_root, alice_address, amount, timestamp, id, proof) = + get_alice_second_wave_allocation(); + + start_cheat_caller_address(offsetter_address, alice_address); + start_cheat_caller_address(project_address, owner_address); + + project.grant_minter_role(minter_address); + project.grant_offsetter_role(offsetter_address); + stop_cheat_caller_address(project_address); + + let vintages = IVintageDispatcher { contract_address: project_address }; + let initial_total_supply = vintages.get_initial_project_cc_supply(); + let cc_to_mint = initial_total_supply / 10; // 10% of the total supply + + buy_utils(owner_address, alice_address, minter_address, cc_to_mint); + let initial_balance = project.balance_of(alice_address, token_id); + + let amount_to_offset: u256 = amount.into(); + + start_cheat_caller_address(offsetter_address, alice_address); + start_cheat_caller_address(project_address, offsetter_address); + + let offsetter = IOffsetHandlerDispatcher { contract_address: offsetter_address }; + + start_cheat_caller_address(offsetter_address, owner_address); + offsetter.set_merkle_root(new_root); + + start_cheat_caller_address(project_address, alice_address); + project.set_approval_for_all(offsetter_address, true); + stop_cheat_caller_address(erc20_address); + + let mut spy = spy_events(); + start_cheat_caller_address(offsetter_address, alice_address); + start_cheat_caller_address(project_address, offsetter_address); + offsetter.retire_carbon_credits(token_id, amount_to_offset); + + let expected_event = helper_expected_transfer_event( + project_address, + offsetter_address, + alice_address, + offsetter_address, + array![token_id].span(), + amount_to_offset + ); + + spy.assert_emitted(@array![(project_address, expected_event)]); + + let carbon_pending = offsetter.get_pending_retirement(alice_address, token_id); + assert(carbon_pending == amount_to_offset, 'Carbon pending is wrong'); + let final_balance = project.balance_of(alice_address, token_id); + assert(final_balance == initial_balance - amount_to_offset, 'Balance is wrong'); + + let current_retirement = offsetter.get_retirement(token_id, alice_address); + let new_retirement = current_retirement + amount.clone().into(); + + assert!(!offsetter.check_claimed(alice_address, timestamp, amount, id)); + offsetter.confirm_offset(amount, timestamp, id, proof); + assert!(offsetter.check_claimed(alice_address, timestamp, amount, id)); + + assert!(offsetter.get_retirement(token_id, alice_address) == new_retirement); +} + +#[test] +fn test_john_confirms_multiple_allocations() { + /// Test that John can two of his three offset from the first allocations wave, and the remaining one from the second wave. + let owner_address = contract_address_const::<'OWNER'>(); + let project_address = default_setup_and_deploy(); + let offsetter_address = deploy_offsetter(project_address); + + let ( + root, + new_root, + john_address, + amount1, + timestamp1, + id_1, + amount2, + timestamp2, + id_2, + _, + _, + _, + amount4, + timestamp4, + id_4, + proof1, + proof2, + _, + proof4 + ) = + get_john_multiple_allocations(); + + let erc20_address = deploy_erc20(); + let minter_address = deploy_minter(project_address, erc20_address); + let token_id: u256 = 1; + + start_cheat_caller_address(offsetter_address, john_address); + start_cheat_caller_address(project_address, owner_address); + + let project = IProjectDispatcher { contract_address: project_address }; + project.grant_minter_role(minter_address); + project.grant_offsetter_role(offsetter_address); + stop_cheat_caller_address(project_address); + + let vintages = IVintageDispatcher { contract_address: project_address }; + let initial_total_supply = vintages.get_initial_project_cc_supply(); + let cc_to_mint = initial_total_supply / 10; // 10% of the total supply + + buy_utils(owner_address, john_address, minter_address, cc_to_mint); + + let amount1_to_offset: u256 = amount1.into(); + let amount2_to_offset: u256 = amount2.into(); + let amount4_to_offset: u256 = amount4.into(); + + start_cheat_caller_address(project_address, owner_address); + vintages.update_vintage_status(token_id, CarbonVintageType::Audited.into()); + + start_cheat_caller_address(offsetter_address, john_address); + start_cheat_caller_address(project_address, offsetter_address); + + let offsetter = IOffsetHandlerDispatcher { contract_address: offsetter_address }; + + start_cheat_caller_address(offsetter_address, owner_address); + offsetter.set_merkle_root(root); + + start_cheat_caller_address(project_address, john_address); + project.set_approval_for_all(offsetter_address, true); + stop_cheat_caller_address(erc20_address); + + start_cheat_caller_address(offsetter_address, john_address); + start_cheat_caller_address(project_address, offsetter_address); + + offsetter.retire_carbon_credits(token_id, amount1_to_offset); + assert!(!offsetter.check_claimed(john_address, timestamp1, amount1, id_1)); + offsetter.confirm_offset(amount1, timestamp1, id_1, proof1); + + offsetter.retire_carbon_credits(token_id, amount2_to_offset); + assert!(!offsetter.check_claimed(john_address, timestamp2, amount2, id_2)); + offsetter.confirm_offset(amount2, timestamp2, id_2, proof2); + + assert!(offsetter.check_claimed(john_address, timestamp1, amount1, id_1)); + assert!(offsetter.check_claimed(john_address, timestamp2, amount2, id_2)); + + start_cheat_caller_address(offsetter_address, owner_address); + offsetter.set_merkle_root(new_root); + + start_cheat_caller_address(offsetter_address, john_address); + start_cheat_caller_address(project_address, offsetter_address); + + offsetter.retire_carbon_credits(token_id, amount4_to_offset); + assert!(!offsetter.check_claimed(john_address, timestamp4, amount4, id_4)); + offsetter.confirm_offset(amount4, timestamp4, id_4, proof4); + + assert!(offsetter.check_claimed(john_address, timestamp4, amount4, id_4)); +}