From 3f3189250b0948b5877cc9e84ed60e39af04c917 Mon Sep 17 00:00:00 2001 From: Arn0d Date: Mon, 6 May 2024 10:47:07 +0200 Subject: [PATCH] Adding Burner and setting Status for Carbon Vintage (#47) * impl test for minter and add mock usdcarb module for testing * add ERC1155 impl and add minter tests * Create component BurnHandler and create contract Burner * set vintage status and adding tests cases * refactor: Remove unused functions from project contract --- src/components/absorber/carbon_handler.cairo | 48 ++- src/components/absorber/interface.cairo | 5 +- src/components/burner.cairo | 4 + src/components/burner/burn_handler.cairo | 233 +++++++++++++ src/components/burner/interface.cairo | 22 ++ src/components/data.cairo | 2 - src/components/data/carbon_project.cairo | 21 -- src/components/minter/mint.cairo | 3 +- src/contracts/burner.cairo | 60 ++++ src/contracts/project.cairo | 49 +-- .../data/carbon_vintage.cairo | 11 + src/lib.cairo | 7 +- tests/lib.cairo | 1 + tests/test_burner.cairo | 313 ++++++++++++++++++ tests/test_carbon_handler.cairo | 2 +- tests/test_mint.cairo | 2 +- tests/test_project.cairo | 82 +++-- 17 files changed, 771 insertions(+), 94 deletions(-) create mode 100644 src/components/burner.cairo create mode 100644 src/components/burner/burn_handler.cairo create mode 100644 src/components/burner/interface.cairo delete mode 100644 src/components/data.cairo delete mode 100644 src/components/data/carbon_project.cairo create mode 100644 src/contracts/burner.cairo rename src/{components => }/data/carbon_vintage.cairo (78%) create mode 100644 tests/test_burner.cairo diff --git a/src/components/absorber/carbon_handler.cairo b/src/components/absorber/carbon_handler.cairo index e5509c0..4360fe3 100644 --- a/src/components/absorber/carbon_handler.cairo +++ b/src/components/absorber/carbon_handler.cairo @@ -12,9 +12,8 @@ mod AbsorberComponent { use alexandria_storage::list::{List, ListTrait}; // Internal imports - use carbon_v3::components::absorber::interface::IAbsorber; - use carbon_v3::components::absorber::interface::ICarbonCreditsHandler; - use carbon_v3::components::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; + use carbon_v3::components::absorber::interface::{IAbsorber, ICarbonCreditsHandler}; + use carbon_v3::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; // Constants @@ -51,6 +50,7 @@ mod AbsorberComponent { value: u256 } + mod Errors { const INVALID_ARRAY_LENGTH: felt252 = 'Absorber: invalid array length'; const INVALID_STARTING_YEAR: felt252 = 'Absorber: invalid starting year'; @@ -281,6 +281,36 @@ mod AbsorberComponent { fn get_cc_decimals(self: @ComponentState) -> u8 { CC_DECIMALS } + + fn update_vintage_status( + ref self: ComponentState, year: u64, status: felt252 + ) { + let mut carbon_vintages: List = self.Absorber_vintage_cc.read(); + let mut index = 0; + + loop { + if index == carbon_vintages.len() { + break (); + } + + let mut tmp_vintage: CarbonVintage = self.Absorber_vintage_cc.read()[index]; + if tmp_vintage.cc_vintage == year.into() { + let new_status: CarbonVintageType = match status { + 0 => CarbonVintageType::Projected, + 1 => CarbonVintageType::Confirmed, + 2 => CarbonVintageType::Audited, + 3 => CarbonVintageType::Retired, + _ => CarbonVintageType::Projected, + }; + + tmp_vintage.cc_status = new_status; + let _ = carbon_vintages.set(index, tmp_vintage); + } + index += 1; + }; + + self.Absorber_vintage_cc.write(carbon_vintages); + } } #[generate_trait] @@ -364,6 +394,18 @@ mod AbsorberComponent { }; array.span() } + + fn __felt252_into_CarbonVintageType( + self: @ComponentState, status: felt252 + ) -> CarbonVintageType { + match status { + 0 => CarbonVintageType::Projected, + 1 => CarbonVintageType::Confirmed, + 2 => CarbonVintageType::Audited, + 3 => CarbonVintageType::Retired, + _ => CarbonVintageType::Projected, + } + } } } diff --git a/src/components/absorber/interface.cairo b/src/components/absorber/interface.cairo index 0e8b4de..5288640 100644 --- a/src/components/absorber/interface.cairo +++ b/src/components/absorber/interface.cairo @@ -1,5 +1,5 @@ use starknet::ContractAddress; -use carbon_v3::components::data::carbon_vintage::{CarbonVintage}; +use carbon_v3::data::carbon_vintage::{CarbonVintage}; #[starknet::interface] trait IAbsorber { @@ -54,4 +54,7 @@ trait ICarbonCreditsHandler { // Get number of decimal for total supply to have a carbon credit fn get_cc_decimals(self: @TContractState) -> u8; + + // Update the vintage status + fn update_vintage_status(ref self: TContractState, year: u64, status: felt252); } diff --git a/src/components/burner.cairo b/src/components/burner.cairo new file mode 100644 index 0000000..1174542 --- /dev/null +++ b/src/components/burner.cairo @@ -0,0 +1,4 @@ +mod burn_handler; +mod interface; + +use burn_handler::BurnComponent; diff --git a/src/components/burner/burn_handler.cairo b/src/components/burner/burn_handler.cairo new file mode 100644 index 0000000..d4bad17 --- /dev/null +++ b/src/components/burner/burn_handler.cairo @@ -0,0 +1,233 @@ +#[starknet::component] +mod BurnComponent { + // Core imports + + use core::clone::Clone; + use core::array::SpanTrait; + use zeroable::Zeroable; + use traits::{Into, TryInto}; + use option::OptionTrait; + use array::{Array, ArrayTrait}; + use hash::HashStateTrait; + use poseidon::PoseidonTrait; + + // Starknet imports + + use starknet::ContractAddress; + use starknet::{get_caller_address, get_contract_address, get_block_timestamp}; + + // Internal imports + + use carbon_v3::components::burner::interface::IBurnHandler; + use carbon_v3::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; + use carbon_v3::components::absorber::interface::{IAbsorberDispatcher, IAbsorberDispatcherTrait}; + use carbon_v3::components::absorber::interface::{ + ICarbonCreditsHandlerDispatcher, ICarbonCreditsHandlerDispatcherTrait + }; + use carbon_v3::components::erc1155::interface::{IERC1155Dispatcher, IERC1155DispatcherTrait}; + use carbon_v3::contracts::project::{ + IExternalDispatcher as IProjectDispatcher, + IExternalDispatcherTrait as IProjectDispatcherTrait + }; + + // Constants + + const MULT_ACCURATE_SHARE: u256 = 1_000_000; + + #[storage] + struct Storage { + Burn_carbonable_project_address: ContractAddress, + Burn_carbon_pending_retirement: LegacyMap<(u256, ContractAddress), u256>, + Burn_carbon_retired: LegacyMap<(u256, ContractAddress), u256>, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + RequestedRetirement: RequestedRetirement, + Retired: Retired, + } + + #[derive(Drop, starknet::Event)] + struct RequestedRetirement { + #[key] + from: ContractAddress, + #[key] + project: ContractAddress, + #[key] + vintage: u256, + amount: u256, + } + + #[derive(Drop, starknet::Event)] + struct Retired { + #[key] + from: ContractAddress, + #[key] + project: ContractAddress, + #[key] + vintage: u256, + amount: u256, + } + + mod Errors { + const INVALID_VINTAGE_STATUS: felt252 = 'vintage status is not audited'; + } + + #[embeddable_as(BurnHandlerImpl)] + impl BurnHandler< + TContractState, +HasComponent, +Drop + > of IBurnHandler> { + fn retire_carbon_credits( + ref self: ComponentState, vintage: u256, carbon_values: u256 + ) { + // [Setup] Setup variable and contract interaction + let caller_address: ContractAddress = get_caller_address(); + let project_address: ContractAddress = self.Burn_carbonable_project_address.read(); + + // [Check] Vintage have the right status + let carbon_credits = ICarbonCreditsHandlerDispatcher { + contract_address: project_address + }; + let stored_vintage: CarbonVintage = carbon_credits + .get_specific_carbon_vintage(vintage.try_into().expect('Invalid vintage year')); + assert( + stored_vintage.cc_status == CarbonVintageType::Audited, + 'Vintage status is not audited' + ); + + // [Check] caller owns the carbon credits for the vintage + let erc1155 = IERC1155Dispatcher { contract_address: project_address }; + let caller_balance = erc1155.balance_of(caller_address, vintage); + assert(caller_balance >= carbon_values, 'Not own enough carbon credits'); + + // [Effect] Add pending retirement + self._add_pending_retirement(caller_address, vintage, carbon_values); + + // [Effect] Burn carbon credits + self._burn_carbon_credit(caller_address, vintage, carbon_values); + } + + fn retire_list_carbon_credits( + ref self: ComponentState, + vintages: Span, + carbon_values: Span + ) { + // [Check] vintages and carbon values are defined + assert(vintages.len() > 0, 'Inputs cannot be empty'); + assert(vintages.len() == carbon_values.len(), 'Vintages and Values mismatch'); + + let mut index: u32 = 0; + loop { + // [Check] Vintage is defined + let vintage = match vintages.get(index) { + Option::Some(value) => *value.unbox(), + Option::None => 0, + }; + let carbon_amount = match carbon_values.get(index) { + Option::Some(value) => *value.unbox(), + Option::None => 0, + }; + + if vintage != 0 && carbon_amount != 0 { + self.retire_carbon_credits(vintage, carbon_amount); + } + + index += 1; + if index == vintages.len() { + break; + } + }; + } + + fn get_pending_retirement(ref self: ComponentState, vintage: u256) -> u256 { + let caller_address: ContractAddress = get_caller_address(); + self.Burn_carbon_pending_retirement.read((vintage, caller_address)) + } + + fn get_carbon_retired(ref self: ComponentState, vintage: u256) -> u256 { + let caller_address: ContractAddress = get_caller_address(); + self.Burn_carbon_retired.read((vintage, caller_address)) + } + } + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent, +Drop + > of InternalTrait { + fn initializer( + ref self: ComponentState, carbonable_project_address: ContractAddress + ) { + // [Effect] Update storage + self.Burn_carbonable_project_address.write(carbonable_project_address); + } + + fn _add_pending_retirement( + ref self: ComponentState, + from: ContractAddress, + vintage: u256, + amount: u256 + ) { + let current_pending_retirement = self + .Burn_carbon_pending_retirement + .read((vintage, from)); + let new_pending_retirement = current_pending_retirement + amount; + self.Burn_carbon_pending_retirement.write((vintage, from), new_pending_retirement); + + // [Event] Emit event + self + .emit( + RequestedRetirement { + from: from, + project: self.Burn_carbonable_project_address.read(), + vintage: vintage, + amount: amount + } + ); + } + + fn _remove_pending_retirement( + ref self: ComponentState, + from: ContractAddress, + vintage: u256, + amount: u256 + ) { + let current_pending_retirement = self + .Burn_carbon_pending_retirement + .read((vintage, from)); + assert(current_pending_retirement >= amount, 'Not enough pending retirement'); + let new_pending_retirement = current_pending_retirement - amount; + self.Burn_carbon_pending_retirement.write((vintage, from), new_pending_retirement); + } + + fn _burn_carbon_credit( + ref self: ComponentState, + from: ContractAddress, + vintage: u256, + amount: u256 + ) { + // [Effect] Remove pending retirement + self._remove_pending_retirement(from, vintage, amount); + + // [Effect] Update storage + let project = IProjectDispatcher { + contract_address: self.Burn_carbonable_project_address.read() + }; + project.burn(from, vintage, amount); + let current_retirement = self.Burn_carbon_retired.read((vintage, from)); + let new_retirement = current_retirement + amount; + self.Burn_carbon_retired.write((vintage, from), new_retirement); + + // [Event] Emit event + self + .emit( + Retired { + from: from, + project: self.Burn_carbonable_project_address.read(), + vintage: vintage, + amount: amount + } + ); + } + } +} diff --git a/src/components/burner/interface.cairo b/src/components/burner/interface.cairo new file mode 100644 index 0000000..f2ba067 --- /dev/null +++ b/src/components/burner/interface.cairo @@ -0,0 +1,22 @@ +use starknet::ContractAddress; +use carbon_v3::data::carbon_vintage::{CarbonVintage}; + +#[starknet::interface] +trait IBurnHandler { + /// Retire carbon credits from one vintage of carbon credits. + fn retire_carbon_credits(ref self: TContractState, vintage: u256, carbon_values: u256); + + /// Retire carbon credits from the list of carbon credits. + /// Behaviour is : + /// - If one of the carbon values is not enough or vintage status is not righ, + /// the function will fail and no carbon will be retired and the function will revert. + fn retire_list_carbon_credits( + ref self: TContractState, vintages: Span, carbon_values: Span + ); + + /// Get the pending retirement of a vintage for the caller address. + fn get_pending_retirement(ref self: TContractState, vintage: u256) -> u256; + + /// Get the carbon retirement of a vintage for the caller address. + fn get_carbon_retired(ref self: TContractState, vintage: u256) -> u256; +} diff --git a/src/components/data.cairo b/src/components/data.cairo deleted file mode 100644 index 9bc2e0a..0000000 --- a/src/components/data.cairo +++ /dev/null @@ -1,2 +0,0 @@ -mod carbon_vintage; -mod carbon_project; diff --git a/src/components/data/carbon_project.cairo b/src/components/data/carbon_project.cairo deleted file mode 100644 index 1dd10ed..0000000 --- a/src/components/data/carbon_project.cairo +++ /dev/null @@ -1,21 +0,0 @@ -/// Struct for orders. -#[derive(Copy, Drop, Debug, starknet::Store, Serde, PartialEq)] -struct CarbonProject { - project_name: felt252, - project_carbon_value: u256, - project_serial_number_metadata: felt252, - project_serial_number_CC_block: felt252, - project_total_retired: felt252, -} - -impl DefaultCarbonProject of Default { - fn default() -> CarbonProject { - CarbonProject { - project_name: 0, - project_carbon_value: 0, - project_serial_number_metadata: 0, - project_serial_number_CC_block: 0, - project_total_retired: 0, - } - } -} diff --git a/src/components/minter/mint.cairo b/src/components/minter/mint.cairo index ede7604..395010b 100644 --- a/src/components/minter/mint.cairo +++ b/src/components/minter/mint.cairo @@ -30,7 +30,7 @@ mod MintComponent { IExternalDispatcher as IProjectDispatcher, IExternalDispatcherTrait as IProjectDispatcherTrait }; - use carbon_v3::components::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; + use carbon_v3::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; // Constants @@ -269,7 +269,6 @@ mod MintComponent { self.Mint_remaining_money_amount.write(remaining_money_amount - money_amount); // [Interaction] Mint - // Implement Span to return the list of cc_vintage (token_id & year) let project = IProjectDispatcher { contract_address: project_address }; project.batch_mint(caller_address, cc_years_vintages, cc_distribution); diff --git a/src/contracts/burner.cairo b/src/contracts/burner.cairo new file mode 100644 index 0000000..ff9ebcf --- /dev/null +++ b/src/contracts/burner.cairo @@ -0,0 +1,60 @@ +use starknet::ContractAddress; + +#[starknet::contract] +mod Burner { + use starknet::{get_caller_address, ContractAddress, ClassHash}; + + // Ownable + use openzeppelin::access::ownable::OwnableComponent; + // Upgradable + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + // Burner + use carbon_v3::components::burner::burn_handler::BurnComponent; + + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: BurnComponent, storage: burner, event: BurnEvent); + + // ABI + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + #[abi(embed_v0)] + impl OwnableCamelOnlyImpl = + OwnableComponent::OwnableCamelOnlyImpl; + #[abi(embed_v0)] + impl MintImpl = BurnComponent::BurnHandlerImpl; + + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + impl MintInternalImpl = BurnComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + #[substorage(v0)] + burner: BurnComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + BurnEvent: BurnComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, carbonable_project_address: ContractAddress, owner: ContractAddress + ) { + self.ownable.initializer(owner); + self.burner.initializer(carbonable_project_address); + } +} diff --git a/src/contracts/project.cairo b/src/contracts/project.cairo index 9324fb2..03cf2f7 100644 --- a/src/contracts/project.cairo +++ b/src/contracts/project.cairo @@ -3,15 +3,15 @@ use starknet::ContractAddress; #[starknet::interface] trait IExternal { fn mint(ref self: ContractState, to: ContractAddress, token_id: u256, value: u256); - fn burn(ref self: ContractState, token_id: u256, value: u256); + fn burn(ref self: ContractState, from: ContractAddress, token_id: u256, value: u256); fn batch_mint( ref self: ContractState, to: ContractAddress, token_ids: Span, values: Span ); - fn batch_burn(ref self: ContractState, token_ids: Span, values: Span); + fn batch_burn( + ref self: ContractState, from: ContractAddress, token_ids: Span, values: Span + ); fn set_uri(ref self: ContractState, uri: ByteArray); fn decimals(self: @ContractState) -> u8; - fn balance(self: @ContractState, account: ContractAddress, token_id: u256) -> u256; - fn only_owner(self: @ContractState); } @@ -128,18 +128,27 @@ mod Project { self.erc1155.mint(to, token_id, value); } - fn burn(ref self: ContractState, token_id: u256, value: u256) { - self.erc1155.burn(get_caller_address(), token_id, value); + fn burn(ref self: ContractState, from: ContractAddress, token_id: u256, value: u256) { + self.erc1155.burn(from, token_id, value); } fn batch_mint( ref self: ContractState, to: ContractAddress, token_ids: Span, values: Span ) { + // TODO : Add access control as only the Minter in the list should be able to mint the tokens + // TODO : Check the avalibility of the ampount of vintage cc_supply for each values.it should be done in the absorber/carbon_handler self.erc1155.batch_mint(to, token_ids, values); } - fn batch_burn(ref self: ContractState, token_ids: Span, values: Span) { - self.erc1155.batch_burn(get_caller_address(), token_ids, values); + fn batch_burn( + ref self: ContractState, + from: ContractAddress, + token_ids: Span, + values: Span + ) { + // TODO : Check that the caller is the owner of the value he wnt to burn + // TODO : Add access control as only the Burner in the list should be able to burn the values + self.erc1155.batch_burn(from, token_ids, values); } fn set_uri(ref self: ContractState, uri: ByteArray) { @@ -149,29 +158,5 @@ mod Project { fn decimals(self: @ContractState) -> u8 { 6 } - - fn balance(self: @ContractState, account: ContractAddress, token_id: u256) -> u256 { - self.erc1155.balance_of(account, token_id) - } - - fn only_owner(self: @ContractState) { - self.ownable.assert_only_owner() - } - // fn set_list_uri( - // ref self: ContractState, mut token_ids: Span, mut uris: Span - // ) { - // assert(token_ids.len() == uris.len(), Errors::UNEQUAL_ARRAYS_URI); - // - // loop { - // if token_ids.len() == 0 { - // break; - // } - // let id = *token_ids.pop_front().unwrap(); - // let uri = *uris.pop_front().unwrap(); - // - // self.erc1155._set_uri(id, uri); - // } - // } - } } diff --git a/src/components/data/carbon_vintage.cairo b/src/data/carbon_vintage.cairo similarity index 78% rename from src/components/data/carbon_vintage.cairo rename to src/data/carbon_vintage.cairo index d60afec..c979911 100644 --- a/src/components/data/carbon_vintage.cairo +++ b/src/data/carbon_vintage.cairo @@ -33,3 +33,14 @@ enum CarbonVintageType { /// Retired: the Carbon Credit is retired in the certifier registry. Retired, } + +impl CarbonVintageTypeInto of Into { + fn into(self: CarbonVintageType) -> felt252 { + match self { + CarbonVintageType::Projected => 0, + CarbonVintageType::Confirmed => 1, + CarbonVintageType::Audited => 2, + CarbonVintageType::Retired => 3, + } + } +} diff --git a/src/lib.cairo b/src/lib.cairo index 7604dc6..8784bd6 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,13 +1,18 @@ mod components { mod absorber; mod minter; - mod data; mod erc1155; + mod burner; +} + +mod data { + mod carbon_vintage; } mod contracts { mod project; mod minter; + mod burner; } mod mock { diff --git a/tests/lib.cairo b/tests/lib.cairo index 742fc19..ab377fa 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -1,3 +1,4 @@ mod test_project; mod test_carbon_handler; mod test_mint; +mod test_burner; diff --git a/tests/test_burner.cairo b/tests/test_burner.cairo new file mode 100644 index 0000000..f527edc --- /dev/null +++ b/tests/test_burner.cairo @@ -0,0 +1,313 @@ +use core::array::SpanTrait; +// Core deps + +use array::ArrayTrait; +use result::ResultTrait; +use option::OptionTrait; +use traits::{Into, TryInto}; +use zeroable::Zeroable; +use debug::PrintTrait; +use hash::HashStateTrait; +use pedersen::PedersenTrait; + +// Starknet deps + +use starknet::{ContractAddress, contract_address_const}; +use starknet::{deploy_syscall, get_block_timestamp}; +use starknet::testing::{set_caller_address, set_contract_address}; + +// External deps + +use openzeppelin::tests::utils::constants as c; +use openzeppelin::utils::serde::SerializedAppend; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + +use snforge_std as snf; +use snforge_std::{ + CheatTarget, ContractClassTrait, test_address, spy_events, EventSpy, SpyOn, EventAssertions, + start_warp, start_prank, stop_prank +}; +use alexandria_storage::list::{List, ListTrait}; + +// Components + +use carbon_v3::components::absorber::interface::{ + IAbsorberDispatcher, IAbsorberDispatcherTrait, ICarbonCreditsHandlerDispatcher, + ICarbonCreditsHandlerDispatcherTrait +}; +use carbon_v3::components::absorber::carbon_handler::AbsorberComponent::{ + Event, AbsorptionUpdate, ProjectValueUpdate +}; +use carbon_v3::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; +use carbon_v3::components::absorber::carbon_handler::AbsorberComponent; + +use carbon_v3::components::erc1155::interface::{IERC1155Dispatcher, IERC1155DispatcherTrait}; + +use carbon_v3::components::burner::interface::{IBurnHandlerDispatcher, IBurnHandlerDispatcherTrait}; + +// Contracts + +use carbon_v3::contracts::project::{ + Project, IExternalDispatcher as IProjectDispatcher, + IExternalDispatcherTrait as IProjectDispatcherTrait +}; + +use carbon_v3::contracts::minter::Minter; + +use carbon_v3::mock::usdcarb::USDCarb; + +// Constants + +const PROJECT_CARBON: u256 = 42; + +// Signers + +#[derive(Drop)] +struct Signers { + owner: ContractAddress, + anyone: ContractAddress, +} + +#[derive(Drop)] +struct Contracts { + project: ContractAddress, + offseter: ContractAddress, +} + +// +// Setup +// + +/// Deploys a project contract. +fn deploy_project(owner: ContractAddress) -> (ContractAddress, EventSpy) { + let contract = snf::declare('Project'); + let uri = 'uri'; + let starting_year: u64 = 2024; + let number_of_years: u64 = 20; + let mut calldata: Array = array![]; + calldata.append(uri); + calldata.append(owner.into()); + calldata.append(starting_year.into()); + calldata.append(number_of_years.into()); + let contract_address = contract.deploy(@calldata).unwrap(); + + let mut spy = snf::spy_events(SpyOn::One(contract_address)); + + (contract_address, spy) +} + +/// Deploys a minter contract. +fn deploy_burner( + owner: ContractAddress, project_address: ContractAddress +) -> (ContractAddress, EventSpy) { + let contract = snf::declare('Burner'); + let mut calldata: Array = array![]; + calldata.append(project_address.into()); + calldata.append(owner.into()); + + let contract_address = contract.deploy(@calldata).unwrap(); + + let mut spy = snf::spy_events(SpyOn::One(contract_address)); + + (contract_address, spy) +} + +/// Sets up the project contract. +fn setup_project( + contract_address: ContractAddress, + project_carbon: u256, + times: Span, + absorptions: Span +) { + let project = IAbsorberDispatcher { contract_address }; + + project.set_absorptions(times, absorptions); + project.set_project_carbon(project_carbon); +} + +fn default_setup(owner: ContractAddress) -> (ContractAddress, EventSpy) { + let (project_address, spy) = deploy_project(owner); + + let times: Span = array![ + 1674579600, + 1706115600, + 1737738000, + 1769274000, + 1800810000, + 1832346000, + 1863968400, + 1895504400, + 1927040400, + 1958576400, + 1990198800, + 2021734800, + 2053270800, + 2084806800, + 2116429200, + 2147965200, + 2179501200, + 2211037200, + 2242659600, + 2274195600 + ] + .span(); + + let absorptions: Span = array![ + 0, + 29609535, + 47991466, + 88828605, + 118438140, + 370922507, + 623406874, + 875891241, + 1128375608, + 1380859976, + 2076175721, + 2771491466, + 3466807212, + 4162122957, + 4857438703, + 5552754448, + 6248070193, + 6943385939, + 7638701684, + 8000000000 + ] + .span(); + + setup_project(project_address, 8000000000, times, absorptions,); + + (project_address, spy) +} + +// +// Tests +// + +#[test] +fn test_burner_init() { + let owner_address: ContractAddress = contract_address_const::<'owner'>(); + let (project_address, _) = default_setup(owner_address); + let (burner_address, _) = deploy_burner(owner_address, project_address); + + let burner = IBurnHandlerDispatcher { contract_address: burner_address }; + + // [Assert] cointract is empty + start_prank(CheatTarget::One(burner_address), owner_address); + let carbon_pending = burner.get_carbon_retired(2025); + assert(carbon_pending == 0, 'carbon pending should be 0'); + + let carbon_retired = burner.get_carbon_retired(2025); + assert(carbon_retired == 0, 'carbon retired should be 0'); +} + +// Nominal cases + +#[test] +fn test_burner_retirement() { + let owner_address: ContractAddress = contract_address_const::<'owner'>(); + let (project_address, _) = default_setup(owner_address); + let (burner_address, _) = deploy_burner(owner_address, project_address); + + // [Prank] use owner address as caller + start_prank(CheatTarget::One(project_address), owner_address); + start_prank(CheatTarget::One(burner_address), owner_address); + + // [Effect] setup a batch of carbon credits + let absorber = IAbsorberDispatcher { contract_address: project_address }; + let carbon_credits = ICarbonCreditsHandlerDispatcher { contract_address: project_address }; + + assert(absorber.is_setup(), 'Error during setup'); + let project_contract = IProjectDispatcher { contract_address: project_address }; + let erc155 = IERC1155Dispatcher { contract_address: project_address }; + + let decimal: u8 = project_contract.decimals(); + assert(decimal == 6, 'Error of decimal'); + + let share: u256 = 125000; + let cc_distribution: Span = absorber.compute_carbon_vintage_distribution(share); + let cc_years_vintages: Span = carbon_credits.get_years_vintage(); + project_contract.batch_mint(owner_address, cc_years_vintages, cc_distribution); + + let carbon_balance = erc155.balance_of(owner_address, 2025); + println!("Carbon balance: {}", carbon_balance); + + // [Effect] update Vintage status + carbon_credits.update_vintage_status(2025, CarbonVintageType::Audited.into()); + + // [Effect] try to retire carbon credits + let burner = IBurnHandlerDispatcher { contract_address: burner_address }; + burner.retire_carbon_credits(2025, 1000000); + + let carbon_retired = burner.get_carbon_retired(2025); + assert(carbon_retired == 1000000, 'carbon retired is wrong'); +} + +// Error cases + +#[test] +#[should_panic(expected: ('Not own enough carbon credits',))] +fn test_burner_not_enough_CC() { + let owner_address: ContractAddress = contract_address_const::<'owner'>(); + let (project_address, _) = default_setup(owner_address); + let (burner_address, _) = deploy_burner(owner_address, project_address); + + // [Prank] use owner address as caller + start_prank(CheatTarget::One(project_address), owner_address); + start_prank(CheatTarget::One(burner_address), owner_address); + + // [Effect] setup a batch of carbon credits + let absorber = IAbsorberDispatcher { contract_address: project_address }; + let carbon_credits = ICarbonCreditsHandlerDispatcher { contract_address: project_address }; + + assert(absorber.is_setup(), 'Error during setup'); + let project_contract = IProjectDispatcher { contract_address: project_address }; + + let decimal: u8 = project_contract.decimals(); + assert(decimal == 6, 'Error of decimal'); + + let share: u256 = 125000; + let cc_distribution: Span = absorber.compute_carbon_vintage_distribution(share); + let cc_years_vintages: Span = carbon_credits.get_years_vintage(); + project_contract.batch_mint(owner_address, cc_years_vintages, cc_distribution); + + // [Effect] update Vintage status + carbon_credits.update_vintage_status(2025, CarbonVintageType::Audited.into()); + + // [Effect] try to retire carbon credits + let burner = IBurnHandlerDispatcher { contract_address: burner_address }; + burner.retire_carbon_credits(2025, 5000000); +} + + +#[test] +#[should_panic(expected: ('Vintage status is not audited',))] +fn test_burner_wrong_status() { + let owner_address: ContractAddress = contract_address_const::<'owner'>(); + let (project_address, _) = default_setup(owner_address); + let (burner_address, _) = deploy_burner(owner_address, project_address); + + // [Prank] use owner address as caller + start_prank(CheatTarget::One(project_address), owner_address); + start_prank(CheatTarget::One(burner_address), owner_address); + + // [Effect] setup a batch of carbon credits + let absorber = IAbsorberDispatcher { contract_address: project_address }; + let carbon_credits = ICarbonCreditsHandlerDispatcher { contract_address: project_address }; + + assert(absorber.is_setup(), 'Error during setup'); + let project_contract = IProjectDispatcher { contract_address: project_address }; + + let decimal: u8 = project_contract.decimals(); + assert(decimal == 6, 'Error of decimal'); + + let share: u256 = 125000; + let cc_distribution: Span = absorber.compute_carbon_vintage_distribution(share); + let cc_years_vintages: Span = carbon_credits.get_years_vintage(); + project_contract.batch_mint(owner_address, cc_years_vintages, cc_distribution); + + // [Effect] try to retire carbon credits + let burner = IBurnHandlerDispatcher { contract_address: burner_address }; + burner.retire_carbon_credits(2025, 1000000); +} diff --git a/tests/test_carbon_handler.cairo b/tests/test_carbon_handler.cairo index ef73796..49ed7ee 100644 --- a/tests/test_carbon_handler.cairo +++ b/tests/test_carbon_handler.cairo @@ -35,7 +35,7 @@ use carbon_v3::components::absorber::interface::{ use carbon_v3::components::absorber::carbon_handler::AbsorberComponent::{ Event, AbsorptionUpdate, ProjectValueUpdate }; -use carbon_v3::components::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; +use carbon_v3::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; use carbon_v3::components::absorber::carbon_handler::AbsorberComponent; // Contracts diff --git a/tests/test_mint.cairo b/tests/test_mint.cairo index 627f3b0..16d3cfa 100644 --- a/tests/test_mint.cairo +++ b/tests/test_mint.cairo @@ -38,7 +38,7 @@ use carbon_v3::components::absorber::interface::{ use carbon_v3::components::absorber::carbon_handler::AbsorberComponent::{ Event, AbsorptionUpdate, ProjectValueUpdate }; -use carbon_v3::components::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; +use carbon_v3::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; use carbon_v3::components::absorber::carbon_handler::AbsorberComponent; use carbon_v3::components::minter::interface::{IMintDispatcher, IMintDispatcherTrait}; diff --git a/tests/test_project.cairo b/tests/test_project.cairo index 99495a9..4cf54da 100644 --- a/tests/test_project.cairo +++ b/tests/test_project.cairo @@ -13,6 +13,10 @@ use snforge_std as snf; use snforge_std::{CheatTarget, ContractClassTrait, EventSpy, SpyOn, start_prank, stop_prank}; use alexandria_storage::list::{List, ListTrait}; +// Data + +use carbon_v3::data::carbon_vintage::{CarbonVintage, CarbonVintageType}; + // Components use carbon_v3::components::absorber::interface::{ @@ -59,33 +63,8 @@ fn setup_project( project.set_project_carbon(project_carbon); } -#[test] -fn test_constructor_ok() { - let (_project_address, _spy) = deploy_project(); -} - -#[test] -fn test_is_setup() { - let (project_address, _) = deploy_project(); - let project = IAbsorberDispatcher { contract_address: project_address }; - - setup_project( - project_address, - 1573000000, - array![1706785200, 2306401200].span(), - array![0, 1573000000].span(), - ); - - assert(project.is_setup(), 'Error during setup'); -} - -#[test] -fn test_project_batch_mint() { - let owner_address: ContractAddress = contract_address_const::<'owner'>(); - let other_address: ContractAddress = contract_address_const::<'other'>(); - let (project_address, _) = deploy_project(); - let absorber = IAbsorberDispatcher { contract_address: project_address }; - let carbon_credits = ICarbonCreditsHandlerDispatcher { contract_address: project_address }; +fn default_setup() -> (ContractAddress, EventSpy) { + let (project_address, spy) = deploy_project(); let times: Span = array![ 1674579600, @@ -137,6 +116,36 @@ fn test_project_batch_mint() { setup_project(project_address, 8000000000, times, absorptions,); + (project_address, spy) +} + +#[test] +fn test_constructor_ok() { + let (_project_address, _spy) = deploy_project(); +} + +#[test] +fn test_is_setup() { + let (project_address, _) = deploy_project(); + let project = IAbsorberDispatcher { contract_address: project_address }; + + setup_project( + project_address, + 1573000000, + array![1706785200, 2306401200].span(), + array![0, 1573000000].span(), + ); + + assert(project.is_setup(), 'Error during setup'); +} + +#[test] +fn test_project_batch_mint() { + let owner_address: ContractAddress = contract_address_const::<'owner'>(); + let (project_address, _) = default_setup(); + let absorber = IAbsorberDispatcher { contract_address: project_address }; + let carbon_credits = ICarbonCreditsHandlerDispatcher { contract_address: project_address }; + start_prank(CheatTarget::One(project_address), owner_address); assert(absorber.is_setup(), 'Error during setup'); @@ -145,11 +154,24 @@ fn test_project_batch_mint() { let decimal: u8 = project_contract.decimals(); assert(decimal == 6, 'Error of decimal'); - let balance: u256 = project_contract.balance(owner_address, 2027); - assert(balance == 0, 'Error of balance'); - let share: u256 = 125000; let cc_distribution: Span = absorber.compute_carbon_vintage_distribution(share); let cc_years_vintages: Span = carbon_credits.get_years_vintage(); project_contract.batch_mint(owner_address, cc_years_vintages, cc_distribution); } + +#[test] +fn test_project_set_vintage_status() { + let owner_address: ContractAddress = contract_address_const::<'owner'>(); + let (project_address, _) = default_setup(); + let absorber = IAbsorberDispatcher { contract_address: project_address }; + let carbon_credits = ICarbonCreditsHandlerDispatcher { contract_address: project_address }; + + start_prank(CheatTarget::One(project_address), owner_address); + + assert(absorber.is_setup(), 'Error during setup'); + + carbon_credits.update_vintage_status(2025, 2); + let vinatge: CarbonVintage = carbon_credits.get_specific_carbon_vintage(2025); + assert(vinatge.cc_status == CarbonVintageType::Audited, 'Error of status'); +}