From 8fe9b1568b17b34419d0df7bba7f47e6a7ef3462 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Wed, 2 Oct 2024 03:47:54 +0100 Subject: [PATCH 01/11] feat: Jolt --- src/base/constants.cairo | 1 + src/base/constants/contract_addresses.cairo | 9 + src/base/constants/errors.cairo | 5 + src/base/constants/types.cairo | 106 ++++- src/interfaces.cairo | 2 + src/interfaces/IERC20.cairo | 20 + src/interfaces/IJolt.cairo | 18 + src/jolt.cairo | 1 + src/jolt/jolt.cairo | 459 ++++++++++++++++++++ src/lib.cairo | 1 + 10 files changed, 602 insertions(+), 20 deletions(-) create mode 100644 src/base/constants/contract_addresses.cairo create mode 100644 src/interfaces/IERC20.cairo create mode 100644 src/interfaces/IJolt.cairo create mode 100644 src/jolt.cairo create mode 100644 src/jolt/jolt.cairo diff --git a/src/base/constants.cairo b/src/base/constants.cairo index 422d669..8b8b437 100644 --- a/src/base/constants.cairo +++ b/src/base/constants.cairo @@ -1,2 +1,3 @@ pub mod errors; pub mod types; +pub mod contract_addresses; \ No newline at end of file diff --git a/src/base/constants/contract_addresses.cairo b/src/base/constants/contract_addresses.cairo new file mode 100644 index 0000000..deed7c7 --- /dev/null +++ b/src/base/constants/contract_addresses.cairo @@ -0,0 +1,9 @@ +// ************************************************************************* +// JOLT - ERC20 CONTRACT ADDRESSES +// ************************************************************************* +pub mod Addresses { + pub const ETH: felt252 = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; + pub const STRK: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; + pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; + pub const USDT: felt252 = 0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8; +} diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index 221d357..cac5131 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -19,4 +19,9 @@ pub mod Errors { pub const INVALID_PROFILE_ADDRESS: felt252 = 'Karst: invalid profile address!'; pub const SELF_FOLLOWING: felt252 = 'Karst: self follow is forbidden'; pub const ALREADY_REACTED: felt252 = 'Karst: already react to post!'; + pub const SELF_TIPPING: felt252 = 'Karst: self-tip forbidden!'; + pub const MAX_TIPPING: felt252 = 'Karst: exceeds max tipping!'; + pub const SELF_TRANSFER: felt252 = 'Karst: self-transfer forbidden!'; + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Karst: insufficient allowance!'; + pub const AUTO_RENEW_DURATION_ENDED: felt252 = 'Karst: auto renew ended!'; } diff --git a/src/base/constants/types.cairo b/src/base/constants/types.cairo index 8e1d6c8..70a4494 100644 --- a/src/base/constants/types.cairo +++ b/src/base/constants/types.cairo @@ -1,25 +1,8 @@ -// ************************************************************************* -// TYPES -// ************************************************************************* use starknet::ContractAddress; -// /** -// * @notice A struct containing token follow-related data. -// * -// * @param followed_profile_address The ID of the profile being followed. -// * @param follower_profile_address The ID of the profile following. -// * @param followTimestamp The timestamp of the current follow, if a profile is using the token to -// follow. -// * @param block_status true if follower is blocked, false otherwise -// */ -#[derive(Drop, Serde, starknet::Store)] -pub struct FollowData { - pub followed_profile_address: ContractAddress, - pub follower_profile_address: ContractAddress, - pub follow_timestamp: u64, - pub block_status: bool, -} - +// ************************************************************************* +// PROFILE +// ************************************************************************* // * @notice A struct containing profile data. // * profile_address The profile ID of a karst profile // * profile_owner The address that created the profile_address @@ -36,6 +19,9 @@ pub struct Profile { pub follow_nft: ContractAddress } +// ************************************************************************* +// PUBLICATION +// ************************************************************************* // /** // * @notice A struct containing publication data. // * @@ -166,3 +152,83 @@ pub struct Downvote { pub transaction_executor: ContractAddress, pub block_timestamp: u64, } + +// ************************************************************************* +// FOLLOW +// ************************************************************************* +// /** +// * @notice A struct containing token follow-related data. +// * +// * @param followed_profile_address The ID of the profile being followed. +// * @param follower_profile_address The ID of the profile following. +// * @param followTimestamp The timestamp of the current follow, if a profile is using the token to +// follow. +// * @param block_status true if follower is blocked, false otherwise +// */ +#[derive(Drop, Serde, starknet::Store)] +pub struct FollowData { + pub followed_profile_address: ContractAddress, + pub follower_profile_address: ContractAddress, + pub follow_timestamp: u64, + pub block_status: bool, +} + +// ************************************************************************* +// JOLT +// ************************************************************************* +#[derive(Drop, Serde, starknet::Store)] +pub struct joltData { + pub jolt_id: u256, + pub jolt_type: JoltType, + pub sender: ContractAddress, + pub recipient: ContractAddress, + pub memo: ByteArray, + pub amount: u256, + pub amount_in_usd: u256, + pub currency: JoltCurrency, + pub status: JoltStatus, + pub expiration_stamp: u64, + pub block_timestamp: u64 +} + +#[derive(Drop, Serde)] +pub struct joltParams { + pub jolt_type: JoltType, + pub recipient: ContractAddress, + pub memo: ByteArray, + pub amount: u256, + pub currency: JoltCurrency, + pub expiration_stamp: u64, + pub auto_renewal: (bool, u256), +} + +#[derive(Drop, Serde, starknet::Store)] +pub struct RenewalData { + pub renewal_duration: u256, + pub renewal_amount: u256, + pub erc20_contract_address: ContractAddress +} + +#[derive(Drop, Serde, starknet::Store, PartialEq)] +pub enum JoltCurrency { + USDT, + USDC, + ETH, + STRK +} + +#[derive(Drop, Serde, starknet::Store, PartialEq)] +pub enum JoltType { + Tip, + Transfer, + Subscription, + Request +} + +#[derive(Drop, Serde, starknet::Store, PartialEq)] +pub enum JoltStatus { + PENDING, + SUCCESSFUL, + EXPIRED, + FAILED + } \ No newline at end of file diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 088ac5a..c3f2864 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -1,5 +1,6 @@ pub mod IKarstNFT; pub mod IERC721; +pub mod IERC20; pub mod IRegistry; pub mod IProfile; pub mod IFollowNFT; @@ -7,3 +8,4 @@ pub mod IPublication; pub mod IHandle; pub mod IHandleRegistry; pub mod IHub; +pub mod IJolt; \ No newline at end of file diff --git a/src/interfaces/IERC20.cairo b/src/interfaces/IERC20.cairo new file mode 100644 index 0000000..349e00b --- /dev/null +++ b/src/interfaces/IERC20.cairo @@ -0,0 +1,20 @@ +use starknet::ContractAddress; + +// ************************************************************************* +// IERC20 +// ************************************************************************* +#[starknet::interface] +pub trait IERC20 { + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; +} diff --git a/src/interfaces/IJolt.cairo b/src/interfaces/IJolt.cairo new file mode 100644 index 0000000..46bdb9b --- /dev/null +++ b/src/interfaces/IJolt.cairo @@ -0,0 +1,18 @@ +use starknet::ContractAddress; +use karst::base::constants::types::{joltParams, joltData}; + +#[starknet::interface] +pub trait IJolt { + // ************************************************************************* + // EXTERNALS + // ************************************************************************* + fn jolt(ref self: TState, jolt_params: joltParams) -> bool; + fn set_fee_address(ref self: TState, _fee_address: ContractAddress); + fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; + // ************************************************************************* + // GETTERS + // ************************************************************************* + fn get_jolt(self: @TState, jolt_id: u256) -> joltData; + fn total_jolts_received(self: @TState, profile: ContractAddress) -> u256; + fn get_fee_address(self: @TState) -> ContractAddress; +} \ No newline at end of file diff --git a/src/jolt.cairo b/src/jolt.cairo new file mode 100644 index 0000000..e28a9e2 --- /dev/null +++ b/src/jolt.cairo @@ -0,0 +1 @@ +pub mod jolt; \ No newline at end of file diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo new file mode 100644 index 0000000..ea58abb --- /dev/null +++ b/src/jolt/jolt.cairo @@ -0,0 +1,459 @@ +#[starknet::contract] +pub mod Jolt { + // ************************************************************************* + // IMPORTS + // ************************************************************************* + use core::num::traits::zero::Zero; + use core::hash::HashStateTrait; + use core::pedersen::PedersenTrait; + use starknet::{ + ContractAddress, ClassHash, get_caller_address, get_contract_address, get_block_timestamp, get_tx_info, contract_address_const, + storage::{ + StoragePointerWriteAccess, StoragePointerReadAccess, Map, StorageMapReadAccess, StorageMapWriteAccess + } + }; + use karst::base::{ + constants::errors::Errors, + constants::types::{joltData, joltParams, JoltType, JoltCurrency, JoltStatus, RenewalData}, + constants::contract_addresses::Addresses, + }; + use karst::interfaces::{ + IJolt::IJolt, + IERC20::{IERC20Dispatcher, IERC20DispatcherTrait} + }; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::UpgradeableComponent; + use openzeppelin::upgrades::interface::IUpgradeable; + + // ************************************************************************* + // COMPONENTS + // ************************************************************************* + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + // ************************************************************************* + // STORAGE + // ************************************************************************* + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + fee_address: ContractAddress, + jolt: Map::, + total_jolts: Map::, + renewals: Map::<(ContractAddress, u256), RenewalData>, + } + + // ************************************************************************* + // EVENTS + // ************************************************************************* + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + Jolted: Jolted + } + + #[derive(Drop, starknet::Event)] + pub struct Jolted { + jolt_id: u256, + jolt_type: felt252, + sender: ContractAddress, + recipient: ContractAddress, + block_timestamp: u64, + } + + const MAX_TIP: u256 = 1000; + + // ************************************************************************* + // CONSTRUCTOR + // ************************************************************************* + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.ownable.initializer(owner); + } + + + #[abi(embed_v0)] + impl JoltImpl of IJolt { + // ************************************************************************* + // EXTERNALS + // ************************************************************************* + fn jolt(ref self: ContractState, jolt_params: joltParams) -> bool { + let sender = get_caller_address(); + let tx_info = get_tx_info().unbox(); + let tx_timestamp = get_block_timestamp(); + + // generate jolt_id + let jolt_hash = PedersenTrait::new(0) + .update(jolt_params.recipient.into()) + .update(jolt_params.amount.low.into()) + .update(jolt_params.amount.high.into()) + .update(tx_info.nonce) + .update(4) + .finalize(); + + let jolt_id: u256 = jolt_hash.try_into().unwrap(); + + // get the appropriate contract address + let mut erc20_contract_address: ContractAddress = contract_address_const::<0>(); + let jolt_currency = @jolt_params.currency; + + match jolt_currency { + JoltCurrency::USDT => erc20_contract_address = Addresses::USDT.try_into().unwrap(), + JoltCurrency::USDC => erc20_contract_address = Addresses::USDC.try_into().unwrap(), + JoltCurrency::ETH => erc20_contract_address = Addresses::ETH.try_into().unwrap(), + JoltCurrency::STRK => erc20_contract_address = Addresses::STRK.try_into().unwrap() + }; + + // jolt + let mut tx_status = false; + let mut jolt_status = JoltStatus::PENDING; + + let jolt_type = @jolt_params.jolt_type; + match jolt_type { + JoltType::Tip => { + let (_tx_status, _jolt_status) = self._tip( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + erc20_contract_address + ); + + tx_status = _tx_status; + jolt_status = _jolt_status; + }, + JoltType::Transfer => { + let (_tx_status, _jolt_status) = self._transfer( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + erc20_contract_address + ); + + tx_status = _tx_status; + jolt_status = _jolt_status; + }, + JoltType::Subscription => { + // check that currency is a stable + if(jolt_currency != @JoltCurrency::USDT || jolt_currency != @JoltCurrency::USDC) { + panic!("Karst: subscription can only be done with stables!"); + } + + let (_tx_status, _jolt_status)= self._subscribe( + jolt_id, + sender, + jolt_params.amount, + jolt_params.auto_renewal, + erc20_contract_address + ); + + tx_status = _tx_status; + jolt_status = _jolt_status; + }, + JoltType::Request => { + let (_tx_status, _jolt_status) = self._request( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + erc20_contract_address + ); + + tx_status = _tx_status; + jolt_status = _jolt_status; + } + }; + + // get jolt amount in usd + let mut amount_in_usd = self._get_usd_equiv(jolt_params.amount, erc20_contract_address); + + // prefill tx data + let jolt_data = joltData { + jolt_id: jolt_id, + jolt_type: jolt_params.jolt_type, + sender: sender, + recipient: jolt_params.recipient, + memo: jolt_params.memo, + amount: jolt_params.amount, + amount_in_usd: amount_in_usd, + currency: jolt_params.currency, + status: jolt_status, + expiration_stamp: jolt_params.expiration_stamp, + block_timestamp: tx_timestamp + }; + let total_jolts_recieved = self.total_jolts.read(jolt_params.recipient) + amount_in_usd; + + // write to storage + self.jolt.write(jolt_id, jolt_data); + self.total_jolts.write(jolt_params.recipient, total_jolts_recieved); + + return tx_status; + } + + fn set_fee_address(ref self: ContractState, _fee_address: ContractAddress) { + self.ownable.assert_only_owner(); + self.fee_address.write(_fee_address); + } + + fn auto_renew(ref self: ContractState, profile: ContractAddress, renewal_id: u256) -> bool { + self._auto_renew(profile, renewal_id) + } + + // ************************************************************************* + // GETTERS + // ************************************************************************* + fn get_jolt(self: @ContractState, jolt_id: u256) -> joltData { + self.jolt.read(jolt_id) + } + + fn total_jolts_received(self: @ContractState, profile: ContractAddress) -> u256 { + self.total_jolts.read(profile) + } + + fn get_fee_address(self: @ContractState) -> ContractAddress { + self.fee_address.read() + } + } + + // ************************************************************************* + // UPGRADEABLE IMPL + // ************************************************************************* + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn _tip( + ref self: ContractState, + jolt_id: u256, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + erc20_contract_address: ContractAddress + ) -> (bool, JoltStatus) { + // check that user is not self-tipping or tipping a non-existent address + assert(sender != recipient, Errors::SELF_TIPPING); + assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); + + // check that tip does not exceed maximum tip + let tipped_amount = self._get_usd_equiv(amount, erc20_contract_address); + assert(tipped_amount <= MAX_TIP, Errors::MAX_TIPPING); + + // tip user + let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + dispatcher.transfer_from(sender, recipient, amount); + + // emit event + self.emit( + Jolted { + jolt_id, + jolt_type: 'TIP', + sender, + recipient: recipient, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + (true, JoltStatus::SUCCESSFUL) + } + + fn _transfer( + ref self: ContractState, + jolt_id: u256, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + erc20_contract_address: ContractAddress + ) -> (bool, JoltStatus) { + // check that user is not transferring to self or to a non-existent address + assert(sender != recipient, Errors::SELF_TRANSFER); + assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); + + // transfer to recipient + let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + dispatcher.transfer_from(sender, recipient, amount); + + // emit event + self.emit( + Jolted { + jolt_id, + jolt_type: 'TRANSFER', + sender, + recipient: recipient, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + (true, JoltStatus::SUCCESSFUL) + } + + fn _subscribe( + ref self: ContractState, + jolt_id: u256, + sender: ContractAddress, + amount: u256, + auto_renewal: (bool, u256), + erc20_contract_address: ContractAddress + ) -> (bool, JoltStatus) { + let (renewal_status, renewal_duration_in_months) = auto_renewal; + let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + let this_contract = get_contract_address(); + let tx_info = get_tx_info().unbox(); + + if (renewal_status) { + // check allowances match auto-renew duration + let allowance = dispatcher.allowance(sender, this_contract); + assert(allowance == renewal_duration_in_months * amount, Errors::INSUFFICIENT_ALLOWANCE); + + // generate renewal ID + let renewal_hash = PedersenTrait::new(0) + .update(sender.into()) + .update(jolt_id.low.into()) + .update(jolt_id.high.into()) + .update(tx_info.nonce) + .update(4) + .finalize(); + + let renewal_id: u256 = renewal_hash.try_into().unwrap(); + + // write renewal details to storage + let renewal_data = RenewalData { renewal_duration: renewal_duration_in_months, renewal_amount: amount, erc20_contract_address }; + self.renewals.write((sender, renewal_id), renewal_data); + } + + // send subscription amount to fee address + let fee_address = self.fee_address.read(); + dispatcher.transfer_from(sender, fee_address, amount); + + // emit event + self.emit( + Jolted { + jolt_id, + jolt_type: 'SUBSCRIPTION', + sender, + recipient: fee_address, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + (true, JoltStatus::SUCCESSFUL) + } + + // TODO + fn _request( + ref self: ContractState, + jolt_id: u256, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + erc20_contract_address: ContractAddress + ) -> (bool, JoltStatus) { + (true, JoltStatus::SUCCESSFUL) + } + + fn _auto_renew( + ref self: ContractState, + sender: ContractAddress, + renewal_id: u256 + ) -> bool { + let tx_info = get_tx_info().unbox(); + let amount = self.renewals.read((sender, renewal_id)).renewal_amount; + let duration = self.renewals.read((sender, renewal_id)).renewal_duration; + let erc20_contract_address = self.renewals.read((sender, renewal_id)).erc20_contract_address; + let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + // check duration is greater than 0 else shouldn't auto renew + assert(duration > 0, Errors::AUTO_RENEW_DURATION_ENDED); + + // send subscription amount to fee address + let fee_address = self.fee_address.read(); + dispatcher.transfer_from(sender, fee_address, amount); + + // generate jolt_id + let jolt_hash = PedersenTrait::new(0) + .update(fee_address.into()) + .update(amount.low.into()) + .update(amount.high.into()) + .update(tx_info.nonce) + .update(4) + .finalize(); + + let jolt_id: u256 = jolt_hash.try_into().unwrap(); + + // reduce duration by one month + let renewal_data = RenewalData { renewal_duration: duration - 1, renewal_amount: amount, erc20_contract_address }; + self.renewals.write((sender, renewal_id), renewal_data); + + // get currency + let mut currency = JoltCurrency::USDT; + let erc20_name = dispatcher.name(); + if (erc20_name == "USDC") { + currency = JoltCurrency::USDC; + } + + // prefill tx data + let jolt_data = joltData { + jolt_id: jolt_id, + jolt_type: JoltType::Subscription, + sender: sender, + recipient: fee_address, + memo: "auto renew successful", + amount: amount, + amount_in_usd: amount, + currency: currency, + status: JoltStatus::SUCCESSFUL, + expiration_stamp: 0, + block_timestamp: get_block_timestamp() + }; + let total_jolts_recieved = self.total_jolts.read(fee_address) + amount; + + // write to storage + self.jolt.write(jolt_id, jolt_data); + self.total_jolts.write(fee_address, total_jolts_recieved); + + // emit event + self.emit( + Jolted { + jolt_id, + jolt_type: 'SUBSCRIPTION', + sender, + recipient: fee_address, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + return true; + } + + // TODO: convert jolt amount to usd equivalent + fn _get_usd_equiv(ref self: ContractState, amount: u256, erc20_contract_address: ContractAddress) -> u256 { + 100 + } + } +} + diff --git a/src/lib.cairo b/src/lib.cairo index c3f5301..75f2b9f 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -8,3 +8,4 @@ pub mod publication; pub mod namespaces; pub mod presets; pub mod hub; +pub mod jolt; \ No newline at end of file From f24d0b7f11babf9ef0ac4a2fedffa04fa27e5239 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Wed, 2 Oct 2024 11:41:26 +0100 Subject: [PATCH 02/11] feat: jolt request --- src/base/constants/errors.cairo | 4 ++ src/base/constants/types.cairo | 1 + src/interfaces/IJolt.cairo | 1 + src/jolt/jolt.cairo | 111 ++++++++++++++++++++++++++++++-- 4 files changed, 111 insertions(+), 6 deletions(-) diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index cac5131..ecdceb2 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -22,6 +22,10 @@ pub mod Errors { pub const SELF_TIPPING: felt252 = 'Karst: self-tip forbidden!'; pub const MAX_TIPPING: felt252 = 'Karst: exceeds max tipping!'; pub const SELF_TRANSFER: felt252 = 'Karst: self-transfer forbidden!'; + pub const SELF_REQUEST: felt252 = 'Karst: self-request forbidden!'; pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Karst: insufficient allowance!'; pub const AUTO_RENEW_DURATION_ENDED: felt252 = 'Karst: auto renew ended!'; + pub const INVALID_JOLT: felt252 = 'Karst: invalid jolt!'; + pub const EXPIRED_JOLT: felt252 = 'Karst: jolt is expired!'; + pub const INVALID_JOLT_RECIPIENT: felt252 = 'Karst: invalid request recipient!'; } diff --git a/src/base/constants/types.cairo b/src/base/constants/types.cairo index 70a4494..b0b22db 100644 --- a/src/base/constants/types.cairo +++ b/src/base/constants/types.cairo @@ -230,5 +230,6 @@ pub enum JoltStatus { PENDING, SUCCESSFUL, EXPIRED, + REJECTED, FAILED } \ No newline at end of file diff --git a/src/interfaces/IJolt.cairo b/src/interfaces/IJolt.cairo index 46bdb9b..bd861eb 100644 --- a/src/interfaces/IJolt.cairo +++ b/src/interfaces/IJolt.cairo @@ -9,6 +9,7 @@ pub trait IJolt { fn jolt(ref self: TState, jolt_params: joltParams) -> bool; fn set_fee_address(ref self: TState, _fee_address: ContractAddress); fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; + fn fullfill_request(ref self: TState, jolt_id: u256, sender: ContractAddress) -> bool; // ************************************************************************* // GETTERS // ************************************************************************* diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index ea58abb..029ab1c 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -64,7 +64,9 @@ pub mod Jolt { OwnableEvent: OwnableComponent::Event, #[flat] UpgradeableEvent: UpgradeableComponent::Event, - Jolted: Jolted + Jolted: Jolted, + JoltRequested: JoltRequested, + JoltRequestFullfilled: JoltRequestFullfilled, } #[derive(Drop, starknet::Event)] @@ -76,6 +78,26 @@ pub mod Jolt { block_timestamp: u64, } + #[derive(Drop, starknet::Event)] + pub struct JoltRequested { + jolt_id: u256, + jolt_type: felt252, + sender: ContractAddress, + recipient: ContractAddress, + expiration_timestamp: u64, + block_timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct JoltRequestFullfilled { + jolt_id: u256, + jolt_type: felt252, + sender: ContractAddress, + recipient: ContractAddress, + expiration_timestamp: u64, + block_timestamp: u64, + } + const MAX_TIP: u256 = 1000; // ************************************************************************* @@ -171,7 +193,8 @@ pub mod Jolt { jolt_id, sender, jolt_params.recipient, - jolt_params.amount, + jolt_params.amount, + jolt_params.expiration_stamp, erc20_contract_address ); @@ -197,11 +220,13 @@ pub mod Jolt { expiration_stamp: jolt_params.expiration_stamp, block_timestamp: tx_timestamp }; - let total_jolts_recieved = self.total_jolts.read(jolt_params.recipient) + amount_in_usd; // write to storage + if(jolt_data.status == JoltStatus::SUCCESSFUL) { + let total_jolts_recieved = self.total_jolts.read(jolt_params.recipient) + amount_in_usd; + self.total_jolts.write(jolt_params.recipient, total_jolts_recieved); + } self.jolt.write(jolt_id, jolt_data); - self.total_jolts.write(jolt_params.recipient, total_jolts_recieved); return tx_status; } @@ -211,6 +236,25 @@ pub mod Jolt { self.fee_address.write(_fee_address); } + fn fullfill_request(ref self: ContractState, jolt_id: u256, sender: ContractAddress) -> bool { + // get jolt details + let mut jolt_details = self.jolt.read(jolt_id); + + // validate request + assert(jolt_details.status == JoltStatus::PENDING, Errors::INVALID_JOLT); + assert(sender == jolt_details.recipient, Errors::INVALID_JOLT_RECIPIENT); + + // if expired write jolt status to expired and exit + if (get_block_timestamp() > jolt_details.expiration_stamp) { + let jolt_data = joltData { status: JoltStatus::EXPIRED, ..jolt_details }; + self.jolt.write(jolt_id, jolt_data); + return false; + } + + // else fulfill request + self._fulfill_request(jolt_id, sender, jolt_details) + } + fn auto_renew(ref self: ContractState, profile: ContractAddress, renewal_id: u256) -> bool { self._auto_renew(profile, renewal_id) } @@ -363,18 +407,69 @@ pub mod Jolt { (true, JoltStatus::SUCCESSFUL) } - // TODO fn _request( ref self: ContractState, jolt_id: u256, sender: ContractAddress, recipient: ContractAddress, amount: u256, + expiration_timestamp: u64, erc20_contract_address: ContractAddress ) -> (bool, JoltStatus) { - (true, JoltStatus::SUCCESSFUL) + // check that user is not requesting to self or to a non-existent address + assert(sender != recipient, Errors::SELF_REQUEST); + assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); + + // emit event + self.emit( + JoltRequested { + jolt_id, + jolt_type: 'REQUEST', + sender, + recipient: recipient, + expiration_timestamp, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + (true, JoltStatus::PENDING) } + fn _fulfill_request(ref self: ContractState, jolt_id: u256, sender: ContractAddress, jolt_details: joltData) -> bool { + // get the appropriate contract address + let jolt_currency = @jolt_details.currency; + let mut erc20_contract_address: ContractAddress = contract_address_const::<0>(); + match jolt_currency { + JoltCurrency::USDT => erc20_contract_address = Addresses::USDT.try_into().unwrap(), + JoltCurrency::USDC => erc20_contract_address = Addresses::USDC.try_into().unwrap(), + JoltCurrency::ETH => erc20_contract_address = Addresses::ETH.try_into().unwrap(), + JoltCurrency::STRK => erc20_contract_address = Addresses::STRK.try_into().unwrap() + }; + + // transfer request amount + let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + dispatcher.transfer_from(sender, jolt_details.sender, jolt_details.amount); + + // update jolt details + let jolt_data = joltData { status: JoltStatus::SUCCESSFUL, ..jolt_details }; + self.jolt.write(jolt_id, jolt_data); + + // emit events + self.emit( + JoltRequestFullfilled { + jolt_id, + jolt_type: 'REQUEST', + sender, + recipient: jolt_details.sender, + expiration_timestamp: jolt_details.expiration_stamp, + block_timestamp: get_block_timestamp(), + } + ); + + return true; + } + fn _auto_renew( ref self: ContractState, sender: ContractAddress, @@ -457,3 +552,7 @@ pub mod Jolt { } } +// TODO: +// 1. implement request +// 2. implement fulfill request +// 3. integrate pragma oracle \ No newline at end of file From 1036372b032cb34794fd61e7f486206c04bf5ab5 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Wed, 2 Oct 2024 11:51:08 +0100 Subject: [PATCH 03/11] chore: scarb fmt --- src/base/constants.cairo | 2 +- src/base/constants/errors.cairo | 2 +- src/base/constants/types.cairo | 10 +- src/interfaces.cairo | 2 +- src/interfaces/IJolt.cairo | 20 +-- src/jolt.cairo | 2 +- src/jolt/jolt.cairo | 307 +++++++++++++++++--------------- src/lib.cairo | 2 +- 8 files changed, 183 insertions(+), 164 deletions(-) diff --git a/src/base/constants.cairo b/src/base/constants.cairo index 8b8b437..c247388 100644 --- a/src/base/constants.cairo +++ b/src/base/constants.cairo @@ -1,3 +1,3 @@ pub mod errors; pub mod types; -pub mod contract_addresses; \ No newline at end of file +pub mod contract_addresses; diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index ecdceb2..3e2662f 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -27,5 +27,5 @@ pub mod Errors { pub const AUTO_RENEW_DURATION_ENDED: felt252 = 'Karst: auto renew ended!'; pub const INVALID_JOLT: felt252 = 'Karst: invalid jolt!'; pub const EXPIRED_JOLT: felt252 = 'Karst: jolt is expired!'; - pub const INVALID_JOLT_RECIPIENT: felt252 = 'Karst: invalid request recipient!'; + pub const INVALID_JOLT_RECIPIENT: felt252 = 'Karst: not request recipient!'; } diff --git a/src/base/constants/types.cairo b/src/base/constants/types.cairo index b0b22db..ca9ee87 100644 --- a/src/base/constants/types.cairo +++ b/src/base/constants/types.cairo @@ -219,10 +219,10 @@ pub enum JoltCurrency { #[derive(Drop, Serde, starknet::Store, PartialEq)] pub enum JoltType { - Tip, - Transfer, - Subscription, - Request + Tip, + Transfer, + Subscription, + Request } #[derive(Drop, Serde, starknet::Store, PartialEq)] @@ -232,4 +232,4 @@ pub enum JoltStatus { EXPIRED, REJECTED, FAILED - } \ No newline at end of file +} diff --git a/src/interfaces.cairo b/src/interfaces.cairo index c3f2864..50efc21 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -8,4 +8,4 @@ pub mod IPublication; pub mod IHandle; pub mod IHandleRegistry; pub mod IHub; -pub mod IJolt; \ No newline at end of file +pub mod IJolt; diff --git a/src/interfaces/IJolt.cairo b/src/interfaces/IJolt.cairo index bd861eb..099db2d 100644 --- a/src/interfaces/IJolt.cairo +++ b/src/interfaces/IJolt.cairo @@ -3,17 +3,17 @@ use karst::base::constants::types::{joltParams, joltData}; #[starknet::interface] pub trait IJolt { - // ************************************************************************* + // ************************************************************************* // EXTERNALS // ************************************************************************* - fn jolt(ref self: TState, jolt_params: joltParams) -> bool; - fn set_fee_address(ref self: TState, _fee_address: ContractAddress); - fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; - fn fullfill_request(ref self: TState, jolt_id: u256, sender: ContractAddress) -> bool; - // ************************************************************************* + fn jolt(ref self: TState, jolt_params: joltParams) -> bool; + fn set_fee_address(ref self: TState, _fee_address: ContractAddress); + fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; + fn fullfill_request(ref self: TState, jolt_id: u256, sender: ContractAddress) -> bool; + // ************************************************************************* // GETTERS // ************************************************************************* - fn get_jolt(self: @TState, jolt_id: u256) -> joltData; - fn total_jolts_received(self: @TState, profile: ContractAddress) -> u256; - fn get_fee_address(self: @TState) -> ContractAddress; -} \ No newline at end of file + fn get_jolt(self: @TState, jolt_id: u256) -> joltData; + fn total_jolts_received(self: @TState, profile: ContractAddress) -> u256; + fn get_fee_address(self: @TState) -> ContractAddress; +} diff --git a/src/jolt.cairo b/src/jolt.cairo index e28a9e2..5e01eb0 100644 --- a/src/jolt.cairo +++ b/src/jolt.cairo @@ -1 +1 @@ -pub mod jolt; \ No newline at end of file +pub mod jolt; diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index 029ab1c..91321b1 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -7,20 +7,19 @@ pub mod Jolt { use core::hash::HashStateTrait; use core::pedersen::PedersenTrait; use starknet::{ - ContractAddress, ClassHash, get_caller_address, get_contract_address, get_block_timestamp, get_tx_info, contract_address_const, + ContractAddress, ClassHash, get_caller_address, get_contract_address, get_block_timestamp, + get_tx_info, contract_address_const, storage::{ - StoragePointerWriteAccess, StoragePointerReadAccess, Map, StorageMapReadAccess, StorageMapWriteAccess + StoragePointerWriteAccess, StoragePointerReadAccess, Map, StorageMapReadAccess, + StorageMapWriteAccess } }; use karst::base::{ - constants::errors::Errors, + constants::errors::Errors, constants::types::{joltData, joltParams, JoltType, JoltCurrency, JoltStatus, RenewalData}, constants::contract_addresses::Addresses, }; - use karst::interfaces::{ - IJolt::IJolt, - IERC20::{IERC20Dispatcher, IERC20DispatcherTrait} - }; + use karst::interfaces::{IJolt::IJolt, IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}}; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::upgrades::UpgradeableComponent; use openzeppelin::upgrades::interface::IUpgradeable; @@ -121,12 +120,12 @@ pub mod Jolt { // generate jolt_id let jolt_hash = PedersenTrait::new(0) - .update(jolt_params.recipient.into()) - .update(jolt_params.amount.low.into()) - .update(jolt_params.amount.high.into()) - .update(tx_info.nonce) - .update(4) - .finalize(); + .update(jolt_params.recipient.into()) + .update(jolt_params.amount.low.into()) + .update(jolt_params.amount.high.into()) + .update(tx_info.nonce) + .update(4) + .finalize(); let jolt_id: u256 = jolt_hash.try_into().unwrap(); @@ -148,55 +147,60 @@ pub mod Jolt { let jolt_type = @jolt_params.jolt_type; match jolt_type { JoltType::Tip => { - let (_tx_status, _jolt_status) = self._tip( - jolt_id, - sender, - jolt_params.recipient, - jolt_params.amount, - erc20_contract_address - ); + let (_tx_status, _jolt_status) = self + ._tip( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + erc20_contract_address + ); tx_status = _tx_status; jolt_status = _jolt_status; }, JoltType::Transfer => { - let (_tx_status, _jolt_status) = self._transfer( - jolt_id, - sender, - jolt_params.recipient, - jolt_params.amount, - erc20_contract_address - ); + let (_tx_status, _jolt_status) = self + ._transfer( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + erc20_contract_address + ); tx_status = _tx_status; jolt_status = _jolt_status; }, JoltType::Subscription => { // check that currency is a stable - if(jolt_currency != @JoltCurrency::USDT || jolt_currency != @JoltCurrency::USDC) { + if (jolt_currency != @JoltCurrency::USDT + || jolt_currency != @JoltCurrency::USDC) { panic!("Karst: subscription can only be done with stables!"); } - let (_tx_status, _jolt_status)= self._subscribe( - jolt_id, - sender, - jolt_params.amount, - jolt_params.auto_renewal, - erc20_contract_address - ); + let (_tx_status, _jolt_status) = self + ._subscribe( + jolt_id, + sender, + jolt_params.amount, + jolt_params.auto_renewal, + erc20_contract_address + ); tx_status = _tx_status; jolt_status = _jolt_status; }, JoltType::Request => { - let (_tx_status, _jolt_status) = self._request( - jolt_id, - sender, - jolt_params.recipient, - jolt_params.amount, - jolt_params.expiration_stamp, - erc20_contract_address - ); + let (_tx_status, _jolt_status) = self + ._request( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + jolt_params.expiration_stamp, + erc20_contract_address + ); tx_status = _tx_status; jolt_status = _jolt_status; @@ -222,8 +226,9 @@ pub mod Jolt { }; // write to storage - if(jolt_data.status == JoltStatus::SUCCESSFUL) { - let total_jolts_recieved = self.total_jolts.read(jolt_params.recipient) + amount_in_usd; + if (jolt_data.status == JoltStatus::SUCCESSFUL) { + let total_jolts_recieved = self.total_jolts.read(jolt_params.recipient) + + amount_in_usd; self.total_jolts.write(jolt_params.recipient, total_jolts_recieved); } self.jolt.write(jolt_id, jolt_data); @@ -236,7 +241,9 @@ pub mod Jolt { self.fee_address.write(_fee_address); } - fn fullfill_request(ref self: ContractState, jolt_id: u256, sender: ContractAddress) -> bool { + fn fullfill_request( + ref self: ContractState, jolt_id: u256, sender: ContractAddress + ) -> bool { // get jolt details let mut jolt_details = self.jolt.read(jolt_id); @@ -258,7 +265,7 @@ pub mod Jolt { fn auto_renew(ref self: ContractState, profile: ContractAddress, renewal_id: u256) -> bool { self._auto_renew(profile, renewal_id) } - + // ************************************************************************* // GETTERS // ************************************************************************* @@ -289,9 +296,9 @@ pub mod Jolt { #[generate_trait] impl Private of PrivateTrait { fn _tip( - ref self: ContractState, - jolt_id: u256, - sender: ContractAddress, + ref self: ContractState, + jolt_id: u256, + sender: ContractAddress, recipient: ContractAddress, amount: u256, erc20_contract_address: ContractAddress @@ -309,24 +316,25 @@ pub mod Jolt { dispatcher.transfer_from(sender, recipient, amount); // emit event - self.emit( - Jolted { - jolt_id, - jolt_type: 'TIP', - sender, - recipient: recipient, - block_timestamp: get_block_timestamp(), - } - ); + self + .emit( + Jolted { + jolt_id, + jolt_type: 'TIP', + sender, + recipient: recipient, + block_timestamp: get_block_timestamp(), + } + ); // return txn status (true, JoltStatus::SUCCESSFUL) } fn _transfer( - ref self: ContractState, - jolt_id: u256, - sender: ContractAddress, + ref self: ContractState, + jolt_id: u256, + sender: ContractAddress, recipient: ContractAddress, amount: u256, erc20_contract_address: ContractAddress @@ -340,23 +348,24 @@ pub mod Jolt { dispatcher.transfer_from(sender, recipient, amount); // emit event - self.emit( - Jolted { - jolt_id, - jolt_type: 'TRANSFER', - sender, - recipient: recipient, - block_timestamp: get_block_timestamp(), - } - ); + self + .emit( + Jolted { + jolt_id, + jolt_type: 'TRANSFER', + sender, + recipient: recipient, + block_timestamp: get_block_timestamp(), + } + ); // return txn status (true, JoltStatus::SUCCESSFUL) } fn _subscribe( - ref self: ContractState, - jolt_id: u256, + ref self: ContractState, + jolt_id: u256, sender: ContractAddress, amount: u256, auto_renewal: (bool, u256), @@ -370,21 +379,27 @@ pub mod Jolt { if (renewal_status) { // check allowances match auto-renew duration let allowance = dispatcher.allowance(sender, this_contract); - assert(allowance == renewal_duration_in_months * amount, Errors::INSUFFICIENT_ALLOWANCE); + assert( + allowance == renewal_duration_in_months * amount, Errors::INSUFFICIENT_ALLOWANCE + ); // generate renewal ID let renewal_hash = PedersenTrait::new(0) - .update(sender.into()) - .update(jolt_id.low.into()) - .update(jolt_id.high.into()) - .update(tx_info.nonce) - .update(4) - .finalize(); + .update(sender.into()) + .update(jolt_id.low.into()) + .update(jolt_id.high.into()) + .update(tx_info.nonce) + .update(4) + .finalize(); let renewal_id: u256 = renewal_hash.try_into().unwrap(); // write renewal details to storage - let renewal_data = RenewalData { renewal_duration: renewal_duration_in_months, renewal_amount: amount, erc20_contract_address }; + let renewal_data = RenewalData { + renewal_duration: renewal_duration_in_months, + renewal_amount: amount, + erc20_contract_address + }; self.renewals.write((sender, renewal_id), renewal_data); } @@ -393,24 +408,25 @@ pub mod Jolt { dispatcher.transfer_from(sender, fee_address, amount); // emit event - self.emit( - Jolted { - jolt_id, - jolt_type: 'SUBSCRIPTION', - sender, - recipient: fee_address, - block_timestamp: get_block_timestamp(), - } - ); + self + .emit( + Jolted { + jolt_id, + jolt_type: 'SUBSCRIPTION', + sender, + recipient: fee_address, + block_timestamp: get_block_timestamp(), + } + ); // return txn status (true, JoltStatus::SUCCESSFUL) } fn _request( - ref self: ContractState, - jolt_id: u256, - sender: ContractAddress, + ref self: ContractState, + jolt_id: u256, + sender: ContractAddress, recipient: ContractAddress, amount: u256, expiration_timestamp: u64, @@ -421,22 +437,25 @@ pub mod Jolt { assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); // emit event - self.emit( - JoltRequested { - jolt_id, - jolt_type: 'REQUEST', - sender, - recipient: recipient, - expiration_timestamp, - block_timestamp: get_block_timestamp(), - } - ); + self + .emit( + JoltRequested { + jolt_id, + jolt_type: 'REQUEST', + sender, + recipient: recipient, + expiration_timestamp, + block_timestamp: get_block_timestamp(), + } + ); // return txn status (true, JoltStatus::PENDING) } - fn _fulfill_request(ref self: ContractState, jolt_id: u256, sender: ContractAddress, jolt_details: joltData) -> bool { + fn _fulfill_request( + ref self: ContractState, jolt_id: u256, sender: ContractAddress, jolt_details: joltData + ) -> bool { // get the appropriate contract address let jolt_currency = @jolt_details.currency; let mut erc20_contract_address: ContractAddress = contract_address_const::<0>(); @@ -446,7 +465,7 @@ pub mod Jolt { JoltCurrency::ETH => erc20_contract_address = Addresses::ETH.try_into().unwrap(), JoltCurrency::STRK => erc20_contract_address = Addresses::STRK.try_into().unwrap() }; - + // transfer request amount let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; dispatcher.transfer_from(sender, jolt_details.sender, jolt_details.amount); @@ -456,29 +475,29 @@ pub mod Jolt { self.jolt.write(jolt_id, jolt_data); // emit events - self.emit( - JoltRequestFullfilled { - jolt_id, - jolt_type: 'REQUEST', - sender, - recipient: jolt_details.sender, - expiration_timestamp: jolt_details.expiration_stamp, - block_timestamp: get_block_timestamp(), - } - ); + self + .emit( + JoltRequestFullfilled { + jolt_id, + jolt_type: 'REQUEST', + sender, + recipient: jolt_details.sender, + expiration_timestamp: jolt_details.expiration_stamp, + block_timestamp: get_block_timestamp(), + } + ); return true; - } + } - fn _auto_renew( - ref self: ContractState, - sender: ContractAddress, - renewal_id: u256 - ) -> bool { + fn _auto_renew(ref self: ContractState, sender: ContractAddress, renewal_id: u256) -> bool { let tx_info = get_tx_info().unbox(); let amount = self.renewals.read((sender, renewal_id)).renewal_amount; let duration = self.renewals.read((sender, renewal_id)).renewal_duration; - let erc20_contract_address = self.renewals.read((sender, renewal_id)).erc20_contract_address; + let erc20_contract_address = self + .renewals + .read((sender, renewal_id)) + .erc20_contract_address; let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; // check duration is greater than 0 else shouldn't auto renew @@ -490,17 +509,19 @@ pub mod Jolt { // generate jolt_id let jolt_hash = PedersenTrait::new(0) - .update(fee_address.into()) - .update(amount.low.into()) - .update(amount.high.into()) - .update(tx_info.nonce) - .update(4) - .finalize(); + .update(fee_address.into()) + .update(amount.low.into()) + .update(amount.high.into()) + .update(tx_info.nonce) + .update(4) + .finalize(); let jolt_id: u256 = jolt_hash.try_into().unwrap(); // reduce duration by one month - let renewal_data = RenewalData { renewal_duration: duration - 1, renewal_amount: amount, erc20_contract_address }; + let renewal_data = RenewalData { + renewal_duration: duration - 1, renewal_amount: amount, erc20_contract_address + }; self.renewals.write((sender, renewal_id), renewal_data); // get currency @@ -531,28 +552,26 @@ pub mod Jolt { self.total_jolts.write(fee_address, total_jolts_recieved); // emit event - self.emit( - Jolted { - jolt_id, - jolt_type: 'SUBSCRIPTION', - sender, - recipient: fee_address, - block_timestamp: get_block_timestamp(), - } - ); + self + .emit( + Jolted { + jolt_id, + jolt_type: 'SUBSCRIPTION', + sender, + recipient: fee_address, + block_timestamp: get_block_timestamp(), + } + ); // return txn status return true; } // TODO: convert jolt amount to usd equivalent - fn _get_usd_equiv(ref self: ContractState, amount: u256, erc20_contract_address: ContractAddress) -> u256 { + fn _get_usd_equiv( + ref self: ContractState, amount: u256, erc20_contract_address: ContractAddress + ) -> u256 { 100 } } } - -// TODO: -// 1. implement request -// 2. implement fulfill request -// 3. integrate pragma oracle \ No newline at end of file diff --git a/src/lib.cairo b/src/lib.cairo index 75f2b9f..98d4f82 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -8,4 +8,4 @@ pub mod publication; pub mod namespaces; pub mod presets; pub mod hub; -pub mod jolt; \ No newline at end of file +pub mod jolt; From f497b7f9cca5c57dd0b8a63c34ff12c483c265b5 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Thu, 3 Oct 2024 01:24:56 +0100 Subject: [PATCH 04/11] feat: add pragma oracle --- Scarb.lock | 6 +++ Scarb.toml | 3 +- src/base/constants/contract_addresses.cairo | 8 +++- src/jolt/jolt.cairo | 41 +++++++++++++++++++-- tests/test_follownft.cairo | 4 +- tests/test_handle.cairo | 2 +- tests/test_hub.cairo | 9 ----- tests/test_jolt.cairo | 32 ++++++++++++++++ tests/test_karstnft.cairo | 5 +-- tests/test_profile.cairo | 18 +-------- tests/test_publication.cairo | 17 ++------- 11 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 tests/test_jolt.cairo diff --git a/Scarb.lock b/Scarb.lock index 133b44a..ae55d89 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -59,6 +59,7 @@ version = "0.1.0" dependencies = [ "alexandria_bytes", "openzeppelin", + "pragma_lib", "snforge_std", "token_bound_accounts", ] @@ -165,6 +166,11 @@ name = "openzeppelin_utils" version = "0.17.0" source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289" +[[package]] +name = "pragma_lib" +version = "1.0.0" +source = "git+https://github.com/astraly-labs/pragma-lib#86d7ccdc15b349b8b48d9796fc8464c947bea6e1" + [[package]] name = "snforge_scarb_plugin" version = "0.31.0" diff --git a/Scarb.toml b/Scarb.toml index 575ff04..01daf6a 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -14,6 +14,7 @@ starknet = "2.8.2" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.17.0" } token_bound_accounts= { git = "https://github.com/Starknet-Africa-Edu/TBA", tag = "v0.3.0" } alexandria_bytes = { git = "https://github.com/keep-starknet-strange/alexandria.git" } +pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" } [dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.31.0" } @@ -25,4 +26,4 @@ url= "https://starknet-sepolia.public.blastapi.io" [[target.starknet-contract]] casm = true build-external-contracts = ["token_bound_accounts::presets::account::Account"] -allowed-libfuncs-list.name = "experimental" \ No newline at end of file +allowed-libfuncs-list.name = "experimental" diff --git a/src/base/constants/contract_addresses.cairo b/src/base/constants/contract_addresses.cairo index deed7c7..9d5ce66 100644 --- a/src/base/constants/contract_addresses.cairo +++ b/src/base/constants/contract_addresses.cairo @@ -4,6 +4,10 @@ pub mod Addresses { pub const ETH: felt252 = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; pub const STRK: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; - pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; - pub const USDT: felt252 = 0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8; + pub const USDT: felt252 = 0x057e859eEE48E899CeF91cf0595661BEc0634dB7d593d98222C68Af6472e8394; + pub const USDC: felt252 = 0x053b40A647CEDfca6cA84f542A0fe36736031905A9639a7f19A3C1e66bFd5080; + pub const PRAGMA_ORACLE: felt252 = 0x36031daa264c24520b11d93af622c848b2499b66b41d611bac95e13cfca131a; + // pub const USDT: felt252 = 0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8; + // pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; + // pub const PRAGMA_ORACLE: felt252 = 0x2a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; } diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index 91321b1..861fd69 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -20,10 +20,14 @@ pub mod Jolt { constants::contract_addresses::Addresses, }; use karst::interfaces::{IJolt::IJolt, IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}}; + use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::upgrades::UpgradeableComponent; use openzeppelin::upgrades::interface::IUpgradeable; + use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; + use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; + // ************************************************************************* // COMPONENTS // ************************************************************************* @@ -293,6 +297,9 @@ pub mod Jolt { } } + // ************************************************************************* + // PRIVATE FUNCTIONS + // ************************************************************************* #[generate_trait] impl Private of PrivateTrait { fn _tip( @@ -526,12 +533,13 @@ pub mod Jolt { // get currency let mut currency = JoltCurrency::USDT; - let erc20_name = dispatcher.name(); - if (erc20_name == "USDC") { + let erc20_symbol = dispatcher.symbol(); + if (erc20_symbol == "USDC") { currency = JoltCurrency::USDC; } // prefill tx data + let amount_in_usd = self._get_usd_equiv(amount, erc20_contract_address); let jolt_data = joltData { jolt_id: jolt_id, jolt_type: JoltType::Subscription, @@ -539,7 +547,7 @@ pub mod Jolt { recipient: fee_address, memo: "auto renew successful", amount: amount, - amount_in_usd: amount, + amount_in_usd, currency: currency, status: JoltStatus::SUCCESSFUL, expiration_stamp: 0, @@ -571,7 +579,32 @@ pub mod Jolt { fn _get_usd_equiv( ref self: ContractState, amount: u256, erc20_contract_address: ContractAddress ) -> u256 { - 100 + // get oracle key + let mut KEY: felt252 = 0; + let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + let erc20_symbol = dispatcher.symbol(); + if (erc20_symbol == "ETH") { + KEY = 19514442401534788; + } + else if(erc20_symbol == "STRK") { + KEY = 6004514686061859652; + } + else if(erc20_symbol == "USDT") { + KEY = 6148333044652921668; + } + else if(erc20_symbol == "USDC") { + KEY = 6148332971638477636; + } + + let oracle_address : ContractAddress = Addresses::PRAGMA_ORACLE.try_into().unwrap(); + let price = self._get_asset_price_median(oracle_address, DataType::SpotEntry(KEY)); + price.try_into().unwrap() + } + + fn _get_asset_price_median(ref self: ContractState, oracle_address: ContractAddress, asset : DataType) -> u128 { + let oracle_dispatcher = IPragmaABIDispatcher{contract_address : oracle_address}; + let output : PragmaPricesResponse= oracle_dispatcher.get_data(asset, AggregationMode::Median(())); + return output.price; } } } diff --git a/tests/test_follownft.cairo b/tests/test_follownft.cairo index 71b9a0e..98c5ad0 100644 --- a/tests/test_follownft.cairo +++ b/tests/test_follownft.cairo @@ -2,9 +2,8 @@ // FOLLOW NFT TEST // ************************************************************************* use core::option::OptionTrait; -use core::starknet::SyscallResultTrait; use core::result::ResultTrait; -use core::traits::{TryInto, Into}; +use core::traits::TryInto; use starknet::{ContractAddress, get_block_timestamp}; use snforge_std::{ @@ -14,7 +13,6 @@ use snforge_std::{ }; use karst::interfaces::IFollowNFT::{IFollowNFTDispatcher, IFollowNFTDispatcherTrait}; -use karst::follownft::follownft::Follow; use karst::follownft::follownft::Follow::{Event as FollowEvent, Followed}; use karst::follownft::follownft::Follow::{Event as UnfollowEvent, Unfollowed}; use karst::follownft::follownft::Follow::{Event as FollowerBlockedEvent, FollowerBlocked}; diff --git a/tests/test_handle.cairo b/tests/test_handle.cairo index 3ecd0c8..dfaacf0 100644 --- a/tests/test_handle.cairo +++ b/tests/test_handle.cairo @@ -1,7 +1,7 @@ use core::option::OptionTrait; use core::starknet::SyscallResultTrait; use core::result::ResultTrait; -use core::traits::{TryInto, Into}; +use core::traits::TryInto; use starknet::{ContractAddress, get_block_timestamp}; use snforge_std::{ diff --git a/tests/test_hub.cairo b/tests/test_hub.cairo index 11accb5..cb10f4e 100644 --- a/tests/test_hub.cairo +++ b/tests/test_hub.cairo @@ -10,15 +10,6 @@ use snforge_std::{ declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, stop_cheat_caller_address }; - -use karst::hub::hub::KarstHub; -use karst::mocks::registry::Registry; -use karst::karstnft::karstnft::KarstNFT; -use karst::follownft::follownft::Follow; -use karst::namespaces::handles::Handles; -use token_bound_accounts::presets::account::Account; -use karst::namespaces::handle_registry::HandleRegistry; - use karst::interfaces::IHub::{IHubDispatcher, IHubDispatcherTrait}; use karst::interfaces::IHandle::{IHandleDispatcher, IHandleDispatcherTrait}; use karst::interfaces::IHandleRegistry::{IHandleRegistryDispatcher, IHandleRegistryDispatcherTrait}; diff --git a/tests/test_jolt.cairo b/tests/test_jolt.cairo new file mode 100644 index 0000000..a648968 --- /dev/null +++ b/tests/test_jolt.cairo @@ -0,0 +1,32 @@ +use core::traits::TryInto; +use starknet::{ContractAddress, get_block_timestamp}; +use snforge_std::{ + declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, + EventSpyAssertionsTrait, ContractClassTrait, DeclareResultTrait, start_cheat_block_timestamp, + stop_cheat_block_timestamp +}; +use karst::interfaces::IJolt::{IJoltDispatcher, IJoltDispatcherTrait}; + +const ADMIN: felt252 = 13245; + +// ************************************************************************* +// SETUP +// ************************************************************************* +fn __setup__() -> ContractAddress { + let jolt_contract = declare("Jolt").unwrap().contract_class(); + let (jolt_contract_address, _) = jolt_contract + .deploy(@array![ADMIN]) + .unwrap(); + return (jolt_contract_address); +} + +// ************************************************************************* +// TEST +// ************************************************************************* +#[test] +fn test_constructor() { + let jolt_contract_address = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let owner = dispatcher.owner(); + assert(owner == ADMIN.try_into().unwrap(), 'invalid owner!'); +} \ No newline at end of file diff --git a/tests/test_karstnft.cairo b/tests/test_karstnft.cairo index d024b0d..ed517f8 100644 --- a/tests/test_karstnft.cairo +++ b/tests/test_karstnft.cairo @@ -1,7 +1,7 @@ use core::num::traits::zero::Zero; use core::starknet::SyscallResultTrait; -use core::traits::{TryInto, Into}; -use starknet::{ContractAddress}; +use core::traits::TryInto; +use starknet::ContractAddress; use snforge_std::{ declare, start_cheat_caller_address, stop_cheat_caller_address, ContractClassTrait, @@ -11,7 +11,6 @@ use snforge_std::{ use openzeppelin::{token::erc721::interface::{ERC721ABIDispatcher, ERC721ABIDispatcherTrait}}; use karst::interfaces::IKarstNFT::{IKarstNFTDispatcher, IKarstNFTDispatcherTrait}; -use karst::base::constants::errors::Errors::ALREADY_MINTED; const ADMIN: felt252 = 'ADMIN'; const USER_ONE: felt252 = 'BOB'; diff --git a/tests/test_profile.cairo b/tests/test_profile.cairo index 7214cc7..21da092 100644 --- a/tests/test_profile.cairo +++ b/tests/test_profile.cairo @@ -3,28 +3,17 @@ use core::starknet::SyscallResultTrait; use core::result::ResultTrait; use core::traits::{TryInto, Into}; -use starknet::{ContractAddress, class_hash::ClassHash, get_block_timestamp}; +use starknet::{ContractAddress, get_block_timestamp}; use snforge_std::{ declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, - EventSpyAssertionsTrait, ContractClass, ContractClassTrait, DeclareResultTrait + EventSpyAssertionsTrait, ContractClassTrait, DeclareResultTrait }; use karst::interfaces::IKarstNFT::{IKarstNFTDispatcher, IKarstNFTDispatcherTrait}; -use karst::karstnft::karstnft::KarstNFT; -use karst::follownft::follownft::Follow; use karst::profile::profile::ProfileComponent::{Event as ProfileEvent, CreatedProfile}; -use karst::interfaces::IERC721::{IERC721Dispatcher, IERC721DispatcherTrait}; use karst::interfaces::IProfile::{IProfileDispatcher, IProfileDispatcherTrait}; -// Account -use token_bound_accounts::interfaces::IAccount::{IAccountDispatcher, IAccountDispatcherTrait}; -use token_bound_accounts::presets::account::Account; - -// Registry -use karst::mocks::registry::Registry; -use karst::interfaces::IRegistry::{IRegistryDispatcher, IRegistryDispatcherTrait}; - const HUB_ADDRESS: felt252 = 'HUB'; const USER: felt252 = 'USER1'; @@ -35,9 +24,6 @@ const USER: felt252 = 'USER1'; fn __setup__() -> (ContractAddress, ContractAddress, felt252, felt252, ContractAddress) { // deploy NFT let nft_contract = declare("KarstNFT").unwrap().contract_class(); - let names: ByteArray = "KarstNFT"; - let symbol: ByteArray = "KNFT"; - let base_uri: ByteArray = "ipfs://QmSkDCsS32eLpcymxtn1cEn7Rc5hfefLBgfvZyjaYXr4gQ/"; let mut calldata: Array = array![USER]; let (nft_contract_address, _) = nft_contract.deploy(@calldata).unwrap_syscall(); diff --git a/tests/test_publication.cairo b/tests/test_publication.cairo index 4b145dd..340c1bc 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -5,27 +5,18 @@ use core::option::OptionTrait; use core::starknet::SyscallResultTrait; use core::result::ResultTrait; use core::traits::{TryInto, Into}; -use starknet::{ContractAddress, class_hash::ClassHash, contract_address_const, get_block_timestamp}; +use starknet::{ContractAddress, get_block_timestamp}; use snforge_std::{ - declare, start_cheat_caller_address, stop_cheat_caller_address, start_cheat_transaction_hash, - start_cheat_nonce, spy_events, EventSpyAssertionsTrait, ContractClass, ContractClassTrait, - DeclareResultTrait, start_cheat_block_timestamp, stop_cheat_block_timestamp, EventSpy + declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, EventSpyAssertionsTrait, ContractClassTrait, + DeclareResultTrait, EventSpy }; use karst::publication::publication::PublicationComponent::{ Event as PublicationEvent, Post, CommentCreated, RepostCreated, Upvoted, Downvoted }; - -use token_bound_accounts::interfaces::IAccount::{IAccountDispatcher, IAccountDispatcherTrait}; -use token_bound_accounts::presets::account::Account; -use karst::mocks::registry::Registry; -use karst::interfaces::IRegistry::{IRegistryDispatcher, IRegistryDispatcherTrait}; -use karst::karstnft::karstnft::KarstNFT; -use karst::presets::publication::KarstPublication; -use karst::follownft::follownft::Follow; use karst::mocks::interfaces::IComposable::{IComposableDispatcher, IComposableDispatcherTrait}; use karst::base::constants::types::{ - PostParams, RepostParams, CommentParams, PublicationType, QuoteParams + PostParams, RepostParams, CommentParams, PublicationType }; const HUB_ADDRESS: felt252 = 'HUB'; From 6e7611b5d544791b4486f8c88dab65334d97f70d Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Thu, 3 Oct 2024 01:26:25 +0100 Subject: [PATCH 05/11] chore: update jolt fn interface --- src/interfaces/IJolt.cairo | 2 +- src/jolt/jolt.cairo | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interfaces/IJolt.cairo b/src/interfaces/IJolt.cairo index 099db2d..817070c 100644 --- a/src/interfaces/IJolt.cairo +++ b/src/interfaces/IJolt.cairo @@ -6,7 +6,7 @@ pub trait IJolt { // ************************************************************************* // EXTERNALS // ************************************************************************* - fn jolt(ref self: TState, jolt_params: joltParams) -> bool; + fn jolt(ref self: TState, jolt_params: joltParams) -> u256; fn set_fee_address(ref self: TState, _fee_address: ContractAddress); fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; fn fullfill_request(ref self: TState, jolt_id: u256, sender: ContractAddress) -> bool; diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index 861fd69..248714d 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -117,7 +117,7 @@ pub mod Jolt { // ************************************************************************* // EXTERNALS // ************************************************************************* - fn jolt(ref self: ContractState, jolt_params: joltParams) -> bool { + fn jolt(ref self: ContractState, jolt_params: joltParams) -> u256 { let sender = get_caller_address(); let tx_info = get_tx_info().unbox(); let tx_timestamp = get_block_timestamp(); @@ -237,7 +237,7 @@ pub mod Jolt { } self.jolt.write(jolt_id, jolt_data); - return tx_status; + return jolt_id; } fn set_fee_address(ref self: ContractState, _fee_address: ContractAddress) { From c184474953c18a02280fcf444969c188c3bbbb04 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Thu, 3 Oct 2024 01:38:09 +0100 Subject: [PATCH 06/11] chore: scarb fmt --- src/base/constants/contract_addresses.cairo | 8 +++++--- src/jolt/jolt.cairo | 20 ++++++++++---------- tests/test_jolt.cairo | 21 ++++++++++----------- tests/test_publication.cairo | 8 +++----- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/base/constants/contract_addresses.cairo b/src/base/constants/contract_addresses.cairo index 9d5ce66..e177e8a 100644 --- a/src/base/constants/contract_addresses.cairo +++ b/src/base/constants/contract_addresses.cairo @@ -6,8 +6,10 @@ pub mod Addresses { pub const STRK: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; pub const USDT: felt252 = 0x057e859eEE48E899CeF91cf0595661BEc0634dB7d593d98222C68Af6472e8394; pub const USDC: felt252 = 0x053b40A647CEDfca6cA84f542A0fe36736031905A9639a7f19A3C1e66bFd5080; - pub const PRAGMA_ORACLE: felt252 = 0x36031daa264c24520b11d93af622c848b2499b66b41d611bac95e13cfca131a; + pub const PRAGMA_ORACLE: felt252 = + 0x36031daa264c24520b11d93af622c848b2499b66b41d611bac95e13cfca131a; // pub const USDT: felt252 = 0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8; - // pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; - // pub const PRAGMA_ORACLE: felt252 = 0x2a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; +// pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; +// pub const PRAGMA_ORACLE: felt252 = +// 0x2a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; } diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index 248714d..0b0a036 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -585,25 +585,25 @@ pub mod Jolt { let erc20_symbol = dispatcher.symbol(); if (erc20_symbol == "ETH") { KEY = 19514442401534788; - } - else if(erc20_symbol == "STRK") { + } else if (erc20_symbol == "STRK") { KEY = 6004514686061859652; - } - else if(erc20_symbol == "USDT") { + } else if (erc20_symbol == "USDT") { KEY = 6148333044652921668; - } - else if(erc20_symbol == "USDC") { + } else if (erc20_symbol == "USDC") { KEY = 6148332971638477636; } - let oracle_address : ContractAddress = Addresses::PRAGMA_ORACLE.try_into().unwrap(); + let oracle_address: ContractAddress = Addresses::PRAGMA_ORACLE.try_into().unwrap(); let price = self._get_asset_price_median(oracle_address, DataType::SpotEntry(KEY)); price.try_into().unwrap() } - fn _get_asset_price_median(ref self: ContractState, oracle_address: ContractAddress, asset : DataType) -> u128 { - let oracle_dispatcher = IPragmaABIDispatcher{contract_address : oracle_address}; - let output : PragmaPricesResponse= oracle_dispatcher.get_data(asset, AggregationMode::Median(())); + fn _get_asset_price_median( + ref self: ContractState, oracle_address: ContractAddress, asset: DataType + ) -> u128 { + let oracle_dispatcher = IPragmaABIDispatcher { contract_address: oracle_address }; + let output: PragmaPricesResponse = oracle_dispatcher + .get_data(asset, AggregationMode::Median(())); return output.price; } } diff --git a/tests/test_jolt.cairo b/tests/test_jolt.cairo index a648968..889618c 100644 --- a/tests/test_jolt.cairo +++ b/tests/test_jolt.cairo @@ -6,27 +6,26 @@ use snforge_std::{ stop_cheat_block_timestamp }; use karst::interfaces::IJolt::{IJoltDispatcher, IJoltDispatcherTrait}; +use karst::base::{ + constants::errors::Errors, + constants::types::{joltData, joltParams, JoltType, JoltCurrency, JoltStatus, RenewalData} +}; const ADMIN: felt252 = 13245; +const ADDRESS1: felt252 = 53435; +const ADDRESS2: felt252 = 204925; +const ADDRESS3: felt252 = 249205; // ************************************************************************* // SETUP // ************************************************************************* fn __setup__() -> ContractAddress { let jolt_contract = declare("Jolt").unwrap().contract_class(); - let (jolt_contract_address, _) = jolt_contract - .deploy(@array![ADMIN]) - .unwrap(); + let (jolt_contract_address, _) = jolt_contract.deploy(@array![ADMIN]).unwrap(); return (jolt_contract_address); } - // ************************************************************************* // TEST // ************************************************************************* -#[test] -fn test_constructor() { - let jolt_contract_address = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; - let owner = dispatcher.owner(); - assert(owner == ADMIN.try_into().unwrap(), 'invalid owner!'); -} \ No newline at end of file + + diff --git a/tests/test_publication.cairo b/tests/test_publication.cairo index 340c1bc..5465eaf 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -8,16 +8,14 @@ use core::traits::{TryInto, Into}; use starknet::{ContractAddress, get_block_timestamp}; use snforge_std::{ - declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, EventSpyAssertionsTrait, ContractClassTrait, - DeclareResultTrait, EventSpy + declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, + EventSpyAssertionsTrait, ContractClassTrait, DeclareResultTrait, EventSpy }; use karst::publication::publication::PublicationComponent::{ Event as PublicationEvent, Post, CommentCreated, RepostCreated, Upvoted, Downvoted }; use karst::mocks::interfaces::IComposable::{IComposableDispatcher, IComposableDispatcherTrait}; -use karst::base::constants::types::{ - PostParams, RepostParams, CommentParams, PublicationType -}; +use karst::base::constants::types::{PostParams, RepostParams, CommentParams, PublicationType}; const HUB_ADDRESS: felt252 = 'HUB'; const USER_ONE: felt252 = 'BOB'; From 200f2b02a8af6fe0e0409375313f6d2fb240ac13 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Fri, 4 Oct 2024 02:39:01 +0100 Subject: [PATCH 07/11] chore: add tip and transfer tests --- Scarb.lock | 6 - Scarb.toml | 4 +- src/base/constants.cairo | 1 - src/base/constants/contract_addresses.cairo | 15 - src/base/constants/errors.cairo | 2 - src/base/constants/types.cairo | 19 +- src/interfaces/IJolt.cairo | 7 +- src/jolt/jolt.cairo | 229 ++++------- src/mocks.cairo | 1 + src/mocks/ERC20.cairo | 38 ++ tests/test_handle_registry.cairo | 4 +- tests/test_jolt.cairo | 410 +++++++++++++++++++- 12 files changed, 519 insertions(+), 217 deletions(-) delete mode 100644 src/base/constants/contract_addresses.cairo create mode 100644 src/mocks/ERC20.cairo diff --git a/Scarb.lock b/Scarb.lock index ae55d89..133b44a 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -59,7 +59,6 @@ version = "0.1.0" dependencies = [ "alexandria_bytes", "openzeppelin", - "pragma_lib", "snforge_std", "token_bound_accounts", ] @@ -166,11 +165,6 @@ name = "openzeppelin_utils" version = "0.17.0" source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.17.0#bf5d02c25c989ccc24f3ab42ec649617d3f21289" -[[package]] -name = "pragma_lib" -version = "1.0.0" -source = "git+https://github.com/astraly-labs/pragma-lib#86d7ccdc15b349b8b48d9796fc8464c947bea6e1" - [[package]] name = "snforge_scarb_plugin" version = "0.31.0" diff --git a/Scarb.toml b/Scarb.toml index 01daf6a..f00def2 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -14,16 +14,14 @@ starknet = "2.8.2" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.17.0" } token_bound_accounts= { git = "https://github.com/Starknet-Africa-Edu/TBA", tag = "v0.3.0" } alexandria_bytes = { git = "https://github.com/keep-starknet-strange/alexandria.git" } -pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" } [dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.31.0" } - [sncast.default] url= "https://starknet-sepolia.public.blastapi.io" [[target.starknet-contract]] casm = true build-external-contracts = ["token_bound_accounts::presets::account::Account"] -allowed-libfuncs-list.name = "experimental" +allowed-libfuncs-list.name = "experimental" \ No newline at end of file diff --git a/src/base/constants.cairo b/src/base/constants.cairo index c247388..422d669 100644 --- a/src/base/constants.cairo +++ b/src/base/constants.cairo @@ -1,3 +1,2 @@ pub mod errors; pub mod types; -pub mod contract_addresses; diff --git a/src/base/constants/contract_addresses.cairo b/src/base/constants/contract_addresses.cairo deleted file mode 100644 index e177e8a..0000000 --- a/src/base/constants/contract_addresses.cairo +++ /dev/null @@ -1,15 +0,0 @@ -// ************************************************************************* -// JOLT - ERC20 CONTRACT ADDRESSES -// ************************************************************************* -pub mod Addresses { - pub const ETH: felt252 = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; - pub const STRK: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; - pub const USDT: felt252 = 0x057e859eEE48E899CeF91cf0595661BEc0634dB7d593d98222C68Af6472e8394; - pub const USDC: felt252 = 0x053b40A647CEDfca6cA84f542A0fe36736031905A9639a7f19A3C1e66bFd5080; - pub const PRAGMA_ORACLE: felt252 = - 0x36031daa264c24520b11d93af622c848b2499b66b41d611bac95e13cfca131a; - // pub const USDT: felt252 = 0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8; -// pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; -// pub const PRAGMA_ORACLE: felt252 = -// 0x2a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; -} diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index 3e2662f..62e29a9 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -20,12 +20,10 @@ pub mod Errors { pub const SELF_FOLLOWING: felt252 = 'Karst: self follow is forbidden'; pub const ALREADY_REACTED: felt252 = 'Karst: already react to post!'; pub const SELF_TIPPING: felt252 = 'Karst: self-tip forbidden!'; - pub const MAX_TIPPING: felt252 = 'Karst: exceeds max tipping!'; pub const SELF_TRANSFER: felt252 = 'Karst: self-transfer forbidden!'; pub const SELF_REQUEST: felt252 = 'Karst: self-request forbidden!'; pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Karst: insufficient allowance!'; pub const AUTO_RENEW_DURATION_ENDED: felt252 = 'Karst: auto renew ended!'; pub const INVALID_JOLT: felt252 = 'Karst: invalid jolt!'; - pub const EXPIRED_JOLT: felt252 = 'Karst: jolt is expired!'; pub const INVALID_JOLT_RECIPIENT: felt252 = 'Karst: not request recipient!'; } diff --git a/src/base/constants/types.cairo b/src/base/constants/types.cairo index ca9ee87..9d6fe54 100644 --- a/src/base/constants/types.cairo +++ b/src/base/constants/types.cairo @@ -177,29 +177,28 @@ pub struct FollowData { // JOLT // ************************************************************************* #[derive(Drop, Serde, starknet::Store)] -pub struct joltData { +pub struct JoltData { pub jolt_id: u256, pub jolt_type: JoltType, pub sender: ContractAddress, pub recipient: ContractAddress, pub memo: ByteArray, pub amount: u256, - pub amount_in_usd: u256, - pub currency: JoltCurrency, pub status: JoltStatus, pub expiration_stamp: u64, - pub block_timestamp: u64 + pub block_timestamp: u64, + pub erc20_contract_address: ContractAddress } #[derive(Drop, Serde)] -pub struct joltParams { +pub struct JoltParams { pub jolt_type: JoltType, pub recipient: ContractAddress, pub memo: ByteArray, pub amount: u256, - pub currency: JoltCurrency, pub expiration_stamp: u64, pub auto_renewal: (bool, u256), + pub erc20_contract_address: ContractAddress, } #[derive(Drop, Serde, starknet::Store)] @@ -209,14 +208,6 @@ pub struct RenewalData { pub erc20_contract_address: ContractAddress } -#[derive(Drop, Serde, starknet::Store, PartialEq)] -pub enum JoltCurrency { - USDT, - USDC, - ETH, - STRK -} - #[derive(Drop, Serde, starknet::Store, PartialEq)] pub enum JoltType { Tip, diff --git a/src/interfaces/IJolt.cairo b/src/interfaces/IJolt.cairo index 817070c..7960f4a 100644 --- a/src/interfaces/IJolt.cairo +++ b/src/interfaces/IJolt.cairo @@ -1,19 +1,18 @@ use starknet::ContractAddress; -use karst::base::constants::types::{joltParams, joltData}; +use karst::base::constants::types::{JoltParams, JoltData}; #[starknet::interface] pub trait IJolt { // ************************************************************************* // EXTERNALS // ************************************************************************* - fn jolt(ref self: TState, jolt_params: joltParams) -> u256; + fn jolt(ref self: TState, jolt_params: JoltParams) -> u256; fn set_fee_address(ref self: TState, _fee_address: ContractAddress); fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; fn fullfill_request(ref self: TState, jolt_id: u256, sender: ContractAddress) -> bool; // ************************************************************************* // GETTERS // ************************************************************************* - fn get_jolt(self: @TState, jolt_id: u256) -> joltData; - fn total_jolts_received(self: @TState, profile: ContractAddress) -> u256; + fn get_jolt(self: @TState, jolt_id: u256) -> JoltData; fn get_fee_address(self: @TState) -> ContractAddress; } diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index 0b0a036..cf3cc5b 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -8,7 +8,7 @@ pub mod Jolt { use core::pedersen::PedersenTrait; use starknet::{ ContractAddress, ClassHash, get_caller_address, get_contract_address, get_block_timestamp, - get_tx_info, contract_address_const, + get_tx_info, storage::{ StoragePointerWriteAccess, StoragePointerReadAccess, Map, StorageMapReadAccess, StorageMapWriteAccess @@ -16,8 +16,7 @@ pub mod Jolt { }; use karst::base::{ constants::errors::Errors, - constants::types::{joltData, joltParams, JoltType, JoltCurrency, JoltStatus, RenewalData}, - constants::contract_addresses::Addresses, + constants::types::{JoltData, JoltParams, JoltType, JoltStatus, RenewalData} }; use karst::interfaces::{IJolt::IJolt, IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}}; @@ -25,9 +24,6 @@ pub mod Jolt { use openzeppelin::upgrades::UpgradeableComponent; use openzeppelin::upgrades::interface::IUpgradeable; - use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; - use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; - // ************************************************************************* // COMPONENTS // ************************************************************************* @@ -52,8 +48,7 @@ pub mod Jolt { #[substorage(v0)] upgradeable: UpgradeableComponent::Storage, fee_address: ContractAddress, - jolt: Map::, - total_jolts: Map::, + jolt: Map::, renewals: Map::<(ContractAddress, u256), RenewalData>, } @@ -62,7 +57,7 @@ pub mod Jolt { // ************************************************************************* #[event] #[derive(Drop, starknet::Event)] - enum Event { + pub enum Event { #[flat] OwnableEvent: OwnableComponent::Event, #[flat] @@ -74,31 +69,31 @@ pub mod Jolt { #[derive(Drop, starknet::Event)] pub struct Jolted { - jolt_id: u256, - jolt_type: felt252, - sender: ContractAddress, - recipient: ContractAddress, - block_timestamp: u64, + pub jolt_id: u256, + pub jolt_type: felt252, + pub sender: ContractAddress, + pub recipient: ContractAddress, + pub block_timestamp: u64, } #[derive(Drop, starknet::Event)] pub struct JoltRequested { - jolt_id: u256, - jolt_type: felt252, - sender: ContractAddress, - recipient: ContractAddress, - expiration_timestamp: u64, - block_timestamp: u64, + pub jolt_id: u256, + pub jolt_type: felt252, + pub sender: ContractAddress, + pub recipient: ContractAddress, + pub expiration_timestamp: u64, + pub block_timestamp: u64, } #[derive(Drop, starknet::Event)] pub struct JoltRequestFullfilled { - jolt_id: u256, - jolt_type: felt252, - sender: ContractAddress, - recipient: ContractAddress, - expiration_timestamp: u64, - block_timestamp: u64, + pub jolt_id: u256, + pub jolt_type: felt252, + pub sender: ContractAddress, + pub recipient: ContractAddress, + pub expiration_timestamp: u64, + pub block_timestamp: u64, } const MAX_TIP: u256 = 1000; @@ -111,13 +106,12 @@ pub mod Jolt { self.ownable.initializer(owner); } - #[abi(embed_v0)] impl JoltImpl of IJolt { // ************************************************************************* // EXTERNALS // ************************************************************************* - fn jolt(ref self: ContractState, jolt_params: joltParams) -> u256 { + fn jolt(ref self: ContractState, jolt_params: JoltParams) -> u256 { let sender = get_caller_address(); let tx_info = get_tx_info().unbox(); let tx_timestamp = get_block_timestamp(); @@ -133,25 +127,14 @@ pub mod Jolt { let jolt_id: u256 = jolt_hash.try_into().unwrap(); - // get the appropriate contract address - let mut erc20_contract_address: ContractAddress = contract_address_const::<0>(); - let jolt_currency = @jolt_params.currency; - - match jolt_currency { - JoltCurrency::USDT => erc20_contract_address = Addresses::USDT.try_into().unwrap(), - JoltCurrency::USDC => erc20_contract_address = Addresses::USDC.try_into().unwrap(), - JoltCurrency::ETH => erc20_contract_address = Addresses::ETH.try_into().unwrap(), - JoltCurrency::STRK => erc20_contract_address = Addresses::STRK.try_into().unwrap() - }; - // jolt - let mut tx_status = false; let mut jolt_status = JoltStatus::PENDING; + let erc20_contract_address = jolt_params.erc20_contract_address; let jolt_type = @jolt_params.jolt_type; match jolt_type { JoltType::Tip => { - let (_tx_status, _jolt_status) = self + let _jolt_status = self ._tip( jolt_id, sender, @@ -159,12 +142,10 @@ pub mod Jolt { jolt_params.amount, erc20_contract_address ); - - tx_status = _tx_status; jolt_status = _jolt_status; }, JoltType::Transfer => { - let (_tx_status, _jolt_status) = self + let _jolt_status = self ._transfer( jolt_id, sender, @@ -172,18 +153,10 @@ pub mod Jolt { jolt_params.amount, erc20_contract_address ); - - tx_status = _tx_status; jolt_status = _jolt_status; }, JoltType::Subscription => { - // check that currency is a stable - if (jolt_currency != @JoltCurrency::USDT - || jolt_currency != @JoltCurrency::USDC) { - panic!("Karst: subscription can only be done with stables!"); - } - - let (_tx_status, _jolt_status) = self + let _jolt_status = self ._subscribe( jolt_id, sender, @@ -191,12 +164,10 @@ pub mod Jolt { jolt_params.auto_renewal, erc20_contract_address ); - - tx_status = _tx_status; jolt_status = _jolt_status; }, JoltType::Request => { - let (_tx_status, _jolt_status) = self + let _jolt_status = self ._request( jolt_id, sender, @@ -205,38 +176,25 @@ pub mod Jolt { jolt_params.expiration_stamp, erc20_contract_address ); - - tx_status = _tx_status; jolt_status = _jolt_status; } }; - // get jolt amount in usd - let mut amount_in_usd = self._get_usd_equiv(jolt_params.amount, erc20_contract_address); - // prefill tx data - let jolt_data = joltData { + let jolt_data = JoltData { jolt_id: jolt_id, jolt_type: jolt_params.jolt_type, sender: sender, recipient: jolt_params.recipient, memo: jolt_params.memo, amount: jolt_params.amount, - amount_in_usd: amount_in_usd, - currency: jolt_params.currency, status: jolt_status, expiration_stamp: jolt_params.expiration_stamp, - block_timestamp: tx_timestamp + block_timestamp: tx_timestamp, + erc20_contract_address: jolt_params.erc20_contract_address }; - // write to storage - if (jolt_data.status == JoltStatus::SUCCESSFUL) { - let total_jolts_recieved = self.total_jolts.read(jolt_params.recipient) - + amount_in_usd; - self.total_jolts.write(jolt_params.recipient, total_jolts_recieved); - } self.jolt.write(jolt_id, jolt_data); - return jolt_id; } @@ -257,7 +215,7 @@ pub mod Jolt { // if expired write jolt status to expired and exit if (get_block_timestamp() > jolt_details.expiration_stamp) { - let jolt_data = joltData { status: JoltStatus::EXPIRED, ..jolt_details }; + let jolt_data = JoltData { status: JoltStatus::EXPIRED, ..jolt_details }; self.jolt.write(jolt_id, jolt_data); return false; } @@ -273,14 +231,10 @@ pub mod Jolt { // ************************************************************************* // GETTERS // ************************************************************************* - fn get_jolt(self: @ContractState, jolt_id: u256) -> joltData { + fn get_jolt(self: @ContractState, jolt_id: u256) -> JoltData { self.jolt.read(jolt_id) } - fn total_jolts_received(self: @ContractState, profile: ContractAddress) -> u256 { - self.total_jolts.read(profile) - } - fn get_fee_address(self: @ContractState) -> ContractAddress { self.fee_address.read() } @@ -309,18 +263,13 @@ pub mod Jolt { recipient: ContractAddress, amount: u256, erc20_contract_address: ContractAddress - ) -> (bool, JoltStatus) { + ) -> JoltStatus { // check that user is not self-tipping or tipping a non-existent address assert(sender != recipient, Errors::SELF_TIPPING); assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); - // check that tip does not exceed maximum tip - let tipped_amount = self._get_usd_equiv(amount, erc20_contract_address); - assert(tipped_amount <= MAX_TIP, Errors::MAX_TIPPING); - // tip user - let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; - dispatcher.transfer_from(sender, recipient, amount); + self._transfer_helper(erc20_contract_address, sender, recipient, amount); // emit event self @@ -335,7 +284,7 @@ pub mod Jolt { ); // return txn status - (true, JoltStatus::SUCCESSFUL) + JoltStatus::SUCCESSFUL } fn _transfer( @@ -345,14 +294,13 @@ pub mod Jolt { recipient: ContractAddress, amount: u256, erc20_contract_address: ContractAddress - ) -> (bool, JoltStatus) { + ) -> JoltStatus { // check that user is not transferring to self or to a non-existent address assert(sender != recipient, Errors::SELF_TRANSFER); assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); // transfer to recipient - let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; - dispatcher.transfer_from(sender, recipient, amount); + self._transfer_helper(erc20_contract_address, sender, recipient, amount); // emit event self @@ -367,7 +315,7 @@ pub mod Jolt { ); // return txn status - (true, JoltStatus::SUCCESSFUL) + JoltStatus::SUCCESSFUL } fn _subscribe( @@ -377,17 +325,17 @@ pub mod Jolt { amount: u256, auto_renewal: (bool, u256), erc20_contract_address: ContractAddress - ) -> (bool, JoltStatus) { - let (renewal_status, renewal_duration_in_months) = auto_renewal; + ) -> JoltStatus { + let (renewal_status, renewal_duration) = auto_renewal; let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let this_contract = get_contract_address(); let tx_info = get_tx_info().unbox(); - if (renewal_status) { + if (renewal_status == true) { // check allowances match auto-renew duration let allowance = dispatcher.allowance(sender, this_contract); assert( - allowance == renewal_duration_in_months * amount, Errors::INSUFFICIENT_ALLOWANCE + allowance >= renewal_duration * amount, Errors::INSUFFICIENT_ALLOWANCE ); // generate renewal ID @@ -403,7 +351,7 @@ pub mod Jolt { // write renewal details to storage let renewal_data = RenewalData { - renewal_duration: renewal_duration_in_months, + renewal_duration: renewal_duration, renewal_amount: amount, erc20_contract_address }; @@ -412,7 +360,7 @@ pub mod Jolt { // send subscription amount to fee address let fee_address = self.fee_address.read(); - dispatcher.transfer_from(sender, fee_address, amount); + self._transfer_helper(erc20_contract_address, sender, fee_address, amount); // emit event self @@ -427,7 +375,7 @@ pub mod Jolt { ); // return txn status - (true, JoltStatus::SUCCESSFUL) + JoltStatus::SUCCESSFUL } fn _request( @@ -438,7 +386,7 @@ pub mod Jolt { amount: u256, expiration_timestamp: u64, erc20_contract_address: ContractAddress - ) -> (bool, JoltStatus) { + ) -> JoltStatus { // check that user is not requesting to self or to a non-existent address assert(sender != recipient, Errors::SELF_REQUEST); assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); @@ -457,28 +405,22 @@ pub mod Jolt { ); // return txn status - (true, JoltStatus::PENDING) + JoltStatus::PENDING } fn _fulfill_request( - ref self: ContractState, jolt_id: u256, sender: ContractAddress, jolt_details: joltData + ref self: ContractState, jolt_id: u256, sender: ContractAddress, jolt_details: JoltData ) -> bool { - // get the appropriate contract address - let jolt_currency = @jolt_details.currency; - let mut erc20_contract_address: ContractAddress = contract_address_const::<0>(); - match jolt_currency { - JoltCurrency::USDT => erc20_contract_address = Addresses::USDT.try_into().unwrap(), - JoltCurrency::USDC => erc20_contract_address = Addresses::USDC.try_into().unwrap(), - JoltCurrency::ETH => erc20_contract_address = Addresses::ETH.try_into().unwrap(), - JoltCurrency::STRK => erc20_contract_address = Addresses::STRK.try_into().unwrap() - }; - // transfer request amount - let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; - dispatcher.transfer_from(sender, jolt_details.sender, jolt_details.amount); + self._transfer_helper( + jolt_details.erc20_contract_address, + jolt_details.sender, + jolt_details.recipient, + jolt_details.amount + ); // update jolt details - let jolt_data = joltData { status: JoltStatus::SUCCESSFUL, ..jolt_details }; + let jolt_data = JoltData { status: JoltStatus::SUCCESSFUL, ..jolt_details }; self.jolt.write(jolt_id, jolt_data); // emit events @@ -505,14 +447,13 @@ pub mod Jolt { .renewals .read((sender, renewal_id)) .erc20_contract_address; - let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; // check duration is greater than 0 else shouldn't auto renew assert(duration > 0, Errors::AUTO_RENEW_DURATION_ENDED); // send subscription amount to fee address let fee_address = self.fee_address.read(); - dispatcher.transfer_from(sender, fee_address, amount); + self._transfer_helper(erc20_contract_address, sender, fee_address, amount); // generate jolt_id let jolt_hash = PedersenTrait::new(0) @@ -531,33 +472,22 @@ pub mod Jolt { }; self.renewals.write((sender, renewal_id), renewal_data); - // get currency - let mut currency = JoltCurrency::USDT; - let erc20_symbol = dispatcher.symbol(); - if (erc20_symbol == "USDC") { - currency = JoltCurrency::USDC; - } - // prefill tx data - let amount_in_usd = self._get_usd_equiv(amount, erc20_contract_address); - let jolt_data = joltData { + let jolt_data = JoltData { jolt_id: jolt_id, jolt_type: JoltType::Subscription, sender: sender, recipient: fee_address, memo: "auto renew successful", amount: amount, - amount_in_usd, - currency: currency, status: JoltStatus::SUCCESSFUL, expiration_stamp: 0, - block_timestamp: get_block_timestamp() + block_timestamp: get_block_timestamp(), + erc20_contract_address }; - let total_jolts_recieved = self.total_jolts.read(fee_address) + amount; // write to storage self.jolt.write(jolt_id, jolt_data); - self.total_jolts.write(fee_address, total_jolts_recieved); // emit event self @@ -575,36 +505,21 @@ pub mod Jolt { return true; } - // TODO: convert jolt amount to usd equivalent - fn _get_usd_equiv( - ref self: ContractState, amount: u256, erc20_contract_address: ContractAddress - ) -> u256 { - // get oracle key - let mut KEY: felt252 = 0; + fn _transfer_helper( + ref self: ContractState, + erc20_contract_address: ContractAddress, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; - let erc20_symbol = dispatcher.symbol(); - if (erc20_symbol == "ETH") { - KEY = 19514442401534788; - } else if (erc20_symbol == "STRK") { - KEY = 6004514686061859652; - } else if (erc20_symbol == "USDT") { - KEY = 6148333044652921668; - } else if (erc20_symbol == "USDC") { - KEY = 6148332971638477636; - } - let oracle_address: ContractAddress = Addresses::PRAGMA_ORACLE.try_into().unwrap(); - let price = self._get_asset_price_median(oracle_address, DataType::SpotEntry(KEY)); - price.try_into().unwrap() - } + // check allowance + let allowance = dispatcher.allowance(sender, get_contract_address()); + assert(allowance >= amount, Errors::INSUFFICIENT_ALLOWANCE); - fn _get_asset_price_median( - ref self: ContractState, oracle_address: ContractAddress, asset: DataType - ) -> u128 { - let oracle_dispatcher = IPragmaABIDispatcher { contract_address: oracle_address }; - let output: PragmaPricesResponse = oracle_dispatcher - .get_data(asset, AggregationMode::Median(())); - return output.price; + // transfer to recipient + dispatcher.transfer_from(sender, recipient, amount); } } -} +} \ No newline at end of file diff --git a/src/mocks.cairo b/src/mocks.cairo index 4dc0e24..1541e6f 100644 --- a/src/mocks.cairo +++ b/src/mocks.cairo @@ -1,2 +1,3 @@ pub mod registry; pub mod interfaces; +pub mod ERC20; \ No newline at end of file diff --git a/src/mocks/ERC20.cairo b/src/mocks/ERC20.cairo new file mode 100644 index 0000000..17076b9 --- /dev/null +++ b/src/mocks/ERC20.cairo @@ -0,0 +1,38 @@ +#[starknet::contract] +mod USDT { + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // ERC20 Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + initial_supply: u256, + recipient: ContractAddress + ) { + let name = "USDT"; + let symbol = "USDT"; + + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + } +} \ No newline at end of file diff --git a/tests/test_handle_registry.cairo b/tests/test_handle_registry.cairo index 94f0d24..c86c920 100644 --- a/tests/test_handle_registry.cairo +++ b/tests/test_handle_registry.cairo @@ -178,7 +178,7 @@ fn test_unlink_fails_if_caller_is_not_owner() { } #[test] -fn test_emmit_linked_event() { +fn test_linked_event_emission() { let (handle_registry_address, handle_contract_address) = __setup__(); let registryDispatcher = IHandleRegistryDispatcher { contract_address: handle_registry_address @@ -207,7 +207,7 @@ fn test_emmit_linked_event() { } #[test] -fn test_emmit_unlinked_event() { +fn test_unlinked_event_emission() { let (handle_registry_address, handle_contract_address) = __setup__(); let registryDispatcher = IHandleRegistryDispatcher { contract_address: handle_registry_address diff --git a/tests/test_jolt.cairo b/tests/test_jolt.cairo index 889618c..5c20c2e 100644 --- a/tests/test_jolt.cairo +++ b/tests/test_jolt.cairo @@ -1,31 +1,415 @@ use core::traits::TryInto; -use starknet::{ContractAddress, get_block_timestamp}; +use core::hash::HashStateTrait; +use core::pedersen::PedersenTrait; +use starknet::{ContractAddress, contract_address_const}; use snforge_std::{ - declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, - EventSpyAssertionsTrait, ContractClassTrait, DeclareResultTrait, start_cheat_block_timestamp, - stop_cheat_block_timestamp + declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, stop_cheat_caller_address, start_cheat_nonce, stop_cheat_nonce, start_cheat_block_timestamp, stop_cheat_block_timestamp, spy_events, EventSpyAssertionsTrait }; use karst::interfaces::IJolt::{IJoltDispatcher, IJoltDispatcherTrait}; +use karst::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; +use karst::jolt::jolt::Jolt::{Event as JoltEvent, Jolted}; use karst::base::{ - constants::errors::Errors, - constants::types::{joltData, joltParams, JoltType, JoltCurrency, JoltStatus, RenewalData} + constants::types::{JoltParams, JoltType, JoltStatus} }; -const ADMIN: felt252 = 13245; -const ADDRESS1: felt252 = 53435; -const ADDRESS2: felt252 = 204925; -const ADDRESS3: felt252 = 249205; +const ADMIN: felt252 = 5382942; +const ADDRESS1: felt252 = 254290; +const ADDRESS2: felt252 = 525616; // ************************************************************************* // SETUP // ************************************************************************* -fn __setup__() -> ContractAddress { +fn __setup__() -> (ContractAddress, ContractAddress) { + // deploy jolt contract let jolt_contract = declare("Jolt").unwrap().contract_class(); let (jolt_contract_address, _) = jolt_contract.deploy(@array![ADMIN]).unwrap(); - return (jolt_contract_address); + + // deploy mock USDT + let usdt_contract = declare("USDT").unwrap().contract_class(); + let (usdt_contract_address, _) = usdt_contract.deploy(@array![1000000000000000000000, 0, ADDRESS1]).unwrap(); + + return (jolt_contract_address, usdt_contract_address); +} + +// ************************************************************************* +// TEST - TIP +// ************************************************************************* +#[test] +fn test_jolt_tipping() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Tip, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first tip ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + start_cheat_nonce(jolt_contract_address, 23); + + let jolt_id = dispatcher.jolt(jolt_params); + + // calculate expected jolt ID + let jolt_hash = PedersenTrait::new(0) + .update(ADDRESS2) + .update(2000000000000000000) + .update(0) + .update(23) + .update(4) + .finalize(); + let expected_jolt_id: u256 = jolt_hash.try_into().unwrap(); + + // check jolt data + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.jolt_id == expected_jolt_id, 'invalid jolt ID'); + assert(jolt_data.jolt_type == JoltType::Tip, 'invalid jolt type'); + assert(jolt_data.sender == ADDRESS1.try_into().unwrap(), 'invalid sender'); + assert(jolt_data.recipient == ADDRESS2.try_into().unwrap(), 'invalid recipient'); + assert(jolt_data.memo == "hey first tip ever!", 'invalid memo'); + assert(jolt_data.amount == 2000000000000000000, 'invalid amount'); + assert(jolt_data.status == JoltStatus::SUCCESSFUL, 'invalid status'); + assert(jolt_data.expiration_stamp == 0, 'invalid expiration stamp'); + assert(jolt_data.block_timestamp == 36000, 'invalid block stamp'); + assert(jolt_data.erc20_contract_address == erc20_contract_address, 'invalid address'); + + // check that recipient received his tip + let balance = erc20_dispatcher.balance_of(ADDRESS2.try_into().unwrap()); + assert(balance == 2000000000000000000, 'incorrect balance'); + + stop_cheat_nonce(jolt_contract_address); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_jolting_with_same_params_have_different_jolt_ids() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params_1 = JoltParams { + jolt_type: JoltType::Tip, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first tip ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + let jolt_params_2 = JoltParams { + jolt_type: JoltType::Tip, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey second tip!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 4000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + start_cheat_nonce(jolt_contract_address, 23); + let jolt_id_1 = dispatcher.jolt(jolt_params_1); + stop_cheat_nonce(jolt_contract_address); + + start_cheat_nonce(jolt_contract_address, 24); + let jolt_id_2 = dispatcher.jolt(jolt_params_2); + stop_cheat_nonce(jolt_contract_address); + + // check jolt ids are not equal + assert(jolt_id_1 != jolt_id_2, 'jolt id should be unique!'); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: self-tip forbidden!',))] +fn test_tipper_cant_self_tip() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Tip, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first tip ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + + dispatcher.jolt(jolt_params); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid profile address!',))] +fn test_tipper_cant_tip_a_zero_address() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Tip, + recipient: contract_address_const::<0>(), + memo: "hey first tip ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + + dispatcher.jolt(jolt_params); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_jolt_event_is_emitted_on_tipping() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Tip, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first tip ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + + let mut spy = spy_events(); + let jolt_id = dispatcher.jolt(jolt_params); + + // check for events + let expected_event = JoltEvent::Jolted( + Jolted { + jolt_id: jolt_id, + jolt_type: 'TIP', + sender: ADDRESS1.try_into().unwrap(), + recipient: ADDRESS2.try_into().unwrap(), + block_timestamp: 36000, + } + ); + spy.assert_emitted(@array![(jolt_contract_address, expected_event)]); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); } + // ************************************************************************* -// TEST +// TEST - TRANSFER // ************************************************************************* +#[test] +fn test_jolt_transfer() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Transfer, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first transfer ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + start_cheat_nonce(jolt_contract_address, 23); + + let jolt_id = dispatcher.jolt(jolt_params); + + // calculate expected jolt ID + let jolt_hash = PedersenTrait::new(0) + .update(ADDRESS2) + .update(2000000000000000000) + .update(0) + .update(23) + .update(4) + .finalize(); + let expected_jolt_id: u256 = jolt_hash.try_into().unwrap(); + + // check jolt data + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.jolt_id == expected_jolt_id, 'invalid jolt ID'); + assert(jolt_data.jolt_type == JoltType::Transfer, 'invalid jolt type'); + assert(jolt_data.sender == ADDRESS1.try_into().unwrap(), 'invalid sender'); + assert(jolt_data.recipient == ADDRESS2.try_into().unwrap(), 'invalid recipient'); + assert(jolt_data.memo == "hey first transfer ever!", 'invalid memo'); + assert(jolt_data.amount == 2000000000000000000, 'invalid amount'); + assert(jolt_data.status == JoltStatus::SUCCESSFUL, 'invalid status'); + assert(jolt_data.expiration_stamp == 0, 'invalid expiration stamp'); + assert(jolt_data.block_timestamp == 36000, 'invalid block stamp'); + assert(jolt_data.erc20_contract_address == erc20_contract_address, 'invalid address'); + + // check that recipient received his tip + let balance = erc20_dispatcher.balance_of(ADDRESS2.try_into().unwrap()); + assert(balance == 2000000000000000000, 'incorrect balance'); + + stop_cheat_nonce(jolt_contract_address); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid profile address!',))] +fn test_sender_cant_transfer_to_a_zero_address() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Transfer, + recipient: contract_address_const::<0>(), + memo: "hey first transfer ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + dispatcher.jolt(jolt_params); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: self-transfer forbidden!',))] +fn test_sender_cant_self_transfer() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Transfer, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first transfer ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + + dispatcher.jolt(jolt_params); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_jolt_event_is_emitted_on_transfer() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Transfer, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first transfer ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + + let mut spy = spy_events(); + let jolt_id = dispatcher.jolt(jolt_params); + + // check for events + let expected_event = JoltEvent::Jolted( + Jolted { + jolt_id: jolt_id, + jolt_type: 'TRANSFER', + sender: ADDRESS1.try_into().unwrap(), + recipient: ADDRESS2.try_into().unwrap(), + block_timestamp: 36000, + } + ); + spy.assert_emitted(@array![(jolt_contract_address, expected_event)]); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +// ************************************************************************* +// TEST - REQUEST +// ************************************************************************* \ No newline at end of file From 804ca37e4c99a05fb2dc26264973443efa0227a4 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Fri, 4 Oct 2024 04:01:36 +0100 Subject: [PATCH 08/11] chore: add tests for jolt request functionality --- src/base/constants/errors.cairo | 1 + src/interfaces/IJolt.cairo | 2 +- src/jolt/jolt.cairo | 43 +-- src/mocks.cairo | 2 +- src/mocks/ERC20.cairo | 8 +- tests/test_jolt.cairo | 470 ++++++++++++++++++++++++++++++-- 6 files changed, 480 insertions(+), 46 deletions(-) diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index 62e29a9..3290474 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -22,6 +22,7 @@ pub mod Errors { pub const SELF_TIPPING: felt252 = 'Karst: self-tip forbidden!'; pub const SELF_TRANSFER: felt252 = 'Karst: self-transfer forbidden!'; pub const SELF_REQUEST: felt252 = 'Karst: self-request forbidden!'; + pub const INVALID_EXPIRATION_STAMP: felt252 = 'Karst: invalid expiration stamp'; pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Karst: insufficient allowance!'; pub const AUTO_RENEW_DURATION_ENDED: felt252 = 'Karst: auto renew ended!'; pub const INVALID_JOLT: felt252 = 'Karst: invalid jolt!'; diff --git a/src/interfaces/IJolt.cairo b/src/interfaces/IJolt.cairo index 7960f4a..9b76dfd 100644 --- a/src/interfaces/IJolt.cairo +++ b/src/interfaces/IJolt.cairo @@ -9,7 +9,7 @@ pub trait IJolt { fn jolt(ref self: TState, jolt_params: JoltParams) -> u256; fn set_fee_address(ref self: TState, _fee_address: ContractAddress); fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; - fn fullfill_request(ref self: TState, jolt_id: u256, sender: ContractAddress) -> bool; + fn fulfill_request(ref self: TState, jolt_id: u256) -> bool; // ************************************************************************* // GETTERS // ************************************************************************* diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index cf3cc5b..ea93216 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -134,7 +134,7 @@ pub mod Jolt { let jolt_type = @jolt_params.jolt_type; match jolt_type { JoltType::Tip => { - let _jolt_status = self + let _jolt_status = self ._tip( jolt_id, sender, @@ -203,13 +203,13 @@ pub mod Jolt { self.fee_address.write(_fee_address); } - fn fullfill_request( - ref self: ContractState, jolt_id: u256, sender: ContractAddress - ) -> bool { + fn fulfill_request(ref self: ContractState, jolt_id: u256) -> bool { // get jolt details let mut jolt_details = self.jolt.read(jolt_id); + let sender = get_caller_address(); // validate request + assert(jolt_details.jolt_type == JoltType::Request, Errors::INVALID_JOLT); assert(jolt_details.status == JoltStatus::PENDING, Errors::INVALID_JOLT); assert(sender == jolt_details.recipient, Errors::INVALID_JOLT_RECIPIENT); @@ -334,9 +334,7 @@ pub mod Jolt { if (renewal_status == true) { // check allowances match auto-renew duration let allowance = dispatcher.allowance(sender, this_contract); - assert( - allowance >= renewal_duration * amount, Errors::INSUFFICIENT_ALLOWANCE - ); + assert(allowance >= renewal_duration * amount, Errors::INSUFFICIENT_ALLOWANCE); // generate renewal ID let renewal_hash = PedersenTrait::new(0) @@ -387,9 +385,11 @@ pub mod Jolt { expiration_timestamp: u64, erc20_contract_address: ContractAddress ) -> JoltStatus { - // check that user is not requesting to self or to a non-existent address + // check that user is not requesting to self or to a non-existent address and expiration + // time exceeds current time assert(sender != recipient, Errors::SELF_REQUEST); assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); + assert(expiration_timestamp > get_block_timestamp(), Errors::INVALID_EXPIRATION_STAMP); // emit event self @@ -412,12 +412,13 @@ pub mod Jolt { ref self: ContractState, jolt_id: u256, sender: ContractAddress, jolt_details: JoltData ) -> bool { // transfer request amount - self._transfer_helper( - jolt_details.erc20_contract_address, - jolt_details.sender, - jolt_details.recipient, - jolt_details.amount - ); + self + ._transfer_helper( + jolt_details.erc20_contract_address, + sender, + jolt_details.sender, + jolt_details.amount + ); // update jolt details let jolt_data = JoltData { status: JoltStatus::SUCCESSFUL, ..jolt_details }; @@ -428,8 +429,8 @@ pub mod Jolt { .emit( JoltRequestFullfilled { jolt_id, - jolt_type: 'REQUEST', - sender, + jolt_type: 'REQUEST FULFILLMENT', + sender: jolt_details.recipient, recipient: jolt_details.sender, expiration_timestamp: jolt_details.expiration_stamp, block_timestamp: get_block_timestamp(), @@ -506,10 +507,10 @@ pub mod Jolt { } fn _transfer_helper( - ref self: ContractState, - erc20_contract_address: ContractAddress, - sender: ContractAddress, - recipient: ContractAddress, + ref self: ContractState, + erc20_contract_address: ContractAddress, + sender: ContractAddress, + recipient: ContractAddress, amount: u256 ) { let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; @@ -522,4 +523,4 @@ pub mod Jolt { dispatcher.transfer_from(sender, recipient, amount); } } -} \ No newline at end of file +} diff --git a/src/mocks.cairo b/src/mocks.cairo index 1541e6f..231805c 100644 --- a/src/mocks.cairo +++ b/src/mocks.cairo @@ -1,3 +1,3 @@ pub mod registry; pub mod interfaces; -pub mod ERC20; \ No newline at end of file +pub mod ERC20; diff --git a/src/mocks/ERC20.cairo b/src/mocks/ERC20.cairo index 17076b9..c184133 100644 --- a/src/mocks/ERC20.cairo +++ b/src/mocks/ERC20.cairo @@ -24,15 +24,11 @@ mod USDT { } #[constructor] - fn constructor( - ref self: ContractState, - initial_supply: u256, - recipient: ContractAddress - ) { + fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { let name = "USDT"; let symbol = "USDT"; self.erc20.initializer(name, symbol); self.erc20.mint(recipient, initial_supply); } -} \ No newline at end of file +} diff --git a/tests/test_jolt.cairo b/tests/test_jolt.cairo index 5c20c2e..2b59287 100644 --- a/tests/test_jolt.cairo +++ b/tests/test_jolt.cairo @@ -3,14 +3,16 @@ use core::hash::HashStateTrait; use core::pedersen::PedersenTrait; use starknet::{ContractAddress, contract_address_const}; use snforge_std::{ - declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, stop_cheat_caller_address, start_cheat_nonce, stop_cheat_nonce, start_cheat_block_timestamp, stop_cheat_block_timestamp, spy_events, EventSpyAssertionsTrait + declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, + stop_cheat_caller_address, start_cheat_nonce, stop_cheat_nonce, start_cheat_block_timestamp, + stop_cheat_block_timestamp, spy_events, EventSpyAssertionsTrait }; use karst::interfaces::IJolt::{IJoltDispatcher, IJoltDispatcherTrait}; use karst::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; use karst::jolt::jolt::Jolt::{Event as JoltEvent, Jolted}; -use karst::base::{ - constants::types::{JoltParams, JoltType, JoltStatus} -}; +use karst::jolt::jolt::Jolt::{Event as JoltRequestEvent, JoltRequested}; +use karst::jolt::jolt::Jolt::{Event as JoltRequestFulfillEvent, JoltRequestFullfilled}; +use karst::base::{constants::types::{JoltParams, JoltType, JoltStatus}}; const ADMIN: felt252 = 5382942; const ADDRESS1: felt252 = 254290; @@ -26,7 +28,9 @@ fn __setup__() -> (ContractAddress, ContractAddress) { // deploy mock USDT let usdt_contract = declare("USDT").unwrap().contract_class(); - let (usdt_contract_address, _) = usdt_contract.deploy(@array![1000000000000000000000, 0, ADDRESS1]).unwrap(); + let (usdt_contract_address, _) = usdt_contract + .deploy(@array![1000000000000000000000, 0, ADDRESS1]) + .unwrap(); return (jolt_contract_address, usdt_contract_address); } @@ -37,7 +41,7 @@ fn __setup__() -> (ContractAddress, ContractAddress) { #[test] fn test_jolt_tipping() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -71,7 +75,7 @@ fn test_jolt_tipping() { .update(4) .finalize(); let expected_jolt_id: u256 = jolt_hash.try_into().unwrap(); - + // check jolt data let jolt_data = dispatcher.get_jolt(jolt_id); assert(jolt_data.jolt_id == expected_jolt_id, 'invalid jolt ID'); @@ -99,7 +103,7 @@ fn test_jolting_with_same_params_have_different_jolt_ids() { let (jolt_contract_address, erc20_contract_address) = __setup__(); let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; - + let jolt_params_1 = JoltParams { jolt_type: JoltType::Tip, recipient: ADDRESS2.try_into().unwrap(), @@ -147,7 +151,7 @@ fn test_jolting_with_same_params_have_different_jolt_ids() { #[should_panic(expected: ('Karst: self-tip forbidden!',))] fn test_tipper_cant_self_tip() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -176,7 +180,7 @@ fn test_tipper_cant_self_tip() { #[should_panic(expected: ('Karst: invalid profile address!',))] fn test_tipper_cant_tip_a_zero_address() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -204,7 +208,7 @@ fn test_tipper_cant_tip_a_zero_address() { #[test] fn test_jolt_event_is_emitted_on_tipping() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -251,7 +255,7 @@ fn test_jolt_event_is_emitted_on_tipping() { #[test] fn test_jolt_transfer() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -285,7 +289,7 @@ fn test_jolt_transfer() { .update(4) .finalize(); let expected_jolt_id: u256 = jolt_hash.try_into().unwrap(); - + // check jolt data let jolt_data = dispatcher.get_jolt(jolt_id); assert(jolt_data.jolt_id == expected_jolt_id, 'invalid jolt ID'); @@ -312,7 +316,7 @@ fn test_jolt_transfer() { #[should_panic(expected: ('Karst: invalid profile address!',))] fn test_sender_cant_transfer_to_a_zero_address() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -341,7 +345,7 @@ fn test_sender_cant_transfer_to_a_zero_address() { #[should_panic(expected: ('Karst: self-transfer forbidden!',))] fn test_sender_cant_self_transfer() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -369,7 +373,7 @@ fn test_sender_cant_self_transfer() { #[test] fn test_jolt_event_is_emitted_on_transfer() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -412,4 +416,436 @@ fn test_jolt_event_is_emitted_on_transfer() { // ************************************************************************* // TEST - REQUEST -// ************************************************************************* \ No newline at end of file +// ************************************************************************* +#[test] +fn test_jolt_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 15640, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + start_cheat_nonce(jolt_contract_address, 23); + + let jolt_id = dispatcher.jolt(jolt_params); + + // calculate expected jolt ID + let jolt_hash = PedersenTrait::new(0) + .update(ADDRESS1) + .update(2000000000000000000) + .update(0) + .update(23) + .update(4) + .finalize(); + let expected_jolt_id: u256 = jolt_hash.try_into().unwrap(); + + // check jolt data + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.jolt_id == expected_jolt_id, 'invalid jolt ID'); + assert(jolt_data.jolt_type == JoltType::Request, 'invalid jolt type'); + assert(jolt_data.sender == ADDRESS2.try_into().unwrap(), 'invalid sender'); + assert(jolt_data.recipient == ADDRESS1.try_into().unwrap(), 'invalid recipient'); + assert(jolt_data.memo == "hey first request ever!", 'invalid memo'); + assert(jolt_data.amount == 2000000000000000000, 'invalid amount'); + assert(jolt_data.status == JoltStatus::PENDING, 'invalid status'); + assert(jolt_data.expiration_stamp == 15640, 'invalid expiration stamp'); + assert(jolt_data.block_timestamp == 5640, 'invalid block stamp'); + assert(jolt_data.erc20_contract_address == erc20_contract_address, 'invalid address'); + + stop_cheat_nonce(jolt_contract_address); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid profile address!',))] +fn test_requester_cant_request_to_a_zero_address() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: contract_address_const::<0>(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 15460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.jolt(jolt_params); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: self-request forbidden!',))] +fn test_requester_cant_self_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 15460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_jolt_event_is_emitted_on_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 154600, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + + let mut spy = spy_events(); + let jolt_id = dispatcher.jolt(jolt_params); + + // check for events + let expected_event = JoltRequestEvent::JoltRequested( + JoltRequested { + jolt_id: jolt_id, + jolt_type: 'REQUEST', + sender: ADDRESS1.try_into().unwrap(), + recipient: ADDRESS2.try_into().unwrap(), + expiration_timestamp: 154600, + block_timestamp: 36000, + } + ); + spy.assert_emitted(@array![(jolt_contract_address, expected_event)]); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid expiration stamp',))] +fn test_request_expiration_time_must_be_greater_than_current_time() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid jolt!',))] +fn test_cant_fulfill_request_if_jolt_type_is_not_a_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Transfer, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 12460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // try to fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.fulfill_request(jolt_id); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid jolt!',))] +fn test_cant_fulfill_request_if_jolt_status_is_not_pending() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 12460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 5000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // try to fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.fulfill_request(jolt_id); + dispatcher.fulfill_request(jolt_id); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: not request recipient!',))] +fn test_cant_fulfill_request_if_sender_is_not_initial_recipient() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 12460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // try to fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.fulfill_request(jolt_id); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_if_expiration_time_has_exceeded_jolt_fails_with_expired_status() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 8460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // try to fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 95640); + + let status = dispatcher.fulfill_request(jolt_id); + + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.status == JoltStatus::EXPIRED, 'invalid jolt status'); + assert(status == false, 'invalid return status'); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_fulfill_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 8460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5840); + + let status = dispatcher.fulfill_request(jolt_id); + + // check jolt data + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.jolt_type == JoltType::Request, 'invalid jolt type'); + assert(jolt_data.sender == ADDRESS2.try_into().unwrap(), 'invalid sender'); + assert(jolt_data.recipient == ADDRESS1.try_into().unwrap(), 'invalid recipient'); + assert(jolt_data.memo == "hey first request ever!", 'invalid memo'); + assert(jolt_data.amount == 2000000000000000000, 'invalid amount'); + assert(jolt_data.status == JoltStatus::SUCCESSFUL, 'invalid status'); + assert(jolt_data.expiration_stamp == 8460, 'invalid expiration stamp'); + assert(jolt_data.block_timestamp == 5640, 'invalid block stamp'); + assert(jolt_data.erc20_contract_address == erc20_contract_address, 'invalid address'); + assert(status == true, 'invalid return status'); + + // check that recipient received his tip + let balance = erc20_dispatcher.balance_of(ADDRESS2.try_into().unwrap()); + assert(balance == 2000000000000000000, 'incorrect balance'); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_jolt_event_is_emitted_on_request_fulfillment() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 8460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5840); + + let mut spy = spy_events(); + dispatcher.fulfill_request(jolt_id); + + // check for events + let expected_event = JoltRequestFulfillEvent::JoltRequestFullfilled( + JoltRequestFullfilled { + jolt_id: jolt_id, + jolt_type: 'REQUEST FULFILLMENT', + sender: ADDRESS1.try_into().unwrap(), + recipient: ADDRESS2.try_into().unwrap(), + expiration_timestamp: 8460, + block_timestamp: 5840, + } + ); + spy.assert_emitted(@array![(jolt_contract_address, expected_event)]); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} From 32daca024246f615406f695fbd6ffc9337eafacd Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Fri, 4 Oct 2024 04:17:53 +0100 Subject: [PATCH 09/11] chore: resolve conflicts --- tests/test_publication.cairo | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_publication.cairo b/tests/test_publication.cairo index 1d48715..ee13ed6 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -16,9 +16,6 @@ use karst::publication::publication::PublicationComponent::{ }; use karst::mocks::interfaces::IComposable::{IComposableDispatcher, IComposableDispatcherTrait}; use karst::base::constants::types::{PostParams, RepostParams, CommentParams, PublicationType}; - -use karst::mocks::interfaces::IComposable::{IComposableDispatcher, IComposableDispatcherTrait}; -use karst::base::constants::types::{PostParams, RepostParams, CommentParams, PublicationType,}; use karst::interfaces::ICollectNFT::{ICollectNFTDispatcher, ICollectNFTDispatcherTrait}; const HUB_ADDRESS: felt252 = 'HUB'; From 4d80dcaa6d2bbf4a4f0d0d66bbfb95e45604f7d2 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Fri, 4 Oct 2024 22:18:20 +0100 Subject: [PATCH 10/11] chore: more jolt tests --- src/base/constants/types.cairo | 2 +- src/interfaces.cairo | 1 + src/interfaces/IJolt.cairo | 7 +- src/interfaces/IUpgradeable.cairo | 9 + src/jolt/jolt.cairo | 116 +++++-- src/mocks.cairo | 1 + src/mocks/interfaces.cairo | 1 + src/mocks/interfaces/IJoltUpgrade.cairo | 13 + src/mocks/jolt_upgrade.cairo | 52 ++++ tests/test_jolt.cairo | 390 +++++++++++++++++++++++- 10 files changed, 556 insertions(+), 36 deletions(-) create mode 100644 src/interfaces/IUpgradeable.cairo create mode 100644 src/mocks/interfaces/IJoltUpgrade.cairo create mode 100644 src/mocks/jolt_upgrade.cairo diff --git a/src/base/constants/types.cairo b/src/base/constants/types.cairo index 9a19640..1f3ee9d 100644 --- a/src/base/constants/types.cairo +++ b/src/base/constants/types.cairo @@ -207,7 +207,7 @@ pub struct JoltParams { #[derive(Drop, Serde, starknet::Store)] pub struct RenewalData { - pub renewal_duration: u256, + pub renewal_iterations: u256, pub renewal_amount: u256, pub erc20_contract_address: ContractAddress } diff --git a/src/interfaces.cairo b/src/interfaces.cairo index e918ff6..d30ad84 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -10,3 +10,4 @@ pub mod IHandleRegistry; pub mod IHub; pub mod IJolt; pub mod ICollectNFT; +pub mod IUpgradeable; diff --git a/src/interfaces/IJolt.cairo b/src/interfaces/IJolt.cairo index 9b76dfd..1c8cc43 100644 --- a/src/interfaces/IJolt.cairo +++ b/src/interfaces/IJolt.cairo @@ -1,5 +1,5 @@ use starknet::ContractAddress; -use karst::base::constants::types::{JoltParams, JoltData}; +use karst::base::constants::types::{JoltParams, JoltData, RenewalData}; #[starknet::interface] pub trait IJolt { @@ -7,12 +7,13 @@ pub trait IJolt { // EXTERNALS // ************************************************************************* fn jolt(ref self: TState, jolt_params: JoltParams) -> u256; - fn set_fee_address(ref self: TState, _fee_address: ContractAddress); - fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; + fn auto_renew(ref self: TState, profile: ContractAddress, jolt_id: u256) -> bool; fn fulfill_request(ref self: TState, jolt_id: u256) -> bool; + fn set_fee_address(ref self: TState, _fee_address: ContractAddress); // ************************************************************************* // GETTERS // ************************************************************************* fn get_jolt(self: @TState, jolt_id: u256) -> JoltData; + fn get_renewal_data(self: @TState, profile: ContractAddress, jolt_id: u256) -> RenewalData; fn get_fee_address(self: @TState) -> ContractAddress; } diff --git a/src/interfaces/IUpgradeable.cairo b/src/interfaces/IUpgradeable.cairo new file mode 100644 index 0000000..e96214b --- /dev/null +++ b/src/interfaces/IUpgradeable.cairo @@ -0,0 +1,9 @@ +// ************************************************************************* +// UPGRADEABLE INTERFACE +// ************************************************************************* +use starknet::ClassHash; + +#[starknet::interface] +pub trait IUpgradeable { + fn upgrade(ref self: TContractState, new_class_hash: ClassHash); +} diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index ea93216..91d8cb5 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -111,6 +111,9 @@ pub mod Jolt { // ************************************************************************* // EXTERNALS // ************************************************************************* + + /// @notice multi-faceted transfer logic + /// @param jolt_params required jolting parameters fn jolt(ref self: ContractState, jolt_params: JoltParams) -> u256 { let sender = get_caller_address(); let tx_info = get_tx_info().unbox(); @@ -198,11 +201,8 @@ pub mod Jolt { return jolt_id; } - fn set_fee_address(ref self: ContractState, _fee_address: ContractAddress) { - self.ownable.assert_only_owner(); - self.fee_address.write(_fee_address); - } - + /// @notice fulfills a pending jolt request + /// @param jolt_id id of jolt request to be fulfilled fn fulfill_request(ref self: ContractState, jolt_id: u256) -> bool { // get jolt details let mut jolt_details = self.jolt.read(jolt_id); @@ -224,17 +224,43 @@ pub mod Jolt { self._fulfill_request(jolt_id, sender, jolt_details) } - fn auto_renew(ref self: ContractState, profile: ContractAddress, renewal_id: u256) -> bool { - self._auto_renew(profile, renewal_id) + /// @notice contains logic for auto renewal of subscriptions + /// @dev can be automated using cron jobs in a backend service + /// @param jolt_id id of jolt subscription to auto-renew + fn auto_renew(ref self: ContractState, profile: ContractAddress, jolt_id: u256) -> bool { + self._auto_renew(profile, jolt_id) + } + + /// @notice sets the fee address which receives subscription payments and maybe actual fees + /// in the future? + /// @param _fee_address address to be set + fn set_fee_address(ref self: ContractState, _fee_address: ContractAddress) { + self.ownable.assert_only_owner(); + self.fee_address.write(_fee_address); } // ************************************************************************* // GETTERS // ************************************************************************* + + /// @notice gets the associated data for a jolt id + /// @param jolt_id id of jolt who's data is to be gotten + /// @returns JoltData struct containing jolt details fn get_jolt(self: @ContractState, jolt_id: u256) -> JoltData { self.jolt.read(jolt_id) } + /// @notice gets the renewal data for a particular jolt id + /// @param jolt_id id of jolt who's renewal data is to be gotten + /// @returns RenewalData struct containing jolt renewal details + fn get_renewal_data( + self: @ContractState, profile: ContractAddress, jolt_id: u256 + ) -> RenewalData { + self.renewals.read((profile, jolt_id)) + } + + /// @notice gets the fee address + /// @returns the fee address for contract fn get_fee_address(self: @ContractState) -> ContractAddress { self.fee_address.read() } @@ -245,6 +271,8 @@ pub mod Jolt { // ************************************************************************* #[abi(embed_v0)] impl UpgradeableImpl of IUpgradeable { + /// @notice upgrades the contract + /// @param new_class_hash the class hash to upgrade to fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { self.ownable.assert_only_owner(); self.upgradeable.upgrade(new_class_hash); @@ -256,6 +284,13 @@ pub mod Jolt { // ************************************************************************* #[generate_trait] impl Private of PrivateTrait { + /// @notice contains the tipping logic + /// @param jolt_id id of txn + /// @param sender the profile performing the tipping + /// @param recipient the profile being tipped + /// @param amount the amount to be tipped + /// @param erc20_contract_address the address of token used in tipping + /// @returns JoltStatus status of the txn fn _tip( ref self: ContractState, jolt_id: u256, @@ -287,6 +322,13 @@ pub mod Jolt { JoltStatus::SUCCESSFUL } + /// @notice contains the transfer logic + /// @param jolt_id id of txn + /// @param sender the profile performing the transfer + /// @param recipient the profile being transferred to + /// @param amount the amount to be transferred + /// @param erc20_contract_address the address of token used + /// @returns JoltStatus status of the txn fn _transfer( ref self: ContractState, jolt_id: u256, @@ -318,6 +360,13 @@ pub mod Jolt { JoltStatus::SUCCESSFUL } + /// @notice contains the subscription logic + /// @param jolt_id id of txn + /// @param sender the profile performing the subscription + /// @param amount the amount to pay + /// @param auto_renewal a tuple containing renewal status and renewal_iterations + /// @param erc20_contract_address the address of token used + /// @returns JoltStatus status of the txn fn _subscribe( ref self: ContractState, jolt_id: u256, @@ -326,34 +375,22 @@ pub mod Jolt { auto_renewal: (bool, u256), erc20_contract_address: ContractAddress ) -> JoltStatus { - let (renewal_status, renewal_duration) = auto_renewal; + let (renewal_status, renewal_iterations) = auto_renewal; let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let this_contract = get_contract_address(); - let tx_info = get_tx_info().unbox(); if (renewal_status == true) { - // check allowances match auto-renew duration + // check allowances match auto-renew iterations let allowance = dispatcher.allowance(sender, this_contract); - assert(allowance >= renewal_duration * amount, Errors::INSUFFICIENT_ALLOWANCE); - - // generate renewal ID - let renewal_hash = PedersenTrait::new(0) - .update(sender.into()) - .update(jolt_id.low.into()) - .update(jolt_id.high.into()) - .update(tx_info.nonce) - .update(4) - .finalize(); - - let renewal_id: u256 = renewal_hash.try_into().unwrap(); + assert(allowance >= renewal_iterations * amount, Errors::INSUFFICIENT_ALLOWANCE); // write renewal details to storage let renewal_data = RenewalData { - renewal_duration: renewal_duration, + renewal_iterations: renewal_iterations, renewal_amount: amount, erc20_contract_address }; - self.renewals.write((sender, renewal_id), renewal_data); + self.renewals.write((sender, jolt_id), renewal_data); } // send subscription amount to fee address @@ -376,6 +413,14 @@ pub mod Jolt { JoltStatus::SUCCESSFUL } + /// @notice contains the request logic + /// @param jolt_id id of txn + /// @param sender the profile performing the request + /// @param recipient the profile being requested of + /// @param amount the amount to be tipped + /// @param expiration_timestamp timestamp of when the request will expire + /// @param erc20_contract_address the address of token used + /// @returns JoltStatus status of the txn fn _request( ref self: ContractState, jolt_id: u256, @@ -408,6 +453,10 @@ pub mod Jolt { JoltStatus::PENDING } + /// @notice internal logic to fulfill a request + /// @param sender the profile fulfilling the request + /// @param jolt_details details of the jolt to be fulfilled + /// @returns bool status of the txn fn _fulfill_request( ref self: ContractState, jolt_id: u256, sender: ContractAddress, jolt_details: JoltData ) -> bool { @@ -440,17 +489,21 @@ pub mod Jolt { return true; } + /// @notice internal logic to auto renew a subscription + /// @param sender the profile renewing a subscription + /// @param renewal_id id jolt to be renewed + /// @returns bool status of the txn fn _auto_renew(ref self: ContractState, sender: ContractAddress, renewal_id: u256) -> bool { let tx_info = get_tx_info().unbox(); let amount = self.renewals.read((sender, renewal_id)).renewal_amount; - let duration = self.renewals.read((sender, renewal_id)).renewal_duration; + let iteration = self.renewals.read((sender, renewal_id)).renewal_iterations; let erc20_contract_address = self .renewals .read((sender, renewal_id)) .erc20_contract_address; - // check duration is greater than 0 else shouldn't auto renew - assert(duration > 0, Errors::AUTO_RENEW_DURATION_ENDED); + // check iteration is greater than 0 else shouldn't auto renew + assert(iteration > 0, Errors::AUTO_RENEW_DURATION_ENDED); // send subscription amount to fee address let fee_address = self.fee_address.read(); @@ -467,9 +520,9 @@ pub mod Jolt { let jolt_id: u256 = jolt_hash.try_into().unwrap(); - // reduce duration by one month + // reduce iteration by one month let renewal_data = RenewalData { - renewal_duration: duration - 1, renewal_amount: amount, erc20_contract_address + renewal_iterations: iteration - 1, renewal_amount: amount, erc20_contract_address }; self.renewals.write((sender, renewal_id), renewal_data); @@ -506,6 +559,11 @@ pub mod Jolt { return true; } + /// @notice internal logic to perform an ERC20 transfer + /// @param erc20_contract_address address of the token to be transferred + /// @param sender profile sending the token + /// @param recipient profile receiving the token + /// @param amount amount to be transferred fn _transfer_helper( ref self: ContractState, erc20_contract_address: ContractAddress, diff --git a/src/mocks.cairo b/src/mocks.cairo index 231805c..e637541 100644 --- a/src/mocks.cairo +++ b/src/mocks.cairo @@ -1,3 +1,4 @@ pub mod registry; pub mod interfaces; pub mod ERC20; +pub mod jolt_upgrade; diff --git a/src/mocks/interfaces.cairo b/src/mocks/interfaces.cairo index 8468de9..b0cdc97 100644 --- a/src/mocks/interfaces.cairo +++ b/src/mocks/interfaces.cairo @@ -1 +1,2 @@ pub mod IComposable; +pub mod IJoltUpgrade; diff --git a/src/mocks/interfaces/IJoltUpgrade.cairo b/src/mocks/interfaces/IJoltUpgrade.cairo new file mode 100644 index 0000000..85087a5 --- /dev/null +++ b/src/mocks/interfaces/IJoltUpgrade.cairo @@ -0,0 +1,13 @@ +use karst::base::constants::types::{JoltParams}; + +#[starknet::interface] +pub trait IJoltUpgrade { + // ************************************************************************* + // EXTERNALS + // ************************************************************************* + fn jolt(ref self: TState, jolt_params: JoltParams) -> u256; + // ************************************************************************* + // GETTERS + // ************************************************************************* + fn version(self: @TState) -> u256; +} diff --git a/src/mocks/jolt_upgrade.cairo b/src/mocks/jolt_upgrade.cairo new file mode 100644 index 0000000..1d65ddf --- /dev/null +++ b/src/mocks/jolt_upgrade.cairo @@ -0,0 +1,52 @@ +#[starknet::contract] +pub mod JoltUpgrade { + // ************************************************************************* + // IMPORTS + // ************************************************************************* + use core::hash::HashStateTrait; + use core::pedersen::PedersenTrait; + use starknet::get_tx_info; + use karst::base::{constants::types::{JoltParams}}; + use karst::mocks::interfaces::IJoltUpgrade::IJoltUpgrade; + + // ************************************************************************* + // STORAGE + // ************************************************************************* + #[storage] + struct Storage {} + + // ************************************************************************* + // EVENTS + // ************************************************************************* + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event {} + + + // ************************************************************************* + // EXTERNALS + // ************************************************************************* + #[abi(embed_v0)] + impl JoltImpl of IJoltUpgrade { + fn jolt(ref self: ContractState, jolt_params: JoltParams) -> u256 { + let tx_info = get_tx_info().unbox(); + + // generate jolt_id + let jolt_hash = PedersenTrait::new(0) + .update(jolt_params.recipient.into()) + .update(jolt_params.amount.low.into()) + .update(jolt_params.amount.high.into()) + .update(tx_info.nonce) + .update(4) + .finalize(); + + let jolt_id: u256 = jolt_hash.try_into().unwrap(); + + return jolt_id; + } + + fn version(self: @ContractState) -> u256 { + 2 + } + } +} diff --git a/tests/test_jolt.cairo b/tests/test_jolt.cairo index 2b59287..591ca52 100644 --- a/tests/test_jolt.cairo +++ b/tests/test_jolt.cairo @@ -2,6 +2,7 @@ use core::traits::TryInto; use core::hash::HashStateTrait; use core::pedersen::PedersenTrait; use starknet::{ContractAddress, contract_address_const}; + use snforge_std::{ declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, stop_cheat_caller_address, start_cheat_nonce, stop_cheat_nonce, start_cheat_block_timestamp, @@ -9,14 +10,20 @@ use snforge_std::{ }; use karst::interfaces::IJolt::{IJoltDispatcher, IJoltDispatcherTrait}; use karst::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; -use karst::jolt::jolt::Jolt::{Event as JoltEvent, Jolted}; -use karst::jolt::jolt::Jolt::{Event as JoltRequestEvent, JoltRequested}; -use karst::jolt::jolt::Jolt::{Event as JoltRequestFulfillEvent, JoltRequestFullfilled}; +use karst::interfaces::IUpgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; + +use karst::jolt::jolt::Jolt::{ + {Event as JoltEvent, Jolted}, {Event as JoltRequestEvent, JoltRequested}, + {Event as JoltRequestFulfillEvent, JoltRequestFullfilled}, +}; + use karst::base::{constants::types::{JoltParams, JoltType, JoltStatus}}; +use karst::mocks::interfaces::IJoltUpgrade::{IJoltUpgradeDispatcher, IJoltUpgradeDispatcherTrait}; const ADMIN: felt252 = 5382942; const ADDRESS1: felt252 = 254290; const ADDRESS2: felt252 = 525616; +const FEE_ADDRESS: felt252 = 250322; // ************************************************************************* // SETUP @@ -849,3 +856,380 @@ fn test_jolt_event_is_emitted_on_request_fulfillment() { stop_cheat_block_timestamp(jolt_contract_address); stop_cheat_caller_address(jolt_contract_address); } + +// ************************************************************************* +// TEST - SUBSCRIPTION +// ************************************************************************* +#[test] +fn test_jolt_subscription() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Subscription, + recipient: contract_address_const::<0>(), + memo: "hey first subscription ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (true, 1), + erc20_contract_address: erc20_contract_address + }; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); + dispatcher.set_fee_address(FEE_ADDRESS.try_into().unwrap()); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + start_cheat_nonce(jolt_contract_address, 23); + + let jolt_id = dispatcher.jolt(jolt_params); + + // check jolt data + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.jolt_type == JoltType::Subscription, 'invalid jolt type'); + assert(jolt_data.sender == ADDRESS1.try_into().unwrap(), 'invalid sender'); + assert(jolt_data.memo == "hey first subscription ever!", 'invalid memo'); + assert(jolt_data.amount == 2000000000000000000, 'invalid amount'); + assert(jolt_data.status == JoltStatus::SUCCESSFUL, 'invalid status'); + assert(jolt_data.block_timestamp == 36000, 'invalid block stamp'); + assert(jolt_data.erc20_contract_address == erc20_contract_address, 'invalid address'); + + // check that fee_address received sub amount + let balance = erc20_dispatcher.balance_of(FEE_ADDRESS.try_into().unwrap()); + assert(balance == 2000000000000000000, 'incorrect balance'); + + // check that renewal data was updated + let renewal_data = dispatcher.get_renewal_data(ADDRESS1.try_into().unwrap(), jolt_id); + assert(renewal_data.renewal_iterations == 1, 'invalid iteration count'); + assert(renewal_data.renewal_amount == 2000000000000000000, 'invalid renewal amount'); + assert(renewal_data.erc20_contract_address == erc20_contract_address, 'invalid erc20'); + + stop_cheat_nonce(jolt_contract_address); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: insufficient allowance!',))] +fn test_jolt_subscription_fails_if_insufficient_allowance() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Subscription, + recipient: contract_address_const::<0>(), + memo: "hey first subscription ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (true, 5), + erc20_contract_address: erc20_contract_address + }; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); + dispatcher.set_fee_address(FEE_ADDRESS.try_into().unwrap()); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 4000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + start_cheat_nonce(jolt_contract_address, 23); + + dispatcher.jolt(jolt_params); + + stop_cheat_nonce(jolt_contract_address); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_jolt_event_is_emitted_on_subscription() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Subscription, + recipient: contract_address_const::<0>(), + memo: "hey first subscription ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (true, 5), + erc20_contract_address: erc20_contract_address + }; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); + dispatcher.set_fee_address(FEE_ADDRESS.try_into().unwrap()); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 10000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + + let mut spy = spy_events(); + let jolt_id = dispatcher.jolt(jolt_params); + + // check for events + let expected_event = JoltEvent::Jolted( + Jolted { + jolt_id: jolt_id, + jolt_type: 'SUBSCRIPTION', + sender: ADDRESS1.try_into().unwrap(), + recipient: FEE_ADDRESS.try_into().unwrap(), + block_timestamp: 36000, + } + ); + spy.assert_emitted(@array![(jolt_contract_address, expected_event)]); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_auto_renewal() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + // user first need to subscribe + let jolt_params = JoltParams { + jolt_type: JoltType::Subscription, + recipient: contract_address_const::<0>(), + memo: "hey first subscription ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (true, 5), + erc20_contract_address: erc20_contract_address + }; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); + dispatcher.set_fee_address(FEE_ADDRESS.try_into().unwrap()); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 10000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + start_cheat_nonce(jolt_contract_address, 23); + + let jolt_id = dispatcher.jolt(jolt_params); + + // try to auto renew thrice + dispatcher.auto_renew(ADDRESS1.try_into().unwrap(), jolt_id); + dispatcher.auto_renew(ADDRESS1.try_into().unwrap(), jolt_id); + dispatcher.auto_renew(ADDRESS1.try_into().unwrap(), jolt_id); + + // check if auto renewal worked + let renewal_data = dispatcher.get_renewal_data(ADDRESS1.try_into().unwrap(), jolt_id); + assert(renewal_data.renewal_iterations == 2, 'invalid iteration count'); + assert(renewal_data.renewal_amount == 2000000000000000000, 'invalid renewal amount'); + assert(renewal_data.erc20_contract_address == erc20_contract_address, 'invalid erc20'); + + // check that fee_address received sub amount plus renewal amounts + let balance = erc20_dispatcher.balance_of(FEE_ADDRESS.try_into().unwrap()); + assert(balance == 8000000000000000000, 'incorrect balance'); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: auto renew ended!',))] +fn test_auto_renewal_fails_once_iteration_count_is_zero() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + // user first need to subscribe + let jolt_params = JoltParams { + jolt_type: JoltType::Subscription, + recipient: contract_address_const::<0>(), + memo: "hey first subscription ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (true, 2), + erc20_contract_address: erc20_contract_address + }; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); + dispatcher.set_fee_address(FEE_ADDRESS.try_into().unwrap()); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 10000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + start_cheat_nonce(jolt_contract_address, 23); + + let jolt_id = dispatcher.jolt(jolt_params); + + // try to auto renew thrice - should fail on third try + dispatcher.auto_renew(ADDRESS1.try_into().unwrap(), jolt_id); + dispatcher.auto_renew(ADDRESS1.try_into().unwrap(), jolt_id); + dispatcher.auto_renew(ADDRESS1.try_into().unwrap(), jolt_id); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_auto_renewal_emits_susbcription_event() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + // user first need to subscribe + let jolt_params = JoltParams { + jolt_type: JoltType::Subscription, + recipient: contract_address_const::<0>(), + memo: "hey first subscription ever!", + amount: 2000000000000000000, + expiration_stamp: 0, + auto_renewal: (true, 2), + erc20_contract_address: erc20_contract_address + }; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); + dispatcher.set_fee_address(FEE_ADDRESS.try_into().unwrap()); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 10000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + start_cheat_nonce(jolt_contract_address, 23); + + let jolt_id = dispatcher.jolt(jolt_params); + + // try to auto renew + let mut spy = spy_events(); + dispatcher.auto_renew(ADDRESS1.try_into().unwrap(), jolt_id); + + // generate expected renewal jolt_id + let renewal_jolt_hash = PedersenTrait::new(0) + .update(FEE_ADDRESS.try_into().unwrap()) + .update(2000000000000000000) + .update(0) + .update(23) + .update(4) + .finalize(); + + let renewal_jolt_id: u256 = renewal_jolt_hash.try_into().unwrap(); + + // check for events + let expected_event = JoltEvent::Jolted( + Jolted { + jolt_id: renewal_jolt_id, + jolt_type: 'SUBSCRIPTION', + sender: ADDRESS1.try_into().unwrap(), + recipient: FEE_ADDRESS.try_into().unwrap(), + block_timestamp: 36000, + } + ); + spy.assert_emitted(@array![(jolt_contract_address, expected_event)]); + + stop_cheat_nonce(jolt_contract_address); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +// ************************************************************************* +// TEST - FEE ADDRESS +// ************************************************************************* +#[test] +fn test_set_fee_address() { + let (jolt_contract_address, _) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); + dispatcher.set_fee_address(FEE_ADDRESS.try_into().unwrap()); + stop_cheat_caller_address(jolt_contract_address); + + // check fee address + let fee_address = dispatcher.get_fee_address(); + assert(fee_address == FEE_ADDRESS.try_into().unwrap(), 'invalid fee address'); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_only_admin_can_set_fee_address() { + let (jolt_contract_address, _) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + dispatcher.set_fee_address(FEE_ADDRESS.try_into().unwrap()); + stop_cheat_caller_address(jolt_contract_address); +} + +// ************************************************************************* +// TEST - UPGRADE +// ************************************************************************* +#[test] +fn test_upgrade() { + let (jolt_contract_address, _) = __setup__(); + let dispatcher = IJoltUpgradeDispatcher { contract_address: jolt_contract_address }; + let upgrade_dispatcher = IUpgradeableDispatcher { contract_address: jolt_contract_address }; + let upgraded_class = declare("JoltUpgrade").unwrap().contract_class(); + let new_class_hash = *upgraded_class.class_hash; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); + upgrade_dispatcher.upgrade(new_class_hash); + stop_cheat_caller_address(jolt_contract_address); + + // check if upgrade worked by calling version which didn't previously exist + let version = dispatcher.version(); + assert(version == 2, 'failed to upgrade'); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_upgrade_fails_if_not_admin() { + let (jolt_contract_address, _) = __setup__(); + let upgrade_dispatcher = IUpgradeableDispatcher { contract_address: jolt_contract_address }; + let upgraded_class = declare("JoltUpgrade").unwrap().contract_class(); + let new_class_hash = *upgraded_class.class_hash; + + // set fee address + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + upgrade_dispatcher.upgrade(new_class_hash); + stop_cheat_caller_address(jolt_contract_address); +} From 195e5392f4e25daebe04773bc1dc8a886d20ba63 Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Fri, 4 Oct 2024 22:20:12 +0100 Subject: [PATCH 11/11] fix: jolt tests --- tests/test_jolt.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_jolt.cairo b/tests/test_jolt.cairo index 591ca52..d8076a0 100644 --- a/tests/test_jolt.cairo +++ b/tests/test_jolt.cairo @@ -1210,7 +1210,7 @@ fn test_upgrade() { let upgraded_class = declare("JoltUpgrade").unwrap().contract_class(); let new_class_hash = *upgraded_class.class_hash; - // set fee address + // upgrade start_cheat_caller_address(jolt_contract_address, ADMIN.try_into().unwrap()); upgrade_dispatcher.upgrade(new_class_hash); stop_cheat_caller_address(jolt_contract_address); @@ -1228,7 +1228,7 @@ fn test_upgrade_fails_if_not_admin() { let upgraded_class = declare("JoltUpgrade").unwrap().contract_class(); let new_class_hash = *upgraded_class.class_hash; - // set fee address + // try to upgrade start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); upgrade_dispatcher.upgrade(new_class_hash); stop_cheat_caller_address(jolt_contract_address);