diff --git a/Scarb.lock b/Scarb.lock index 0448917..c502793 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -4,3 +4,21 @@ version = 1 [[package]] name = "cohort_4" version = "0.1.0" +dependencies = [ + "snforge_std", +] + +[[package]] +name = "snforge_scarb_plugin" +version = "0.40.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:7c3b21f6cdab14fc63e19f9e6789b6a3d44f5618ebcf02d03b397375304e1891" + +[[package]] +name = "snforge_std" +version = "0.40.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:0221bbe959eec72eb2e30be68df66c4ff5dcd924ec491f285c974e49671fabc0" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/Scarb.toml b/Scarb.toml index dcbf065..4419ef1 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -5,11 +5,50 @@ edition = "2024_07" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html # [executable] -[dependencies] -[cairo] -enable-gas = false +[dependencies] +starknet = "2.11.2" [dev-dependencies] -cairo_test = "2.11.2" -# cairo_execute = "2.11.3" +snforge_std = "0.40.0" +assert_macros = "2.11.2" + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[tool.scarb] +allow-prebuilt-plugins = ["snforge_std"] + +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information + +# [tool.snforge] # Define `snforge` tool section +# exit_first = true # Stop tests execution immediately upon the first failure +# fuzzer_runs = 1234 # Number of runs of the random fuzzer +# fuzzer_seed = 1111 # Seed for the random fuzzer + +# [[tool.snforge.fork]] # Used for fork testing +# name = "SOME_NAME" # Fork name +# url = "http://your.rpc.url" # Url of the RPC provider +# block_id.tag = "latest" # Block to fork from (block tag) + +# [[tool.snforge.fork]] +# name = "SOME_SECOND_NAME" +# url = "http://your.second.rpc.url" +# block_id.number = "123" # Block to fork from (block number) + +# [[tool.snforge.fork]] +# name = "SOME_THIRD_NAME" +# url = "http://your.third.rpc.url" +# block_id.hash = "0x123" # Block to fork from (block hash) + +# [profile.dev.cairo] # Configure Cairo compiler +# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage +# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler +# inlining-strategy = "avoid" # Should be used if you want to use coverage + +# [features] # Used for conditional compilation +# enable_for_tests = [] # Feature name and list of other features that should be enabled with it + diff --git a/assignments/task_4.md b/assignments/task_4.md new file mode 100644 index 0000000..9918984 --- /dev/null +++ b/assignments/task_4.md @@ -0,0 +1,7 @@ +# Write comprehensive tests for newly added contracts: +- `Counter` +- `KillSwitch` +- `Aggregator` + +### Objective +The goal of this assignment is to familiarize developers with dispatchers and how they can be leveraged to interact with other contracts outside the file where it was created thus making contracts composable. Through this assignment, you will learn how to send modify and retrieve state variables of other contracts diff --git a/src/IHello.cairo b/src/IHello.cairo new file mode 100644 index 0000000..0d1c511 --- /dev/null +++ b/src/IHello.cairo @@ -0,0 +1,11 @@ +/// Interface representing `HelloContract`. +/// This interface allows modification and retrieval of the contract balance. +#[starknet::interface] +pub trait IHelloStarknet { + /// Increase contract balance. + fn increase_balance(ref self: TContractState, amount: felt252); + /// Retrieve contract balance. + fn get_balance(self: @TContractState) -> felt252; + + fn add_and_subtract(ref self: TContractState, amount: felt252); +} \ No newline at end of file diff --git a/src/INumber.cairo b/src/INumber.cairo new file mode 100644 index 0000000..eb4014b --- /dev/null +++ b/src/INumber.cairo @@ -0,0 +1,8 @@ +/// A trait for a contract that can store and retrieve a number +#[starknet::interface] +pub trait INumber { + /// Sets the number to the given value + fn set_number(ref self: TContractState, amount: u8); + /// Returns the current number + fn get_number(self: @TContractState) -> u8; +} \ No newline at end of file diff --git a/src/aggregator.cairo b/src/aggregator.cairo new file mode 100644 index 0000000..3e4cbe8 --- /dev/null +++ b/src/aggregator.cairo @@ -0,0 +1,77 @@ +#[starknet::interface] +pub trait IAggregator { + /// Increase contract count. + fn increase_count(ref self: TContractState, amount: u32); + /// Increase contract count. + /// + fn increase_counter_count(ref self: TContractState, amount: u32); + + /// Retrieve contract count. + fn decrease_count_by_one(ref self: TContractState); + /// Retrieve contract count. + fn get_count(self: @TContractState) -> u32; + + fn activate_switch(ref self: TContractState); +} + +/// Simple contract for managing count. +#[starknet::contract] +mod Agggregator { + use cohort_4::counter::{ICounterDispatcher, ICounterDispatcherTrait}; + use cohort_4::killswitch::{IKillSwitchDispatcher, IKillSwitchDispatcherTrait}; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + + #[storage] + struct Storage { + count: u32, + counter: ContractAddress, + killswitch: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, counter: ContractAddress, killswitch: ContractAddress) { + self.counter.write(counter); + self.killswitch.write(killswitch); + } + + + #[abi(embed_v0)] + impl AggregatorImpl of super::IAggregator { + fn increase_count(ref self: ContractState, amount: u32) { + assert(amount > 0, 'Amount cannot be 0'); + let counter = ICounterDispatcher { contract_address: self.counter.read() }; + let counter_count = counter.get_count(); + self.count.write(counter_count + amount); + } + + fn increase_counter_count(ref self: ContractState, amount: u32) { + let killswitch: IKillSwitchDispatcher = IKillSwitchDispatcher { + contract_address: self.killswitch.read(), + }; + assert(killswitch.get_status(), 'not active'); + ICounterDispatcher { contract_address: self.counter.read() }.increase_count(amount) + } + + fn decrease_count_by_one(ref self: ContractState) { + let current_count = self.get_count(); + assert!(current_count != 0, "Amount cannot be 0"); + self.count.write(current_count - 1); + } + + fn activate_switch(ref self: ContractState) { + let killswitch: IKillSwitchDispatcher = IKillSwitchDispatcher { + contract_address: self.killswitch.read(), + }; + + if !killswitch.get_status() { + killswitch.switch() + } + } + + fn get_count(self: @ContractState) -> u32 { + self.count.read() + } + } +} diff --git a/src/counter.cairo b/src/counter.cairo new file mode 100644 index 0000000..190c073 --- /dev/null +++ b/src/counter.cairo @@ -0,0 +1,43 @@ +/// Interface representing `Counter`. +/// This interface allows modification and retrieval of the contract count. +#[starknet::interface] +pub trait ICounter { + /// Increase contract count. + fn increase_count(ref self: TContractState, amount: u32); + + /// Decrease contract count by one + fn decrease_count_by_one(ref self: TContractState); + + /// Retrieve contract count. + fn get_count(self: @TContractState) -> u32; +} + +/// Simple contract for managing count. +#[starknet::contract] +mod Counter { + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + struct Storage { + count: u32, + } + + #[abi(embed_v0)] + impl CounterImpl of super::ICounter { + fn increase_count(ref self: ContractState, amount: u32) { + assert(amount > 0, 'Amount cannot be 0'); + let counter_count = self.get_count(); + self.count.write(counter_count + amount); + } + + fn decrease_count_by_one(ref self: ContractState) { + let current_count = self.get_count(); + assert(current_count > 0, 'Amount cannot be 0'); + self.count.write(current_count - 1); + } + + fn get_count(self: @ContractState) -> u32 { + self.count.read() + } + } +} diff --git a/src/hello.cairo b/src/hello.cairo new file mode 100644 index 0000000..fd7d3e2 --- /dev/null +++ b/src/hello.cairo @@ -0,0 +1,55 @@ +#[starknet::contract] +mod HelloStarknet { + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use crate::IHello::IHelloStarknet; + use crate::INumber::INumber; + + #[storage] + struct Storage { + balance: felt252, + } + + #[generate_trait] + impl InternalTrait of InternalImpl { + fn _add(ref self: ContractState, amount: felt252) { + let old_balance = self.balance.read(); + self.balance.write(old_balance + amount); + } + + fn _subtract(ref self: ContractState, amount: felt252) { + let old_balance = self.balance.read(); + self.balance.write(old_balance - amount); + } + } + + fn add(amount: felt252) -> felt252 { + amount + 2 + } + + + #[abi(embed_v0)] + impl HelloStarknet of IHelloStarknet { + fn increase_balance(ref self: ContractState, amount: felt252) { + let upgraded_score = add(amount); + self._add(upgraded_score); + } + + fn get_balance(self: @ContractState) -> felt252 { + self.balance.read() + } + + fn add_and_subtract(ref self: ContractState, amount: felt252) { + self._add(amount); + self._subtract(amount); + } + } + + #[abi(embed_v0)] + impl INumberImpl of INumber { + fn set_number(ref self: ContractState, amount: u8) {} + fn get_number(self: @ContractState) -> u8 { + let number: u8 = 8; + number + } + } +} \ No newline at end of file diff --git a/src/killswitch.cairo b/src/killswitch.cairo new file mode 100644 index 0000000..6437088 --- /dev/null +++ b/src/killswitch.cairo @@ -0,0 +1,35 @@ +/// Interface representing `HelloContract`. +/// This interface allows modification and retrieval of the contract count. +#[starknet::interface] +pub trait IKillSwitch { + /// Increase contract count. + fn switch(ref self: TContractState); + + /// Retrieve contract count. + fn get_status(self: @TContractState) -> bool; +} + +/// Simple contract for managing count. +#[starknet::contract] +mod KillSwitch { + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + struct Storage { + status: bool, + } + + + #[abi(embed_v0)] + impl KillSwitchImpl of super::IKillSwitch { + fn switch(ref self: ContractState) { + // assert(amount != 0, 'Amount cannot be 0'); + self.status.write(!self.status.read()); + } + + + fn get_status(self: @ContractState) -> bool { + self.status.read() + } + } +} diff --git a/src/lib.cairo b/src/lib.cairo index 2861ea0..8b3993c 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,3 +1,12 @@ +pub mod hello; +pub mod IHello; +pub mod INumber; +pub mod counter; +pub mod killswitch; +pub mod aggregator; + + + fn main() { // Function calls (Uncomment to execute them) // say_name("Sylvia Nnoruka!"); diff --git a/tests/test_contract.cairo b/tests/test_contract.cairo new file mode 100644 index 0000000..9d6708a --- /dev/null +++ b/tests/test_contract.cairo @@ -0,0 +1,49 @@ +use starknet::ContractAddress; + +use snforge_std::{declare, ContractClassTrait, DeclareResultTrait}; + +use cohort_4::counter::{ICounterDispatcher, ICounterDispatcherTrait, ICounterSafeDispatcher, ICounterSafeDispatcherTrait}; + +fn deploy_contract() -> ContractAddress { + let countract_name: ByteArray = "Counter"; + let contract = declare(countract_name).unwrap().contract_class(); + let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); + contract_address +} + +#[test] +fn test_increase_count() { + let contract_address = deploy_contract(); + + let dispatcher = ICounterDispatcher { contract_address }; + + let balance_before = dispatcher.get_count(); + assert(balance_before == 0, 'Invalid balance'); + + dispatcher.increase_count(42); + + let balance_after = dispatcher.get_count(); + assert(balance_after == 42, 'Invalid balance'); +} + +#[test] +#[feature("safe_dispatcher")] +fn test_cannot_increase_balance_with_zero_value() { + let contract_address = deploy_contract(); + + let dispatcher = ICounterDispatcher { contract_address }; + + let balance_before = dispatcher.get_count(); + assert(balance_before == 0 , 'Invalid balance'); + + let safe_dispatcher = ICounterSafeDispatcher { contract_address }; + + match safe_dispatcher.increase_count(0) { + Result::Ok(_) => core::panic_with_felt252('Should have panicked'), + Result::Err(panic_data) => { + assert(*panic_data.at(0) == 'Amount cannot be 0', *panic_data.at(0)); + } + }; + + +}