diff --git a/Scarb.toml b/Scarb.toml index 575ff04..f00def2 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -18,7 +18,6 @@ alexandria_bytes = { git = "https://github.com/keep-starknet-strange/alexandria. [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" diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index ed767e5..cce1bff 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -25,4 +25,12 @@ pub mod Errors { pub const NOT_MEMBER: felt252 = 'Karst: Not a Community Member'; pub const NOT_TOKEN_OWNER: felt252 = 'Karst: Not a Token Owner'; pub const TOKEN_DOES_NOT_EXIST: felt252 = 'Karst: Token does not exist'; + 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!'; + 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 49ba1e4..60cd615 100644 --- a/src/base/constants/types.cairo +++ b/src/base/constants/types.cairo @@ -212,3 +212,89 @@ pub enum CommunityType { Standard, Business } + +#[derive(Debug, Drop, Serde, starknet::Store, Clone)] +pub struct Upvote { + pub publication_id: u256, + pub transaction_executor: ContractAddress, + pub block_timestamp: u64, +} + +#[derive(Debug, Drop, Serde, starknet::Store, Clone)] +pub struct Downvote { + pub publication_id: u256, + 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 status: JoltStatus, + pub expiration_stamp: u64, + pub block_timestamp: u64, + pub erc20_contract_address: ContractAddress +} + +#[derive(Drop, Serde)] +pub struct JoltParams { + pub jolt_type: JoltType, + pub recipient: ContractAddress, + pub memo: ByteArray, + pub amount: u256, + pub expiration_stamp: u64, + pub auto_renewal: (bool, u256), + pub erc20_contract_address: ContractAddress, +} + +#[derive(Drop, Serde, starknet::Store)] +pub struct RenewalData { + pub renewal_iterations: u256, + pub renewal_amount: u256, + pub erc20_contract_address: ContractAddress +} + +#[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, + REJECTED, + FAILED +} \ No newline at end of file diff --git a/src/interfaces.cairo b/src/interfaces.cairo index aad3b7f..7eadf74 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; @@ -11,3 +12,7 @@ pub mod ICommunity; pub mod ICollectNFT; pub mod ICommunityNft; +pub mod IJolt; +pub mod ICollectNFT; +pub mod IUpgradeable; + 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..1c8cc43 --- /dev/null +++ b/src/interfaces/IJolt.cairo @@ -0,0 +1,19 @@ +use starknet::ContractAddress; +use karst::base::constants::types::{JoltParams, JoltData, RenewalData}; + +#[starknet::interface] +pub trait IJolt { + // ************************************************************************* + // EXTERNALS + // ************************************************************************* + fn jolt(ref self: TState, jolt_params: JoltParams) -> u256; + 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.cairo b/src/jolt.cairo new file mode 100644 index 0000000..5e01eb0 --- /dev/null +++ b/src/jolt.cairo @@ -0,0 +1 @@ +pub mod jolt; diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo new file mode 100644 index 0000000..91d8cb5 --- /dev/null +++ b/src/jolt/jolt.cairo @@ -0,0 +1,584 @@ +#[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, + storage::{ + StoragePointerWriteAccess, StoragePointerReadAccess, Map, StorageMapReadAccess, + StorageMapWriteAccess + } + }; + use karst::base::{ + constants::errors::Errors, + constants::types::{JoltData, JoltParams, JoltType, JoltStatus, RenewalData} + }; + 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::, + renewals: Map::<(ContractAddress, u256), RenewalData>, + } + + // ************************************************************************* + // EVENTS + // ************************************************************************* + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + Jolted: Jolted, + JoltRequested: JoltRequested, + JoltRequestFullfilled: JoltRequestFullfilled, + } + + #[derive(Drop, starknet::Event)] + pub struct Jolted { + 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 { + 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 { + 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; + + // ************************************************************************* + // CONSTRUCTOR + // ************************************************************************* + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl JoltImpl of IJolt { + // ************************************************************************* + // 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(); + 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(); + + // jolt + 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 _jolt_status = self + ._tip( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + erc20_contract_address + ); + jolt_status = _jolt_status; + }, + JoltType::Transfer => { + let _jolt_status = self + ._transfer( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + erc20_contract_address + ); + jolt_status = _jolt_status; + }, + JoltType::Subscription => { + let _jolt_status = self + ._subscribe( + jolt_id, + sender, + jolt_params.amount, + jolt_params.auto_renewal, + erc20_contract_address + ); + jolt_status = _jolt_status; + }, + JoltType::Request => { + let _jolt_status = self + ._request( + jolt_id, + sender, + jolt_params.recipient, + jolt_params.amount, + jolt_params.expiration_stamp, + erc20_contract_address + ); + jolt_status = _jolt_status; + } + }; + + // 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, + status: jolt_status, + expiration_stamp: jolt_params.expiration_stamp, + block_timestamp: tx_timestamp, + erc20_contract_address: jolt_params.erc20_contract_address + }; + + self.jolt.write(jolt_id, jolt_data); + return jolt_id; + } + + /// @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); + 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); + + // 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) + } + + /// @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() + } + } + + // ************************************************************************* + // UPGRADEABLE IMPL + // ************************************************************************* + #[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); + } + } + + // ************************************************************************* + // PRIVATE FUNCTIONS + // ************************************************************************* + #[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, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + erc20_contract_address: ContractAddress + ) -> 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); + + // tip user + self._transfer_helper(erc20_contract_address, sender, recipient, amount); + + // emit event + self + .emit( + Jolted { + jolt_id, + jolt_type: 'TIP', + sender, + recipient: recipient, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + 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, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + erc20_contract_address: ContractAddress + ) -> 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 + self._transfer_helper(erc20_contract_address, sender, recipient, amount); + + // emit event + self + .emit( + Jolted { + jolt_id, + jolt_type: 'TRANSFER', + sender, + recipient: recipient, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + 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, + sender: ContractAddress, + amount: u256, + auto_renewal: (bool, u256), + erc20_contract_address: ContractAddress + ) -> JoltStatus { + let (renewal_status, renewal_iterations) = auto_renewal; + let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + let this_contract = get_contract_address(); + + if (renewal_status == true) { + // check allowances match auto-renew iterations + let allowance = dispatcher.allowance(sender, this_contract); + assert(allowance >= renewal_iterations * amount, Errors::INSUFFICIENT_ALLOWANCE); + + // write renewal details to storage + let renewal_data = RenewalData { + renewal_iterations: renewal_iterations, + renewal_amount: amount, + erc20_contract_address + }; + self.renewals.write((sender, jolt_id), renewal_data); + } + + // send subscription amount to fee address + let fee_address = self.fee_address.read(); + self._transfer_helper(erc20_contract_address, 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 + 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, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + expiration_timestamp: u64, + erc20_contract_address: ContractAddress + ) -> JoltStatus { + // 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 + .emit( + JoltRequested { + jolt_id, + jolt_type: 'REQUEST', + sender, + recipient: recipient, + expiration_timestamp, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + 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 { + // transfer request 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 }; + self.jolt.write(jolt_id, jolt_data); + + // emit events + self + .emit( + JoltRequestFullfilled { + jolt_id, + jolt_type: 'REQUEST FULFILLMENT', + sender: jolt_details.recipient, + recipient: jolt_details.sender, + expiration_timestamp: jolt_details.expiration_stamp, + block_timestamp: get_block_timestamp(), + } + ); + + 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 iteration = self.renewals.read((sender, renewal_id)).renewal_iterations; + let erc20_contract_address = self + .renewals + .read((sender, renewal_id)) + .erc20_contract_address; + + // 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(); + self._transfer_helper(erc20_contract_address, 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 iteration by one month + let renewal_data = RenewalData { + renewal_iterations: iteration - 1, renewal_amount: amount, erc20_contract_address + }; + self.renewals.write((sender, renewal_id), renewal_data); + + // 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, + status: JoltStatus::SUCCESSFUL, + expiration_stamp: 0, + block_timestamp: get_block_timestamp(), + erc20_contract_address + }; + + // write to storage + self.jolt.write(jolt_id, jolt_data); + + // emit event + self + .emit( + Jolted { + jolt_id, + jolt_type: 'SUBSCRIPTION', + sender, + recipient: fee_address, + block_timestamp: get_block_timestamp(), + } + ); + + // return txn status + 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, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + // check allowance + let allowance = dispatcher.allowance(sender, get_contract_address()); + assert(allowance >= amount, Errors::INSUFFICIENT_ALLOWANCE); + + // transfer to recipient + dispatcher.transfer_from(sender, recipient, amount); + } + } +} diff --git a/src/lib.cairo b/src/lib.cairo index c88c27b..f9f77bd 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -8,6 +8,7 @@ pub mod publication; pub mod namespaces; pub mod presets; pub mod hub; +pub mod jolt; pub mod collectnft; pub mod community; pub mod communitynft; diff --git a/src/mocks.cairo b/src/mocks.cairo index 4dc0e24..e637541 100644 --- a/src/mocks.cairo +++ b/src/mocks.cairo @@ -1,2 +1,4 @@ pub mod registry; pub mod interfaces; +pub mod ERC20; +pub mod jolt_upgrade; diff --git a/src/mocks/ERC20.cairo b/src/mocks/ERC20.cairo new file mode 100644 index 0000000..c184133 --- /dev/null +++ b/src/mocks/ERC20.cairo @@ -0,0 +1,34 @@ +#[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); + } +} 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_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_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_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..d8076a0 --- /dev/null +++ b/tests/test_jolt.cairo @@ -0,0 +1,1235 @@ +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, + stop_cheat_block_timestamp, spy_events, EventSpyAssertionsTrait +}; +use karst::interfaces::IJolt::{IJoltDispatcher, IJoltDispatcherTrait}; +use karst::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; +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 +// ************************************************************************* +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(); + + // 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 - 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 +// ************************************************************************* +#[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); +} + +// ************************************************************************* +// 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; + + // 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); + + // 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; + + // 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); +} 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 63ca18b..ee13ed6 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -14,9 +14,8 @@ use snforge_std::{ 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}; use karst::interfaces::ICollectNFT::{ICollectNFTDispatcher, ICollectNFTDispatcherTrait}; const HUB_ADDRESS: felt252 = 'HUB';