Skip to content

Commit

Permalink
Add successful campaign test
Browse files Browse the repository at this point in the history
  • Loading branch information
Nenad committed Jun 12, 2024
1 parent e581234 commit 4a6e05d
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 2 deletions.
3 changes: 2 additions & 1 deletion listings/applications/crowdfunding/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "crowdfunding"
version.workspace = true
edition = "2023_11"

[lib]
# [lib]

[dependencies]
starknet.workspace = true
Expand All @@ -16,3 +16,4 @@ test.workspace = true

[[target.starknet-contract]]
casm = true
build-external-contracts = ["openzeppelin::presets::erc20::ERC20"]
21 changes: 20 additions & 1 deletion listings/applications/crowdfunding/src/campaign.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ pub mod Campaign {

let contributor = get_caller_address();
let this = get_contract_address();
let success = self.token.read().transfer_from(contributor, this, amount.into());
let success = self.token.read().transfer_from(contributor, this, amount);
assert(success, Errors::TRANSFER_FAILED);

self.contributions.add(contributor, amount);
Expand Down Expand Up @@ -248,6 +248,25 @@ pub mod Campaign {
self.emit(Event::Activated(Activated {}));
}

/// There are currently 3 possibilities for performing contract upgrades:
/// 1. Trust the campaign factory owner -> this is suboptimal, as factory owners have no responsibility to either creators or contributors,
/// and there's nothing stopping them from implementing a malicious upgrade.
/// 2. Trust the campaign creator -> the contributors already trust the campaign creator that they'll do what they promised in the campaign.
/// It's not a stretch to trust them with verifying that the contract upgrade is necessary.
/// 3. Trust no one, contract upgrades are forbidden -> could be a problem if a vulnerability is discovered and campaign funds are in danger.
///
/// This function implements the 2nd option, as it seems to be the most optimal solution, especially from the point of view of what to do if
/// any of the upgrades fail for whatever reason - campaign creator is solely responsible for upgrading their contracts.
///
/// To improve contributor trust, contract upgrades refund all of contributor funds, so that on the off chance that the creator is in cahoots
/// with factory owners to implement a malicious upgrade, the contributor funds would be returned.
/// There are some problems with this though:
/// - contributors wouldn't have even been donating if they weren't trusting the creator - since the funds end up with them in the end, they
/// have to trust that creators would use the campaign funds as they promised when creating the campaign.
/// - since the funds end up with the creators, they have no incentive to implement a malicious upgrade - they'll have the funds either way.
/// - each time there's an upgrade, the campaign gets reset, which introduces new problems:
/// - What if the Campaign was close to ending? We just took all of their contributions away, and there might not be enough time to get them back.
/// We solve this by letting the creators prolong the duration of the campaign.
fn upgrade(ref self: ContractState, impl_hash: ClassHash) -> Result<(), Array<felt252>> {
if get_caller_address() != self.ownable.owner() {
return Result::Err(array![components::ownable::Errors::UNAUTHORIZED]);
Expand Down
93 changes: 93 additions & 0 deletions listings/applications/crowdfunding/src/tests.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ use snforge_std::{
use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait};
use crowdfunding::campaign::Status;
use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};

const ERC20_SUPPLY: u256 = 10000;

/// Deploy a campaign contract with the provided data
fn deploy_with(
Expand All @@ -38,6 +41,54 @@ fn deploy() -> ICampaignDispatcher {
deploy_with("title 1", "description 1", 10000, 60, contract_address_const::<'token'>())
}

fn deploy_with_token() -> ICampaignDispatcher {
// define ERC20 data
let erc20_name: ByteArray = "My Token";
let erc20_symbol: ByteArray = "MTKN";
let erc20_supply: u256 = 100000;
let erc20_owner = contract_address_const::<'erc20_owner'>();

// deploy ERC20 token
let erc20 = declare("ERC20").unwrap();
let mut erc20_constructor_calldata = array![];
(erc20_name, erc20_symbol, erc20_supply, erc20_owner).serialize(ref erc20_constructor_calldata);
let (erc20_address, _) = erc20.deploy(@erc20_constructor_calldata).unwrap();

// transfer amounts to some contributors
let contributor_1 = contract_address_const::<'contributor_1'>();
let contributor_2 = contract_address_const::<'contributor_2'>();
let contributor_3 = contract_address_const::<'contributor_3'>();

start_cheat_caller_address(erc20_address, erc20_owner);
let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address };
erc20_dispatcher.transfer(contributor_1, 10000);
erc20_dispatcher.transfer(contributor_2, 10000);
erc20_dispatcher.transfer(contributor_3, 10000);

// deploy the actual Campaign contract
let campaign_dispatcher = deploy_with("title 1", "description 1", 10000, 60, erc20_address);

// approve the contributions for each contributor
start_cheat_caller_address(erc20_address, contributor_1);
erc20_dispatcher.approve(campaign_dispatcher.contract_address, 10000);
start_cheat_caller_address(erc20_address, contributor_2);
erc20_dispatcher.approve(campaign_dispatcher.contract_address, 10000);
start_cheat_caller_address(erc20_address, contributor_3);
erc20_dispatcher.approve(campaign_dispatcher.contract_address, 10000);

// NOTE: don't forget to stop the caller address cheat on the ERC20 contract!!
// Otherwise, any call to this contract from any source will have the cheated
// address as the caller
stop_cheat_caller_address(erc20_address);

campaign_dispatcher
}

fn _get_token_dispatcher(campaign: ICampaignDispatcher) -> IERC20Dispatcher {
let token_address = campaign.get_details().token;
IERC20Dispatcher { contract_address: token_address }
}

#[test]
fn test_deploy() {
let campaign = deploy();
Expand All @@ -57,6 +108,48 @@ fn test_deploy() {
assert_eq!(campaign_ownable.owner(), owner);
}

#[test]
fn test_successful_campaign() {
let campaign = deploy_with_token();
let token = _get_token_dispatcher(campaign);

let creator = contract_address_const::<'creator'>();
let contributor_1 = contract_address_const::<'contributor_1'>();
let contributor_2 = contract_address_const::<'contributor_2'>();
let contributor_3 = contract_address_const::<'contributor_3'>();

start_cheat_caller_address(campaign.contract_address, creator);
campaign.start();
assert_eq!(campaign.get_details().status, Status::ACTIVE);

start_cheat_caller_address(campaign.contract_address, contributor_1);
let mut prev_balance = token.balance_of(contributor_1);
campaign.contribute(3000);
assert_eq!(campaign.get_details().total_contributions, 3000);
assert_eq!(campaign.get_contribution(contributor_1), 3000);
assert_eq!(token.balance_of(contributor_1), prev_balance - 3000);

start_cheat_caller_address(campaign.contract_address, contributor_2);
prev_balance = token.balance_of(contributor_2);
campaign.contribute(500);
assert_eq!(campaign.get_details().total_contributions, 3500);
assert_eq!(campaign.get_contribution(contributor_2), 500);
assert_eq!(token.balance_of(contributor_2), prev_balance - 500);

start_cheat_caller_address(campaign.contract_address, contributor_3);
prev_balance = token.balance_of(contributor_3);
campaign.contribute(7000);
assert_eq!(campaign.get_details().total_contributions, 10500);
assert_eq!(campaign.get_contribution(contributor_3), 7000);
assert_eq!(token.balance_of(contributor_3), prev_balance - 7000);

start_cheat_caller_address(campaign.contract_address, creator);
prev_balance = token.balance_of(creator);
campaign.claim();
assert_eq!(token.balance_of(creator), prev_balance + 10500);
assert_eq!(campaign.get_details().status, Status::SUCCESSFUL);
}

#[test]
fn test_upgrade_class_hash() {
let campaign = deploy();
Expand Down

0 comments on commit 4a6e05d

Please sign in to comment.