Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 49 additions & 37 deletions src/predifi.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,9 @@ pub mod Predifi {

self.pool_odds.write(pool_id, initial_odds);

// Add to pool count
self.pool_count.write(self.pool_count.read() + 1);
// Cache current pool count to avoid redundant read
let current_count = self.pool_count.read();
self.pool_count.write(current_count + 1);

pool_id
}
Expand Down Expand Up @@ -485,6 +486,13 @@ pub mod Predifi {
self.assert_greater_than_zero(pool_id);
self.assert_valid_felt252(option);

// Cache frequently used values to reduce storage reads
let caller = get_caller_address();
let contract_address = get_contract_address();
let token_addr = self.token_addr.read();
let dispatcher = IERC20Dispatcher { contract_address: token_addr };

// Single pool read for validation and data access
let mut pool = self.pools.read(pool_id);
self.assert_pool_exists(@pool);
self.assert_pool_active(@pool);
Expand All @@ -496,12 +504,7 @@ pub mod Predifi {
self.assert_valid_pool_option(option, option1, option2);
self.assert_amount_within_limits(amount, pool.minBetAmount, pool.maxBetAmount);

// Transfer betting amount from the user to the contract
let caller = get_caller_address();
let dispatcher = IERC20Dispatcher { contract_address: self.token_addr.read() };

// Check balance and allowance using SecurityTrait
let contract_address = get_contract_address();
self.assert_sufficient_balance(dispatcher, caller, amount);
self.assert_sufficient_allowance(dispatcher, caller, contract_address, amount);

Expand All @@ -511,7 +514,7 @@ pub mod Predifi {
// Transfer the tokens
dispatcher.transfer_from(caller, contract_address, amount);

let mut pool = self.pools.read(pool_id);
// Update pool state in memory
if option == option1 {
pool.totalStakeOption1 += amount;
pool
Expand All @@ -526,37 +529,44 @@ pub mod Predifi {
pool.totalBetAmountStrk += amount;
pool.totalBetCount += 1;

// Update pool odds
let odds = self
.calculate_odds(pool.pool_id, pool.totalStakeOption1, pool.totalStakeOption2);
self.pool_odds.write(pool_id, odds);

// Calculate the user's shares
let shares: u256 = if option == option1 {
self.calculate_shares(amount, pool.totalStakeOption1, pool.totalStakeOption2)
} else {
self.calculate_shares(amount, pool.totalStakeOption2, pool.totalStakeOption1)
};

// Store user stake
// Create user stake object
let user_stake = UserStake {
option: option == option2,
amount: amount,
shares: shares,
timestamp: get_block_timestamp(),
};
let address: ContractAddress = get_caller_address();
self.user_stakes.write((pool.pool_id, address), user_stake);
self.pool_vote.write(pool.pool_id, option == option2);
self.pool_stakes.write(pool.pool_id, user_stake);
self.pools.write(pool.pool_id, pool);
self.track_user_participation(address, pool_id);

// Batch storage writes to minimize gas costs
self
.pool_odds
.write(
pool_id,
self.calculate_odds(pool_id, pool.totalStakeOption1, pool.totalStakeOption2),
);
self.user_stakes.write((pool_id, caller), user_stake);
self.pool_vote.write(pool_id, option == option2);
self.pool_stakes.write(pool_id, user_stake);
self.pools.write(pool_id, pool);
self.track_user_participation(caller, pool_id);

// End reentrancy guard
self.reentrancy_guard.end();

// Emit event
self.emit(Event::BetPlaced(BetPlaced { pool_id, address, option, amount, shares }));
self
.emit(
Event::BetPlaced(
BetPlaced { pool_id, address: caller, option, amount, shares },
),
);
}

/// @notice Stakes tokens to become eligible for validation rewards
Expand All @@ -578,13 +588,13 @@ pub mod Predifi {
self.assert_pool_not_suspended(@pool);
self.assert_min_stake_amount(amount);

// Cache frequently used values to reduce redundant calls
let address: ContractAddress = get_caller_address();

// Transfer stake amount from user to contract
let dispatcher = IERC20Dispatcher { contract_address: self.token_addr.read() };
let contract_address = get_contract_address();
let token_addr = self.token_addr.read();
let dispatcher = IERC20Dispatcher { contract_address: token_addr };

// Check balance and allowance using SecurityTrait
let contract_address = get_contract_address();
self.assert_sufficient_balance(dispatcher, address, amount);
self.assert_sufficient_allowance(dispatcher, address, contract_address, amount);

Expand All @@ -594,21 +604,21 @@ pub mod Predifi {
// Transfer the tokens
dispatcher.transfer_from(address, contract_address, amount);

// Add to previous stake if any
// Read and update user stake in a single operation
let mut stake = self.user_stakes.read((pool_id, address));
stake.amount = amount + stake.amount;
// write the new stake

// Batch storage writes to minimize gas costs
self.user_stakes.write((pool_id, address), stake);
// grant the validator role
self.accesscontrol._grant_role(VALIDATOR_ROLE, address);
// add caller to validator list
self.validators.push(address);
self.track_user_participation(address, pool_id);
// emit event
self.emit(UserStaked { pool_id, address, amount });

// End reentrancy guard
self.reentrancy_guard.end();

// emit event
self.emit(UserStaked { pool_id, address, amount });
}


Expand Down Expand Up @@ -853,6 +863,11 @@ pub mod Predifi {
/// @dev Allows users to withdraw funds from pools in emergency state.
/// @param pool_id The pool ID to withdraw from.
fn emergency_withdraw(ref self: ContractState, pool_id: u256) {
// Cache frequently used values to reduce storage reads
let caller = get_caller_address();
let token_addr = self.token_addr.read();
let strk_token = IERC20Dispatcher { contract_address: token_addr };

// Check if pool exists
let pool = self.pools.read(pool_id);
assert(pool.exists, Errors::POOL_DOES_NOT_EXIST);
Expand All @@ -865,8 +880,6 @@ pub mod Predifi {
Errors::EMERGENCY_WITHDRAWALS_NOT_ALLOWED,
);

let caller = get_caller_address();

// Check if user has participated in this pool
let has_participated = self.has_user_participated_in_pool(caller, pool_id);
assert(has_participated, Errors::UNAUTHORIZED_CALLER);
Expand All @@ -887,7 +900,6 @@ pub mod Predifi {
);

// Transfer tokens back to user
let strk_token = IERC20Dispatcher { contract_address: self.token_addr.read() };
strk_token.transfer(caller, withdrawal_amount);

// Emit emergency withdrawal event
Expand Down Expand Up @@ -934,7 +946,7 @@ pub mod Predifi {
if action_type == 3 { // EmergencyWithdrawal
// For emergency withdrawal, action_data must contain a valid user address
// Use the helper function to validate the address and handle potential panics
let user_address = self.extract_user_address_from_action_data(action_data);
let _user_address = self.extract_user_address_from_action_data(action_data);
// Store the validated address for later use if needed
// Note: The validation is done here to fail fast if the address is invalid
}
Expand Down Expand Up @@ -1802,7 +1814,7 @@ pub mod Predifi {
/// @param total_stake_other_option Total stake for other option.
/// @return The calculated shares.
fn calculate_shares(
ref self: ContractState,
self: @ContractState,
amount: u256,
total_stake_selected_option: u256,
total_stake_other_option: u256,
Expand All @@ -1823,7 +1835,7 @@ pub mod Predifi {
/// @param total_stake_option2 Total stake for option 2.
/// @return The PoolOdds struct.
fn calculate_odds(
ref self: ContractState,
self: @ContractState,
pool_id: u256,
total_stake_option1: u256,
total_stake_option2: u256,
Expand Down
185 changes: 185 additions & 0 deletions tests/test_gas_benchmarking.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use contract::interfaces::ipredifi::IPredifiDispatcherTrait;
use snforge_std::{
start_cheat_block_timestamp, start_cheat_caller_address, stop_cheat_caller_address,
};
use starknet::ContractAddress;

// Validator role
const VALIDATOR_ROLE: felt252 = selector!("VALIDATOR_ROLE");
const POOL_CREATOR: ContractAddress = 123.try_into().unwrap();
const USER_ONE: ContractAddress = 'User1'.try_into().unwrap();
const ONE_STRK: u256 = 1_000_000_000_000_000_000;
use super::test_utils::{
approve_tokens_for_payment, create_default_pool, deploy_predifi, mint_tokens_for,
};
/// @notice Gas benchmarking tests for optimized storage operations
/// @dev These tests measure gas consumption of optimized functions to ensure efficiency

#[test]
fn test_vote_function_gas_optimization() {
let (contract, _, _, pool_creator, erc20_address) = deploy_predifi();

// Setup pool
start_cheat_caller_address(erc20_address, pool_creator);
approve_tokens_for_payment(
contract.contract_address, erc20_address, 200_000_000_000_000_000_000_000,
);
stop_cheat_caller_address(erc20_address);

start_cheat_caller_address(contract.contract_address, pool_creator);
let pool_id = create_default_pool(contract);

// Setup user with tokens
start_cheat_caller_address(erc20_address, USER_ONE);
mint_tokens_for(USER_ONE, erc20_address, ONE_STRK * 1000);
approve_tokens_for_payment(contract.contract_address, erc20_address, ONE_STRK * 1000);
stop_cheat_caller_address(erc20_address);

// Set future timestamp for pool to be active
start_cheat_block_timestamp(contract.contract_address, 1710000001);

start_cheat_caller_address(contract.contract_address, USER_ONE);

// Execute vote function (without gas metering since it's not available in this version)
contract.vote(pool_id, 'Team A', 1000);

// Verify the vote was successful by checking pool state
let pool = contract.get_pool(pool_id);
assert(pool.totalBetCount == 1, 'Vote not recorded');
assert(pool.totalStakeOption1 == 1000, 'Stake incorrect');
}

#[test]
fn test_stake_function_gas_optimization() {
let (contract, _, _, pool_creator, erc20_address) = deploy_predifi();

// Setup pool
start_cheat_caller_address(erc20_address, pool_creator);
approve_tokens_for_payment(
contract.contract_address, erc20_address, 200_000_000_000_000_000_000_000,
);
stop_cheat_caller_address(erc20_address);

start_cheat_caller_address(contract.contract_address, pool_creator);
let pool_id = create_default_pool(contract);

// Setup user with tokens
start_cheat_caller_address(erc20_address, USER_ONE);
mint_tokens_for(USER_ONE, erc20_address, ONE_STRK * 1000);
approve_tokens_for_payment(contract.contract_address, erc20_address, ONE_STRK * 1000);
stop_cheat_caller_address(erc20_address);

start_cheat_caller_address(contract.contract_address, USER_ONE);

// Execute stake function
contract.stake(pool_id, 200_000_000_000_000_000_000);

// Verify the stake was successful
let user_stake = contract.get_user_stake(pool_id, USER_ONE);
assert(user_stake.amount == 200_000_000_000_000_000_000, 'Stake amount wrong');
}

#[test]
fn test_create_pool_function_gas_optimization() {
let (contract, _, _, pool_creator, erc20_address) = deploy_predifi();

// Setup tokens for pool creation fee
start_cheat_caller_address(erc20_address, pool_creator);
approve_tokens_for_payment(
contract.contract_address, erc20_address, 200_000_000_000_000_000_000_000,
);
stop_cheat_caller_address(erc20_address);

start_cheat_caller_address(contract.contract_address, pool_creator);

// Execute create_pool function
let pool_id = contract
.create_pool(
'Optimized Pool',
0, // 0 = WinBet
"A gas-optimized betting pool",
"image.png",
"event.com/details",
1710000000,
1710003600,
1710007200,
'Option A',
'Option B',
100,
10000,
5,
false,
0,
);

// Stop gas metering and log gas usage
// Note: Gas metering not available in current snforge version
assert!(pool_id != 0, "Pool creation failed");
}

#[test]
fn test_emergency_withdraw_gas_optimization() {
let (contract, _, _, pool_creator, erc20_address) = deploy_predifi();

// Setup pool and user participation
start_cheat_caller_address(erc20_address, pool_creator);
approve_tokens_for_payment(
contract.contract_address, erc20_address, 200_000_000_000_000_000_000_000,
);
stop_cheat_caller_address(erc20_address);

start_cheat_caller_address(contract.contract_address, pool_creator);
let pool_id = create_default_pool(contract);

// Setup user with tokens and make them participate
start_cheat_caller_address(erc20_address, USER_ONE);
mint_tokens_for(USER_ONE, erc20_address, ONE_STRK * 1000);
approve_tokens_for_payment(contract.contract_address, erc20_address, ONE_STRK * 1000);
stop_cheat_caller_address(erc20_address);

start_cheat_block_timestamp(contract.contract_address, 1710000001);
start_cheat_caller_address(contract.contract_address, USER_ONE);
contract.vote(pool_id, 'Team A', 1000);

// Simulate emergency state (this would normally be done by admin)
// For testing purposes, we'll assume emergency state is set

// Note: Emergency withdrawal test requires emergency state to be set first
// This is a placeholder for when emergency functionality is properly set up
println!("Emergency withdrawal gas test requires emergency state setup");
}

#[test]
fn test_multiple_votes_gas_comparison() {
let (contract, _, _, pool_creator, erc20_address) = deploy_predifi();

// Setup pool
start_cheat_caller_address(erc20_address, pool_creator);
approve_tokens_for_payment(
contract.contract_address, erc20_address, 200_000_000_000_000_000_000_000,
);
stop_cheat_caller_address(erc20_address);

start_cheat_caller_address(contract.contract_address, pool_creator);
let pool_id = create_default_pool(contract);

// Setup user with tokens
start_cheat_caller_address(erc20_address, USER_ONE);
mint_tokens_for(USER_ONE, erc20_address, ONE_STRK * 10000);
approve_tokens_for_payment(contract.contract_address, erc20_address, ONE_STRK * 10000);
stop_cheat_caller_address(erc20_address);

start_cheat_block_timestamp(contract.contract_address, 1710000001);
start_cheat_caller_address(contract.contract_address, USER_ONE);

// Execute multiple votes to test cumulative gas efficiency
contract.vote(pool_id, 'Team A', 1000);
contract.vote(pool_id, 'Team B', 2000);
contract.vote(pool_id, 'Team A', 3000);

// Verify final state
let pool = contract.get_pool(pool_id);
assert(pool.totalBetCount == 3, 'Should have 3 bets');
assert(pool.totalStakeOption1 == 1000 + 3000, 'Option1 stake wrong');
assert(pool.totalStakeOption2 == 2000, 'Option2 stake wrong');
}