diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..458b991 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store + +.snfoundry_cache/ +target/ \ No newline at end of file diff --git a/README.md b/README.md index d0fa90c..01d4fae 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ -# gmx-referral-fork-cairo \ No newline at end of file +# JediSwap Referral Contracts + +This repository consists of the referral contracts of JediSwap protocol, a cairo fork of Uniswap V3. CLAMM for Starknet. + +## Testing and Development + +Prerequisites: + +- [Scarb](https://github.com/software-mansion/scarb) for managing the project. +- [starknet-foundry](https://github.com/foundry-rs/starknet-foundry) for testing and writing scripts. + +### Compile Contracts + +``` +scarb build +``` + +### Run Tests + +``` +snforge test +``` + +### Run Scripts + +#### Run a local devnet + +We use [starknet-devnet-rs](https://github.com/0xSpaceShard/starknet-devnet-rs) + +Run the devnet and add one of the predeployed accounts with your preferred name . See the instructions [here](https://foundry-rs.github.io/starknet-foundry/starknet/account.html#importing-an-account). + +#### Run Scripts + +Run sncast in the parent folder by specifying the path to the script file. Example: + +``` +sncast --url http://127.0.0.1:5050 --account --path-to-scarb-toml scripts/Scarb.toml script deploy_factory_and_pool +``` diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 0000000..1b0828d --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,20 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "jediswap_referral" +version = "0.1.0" +dependencies = [ + "openzeppelin", + "snforge_std", +] + +[[package]] +name = "openzeppelin" +version = "0.11.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.11.0#a83f36b23f1af6e160288962be4a2701c3ecbcda" + +[[package]] +name = "snforge_std" +version = "0.20.1" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.20.1#fea2db8f2b20148cc15ee34b08de12028eb42942" diff --git a/Scarb.toml b/Scarb.toml new file mode 100644 index 0000000..2d690bb --- /dev/null +++ b/Scarb.toml @@ -0,0 +1,17 @@ +[package] +name = "jediswap_referral" +version = "0.1.0" + +[lib] + + +[[target.starknet-contract]] +sierra = true +casm = true +casm-add-pythonic-hints = true +allowed-libfuncs-list.name = "all" + +[dependencies] +starknet = "2.4.3" +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.11.0" } +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.20.1" } \ No newline at end of file diff --git a/snfoundry.toml b/snfoundry.toml new file mode 100644 index 0000000..da7836f --- /dev/null +++ b/snfoundry.toml @@ -0,0 +1,4 @@ +[sncast.default] +account = "my_imported_account" +accounts-file = "~/my_accounts.json" +url = "https://api-starknet-sepolia.dwellir.com/dd28e566-3260-4d8d-8180-6ef1a161e41c" \ No newline at end of file diff --git a/src/lib.cairo b/src/lib.cairo new file mode 100644 index 0000000..9cfe5fa --- /dev/null +++ b/src/lib.cairo @@ -0,0 +1,7 @@ +mod referral_storage; + +mod test_contracts { + mod referral_storage_v2; +} + + diff --git a/src/referral_storage.cairo b/src/referral_storage.cairo new file mode 100644 index 0000000..08b7337 --- /dev/null +++ b/src/referral_storage.cairo @@ -0,0 +1,109 @@ +use starknet::{ContractAddress,ClassHash}; +use zeroable::Zeroable; + + +#[starknet::interface] +trait IReferralStorage { + fn get_referrer( + self: @TContractState, + account: ContractAddress, + ) -> ContractAddress; + + fn set_referrer( + ref self: TContractState, + address: ContractAddress, + ); + + fn upgrade(ref self: TContractState, new_class_hash: ClassHash); +} + + +#[starknet::contract] +mod ReferralStorage { + use starknet::ContractAddress; + use starknet::get_caller_address; + use zeroable::Zeroable; + use starknet::ClassHash; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::UpgradeableComponent; + use openzeppelin::upgrades::interface::IUpgradeable; + + component!(path: UpgradeableComponent, storage: upgradeable_storage, event: UpgradeableEvent); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + /// Ownable + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + /// Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + SetReferrer: SetReferrer, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + + #[derive(Drop, starknet::Event)] + struct SetReferrer { + account: ContractAddress, + referrer: ContractAddress, + } + + #[storage] + struct Storage { + // @notice Maps the referee to the referrer + referrers: LegacyMap::, + + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable_storage: UpgradeableComponent::Storage, + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl ReferralStorage of super::IReferralStorage { + + fn get_referrer( + self: @ContractState, + account: ContractAddress, + ) -> ContractAddress { + self.referrers.read(account) + } + + // @notice Sets the referrer to be referred by the caller + // @param address The referrer to set + // @dev An address can set the referrer only once + // @dev referrer cannot set refer themselves + fn set_referrer( + ref self: ContractState, + address: ContractAddress, + ){ + assert!(address != get_caller_address(), "ReferralStorage: referrer cannot refer themselves"); + + if(!self.referrers.read(get_caller_address()).is_non_zero()){ + let account = get_caller_address(); + self.referrers.write(account, address); + self.emit(SetReferrer{account:account, referrer: address}); + } + } + + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + + self.upgradeable_storage._upgrade(new_class_hash); + } + } +} + diff --git a/src/test_contracts/referral_storage_v2.cairo b/src/test_contracts/referral_storage_v2.cairo new file mode 100644 index 0000000..d671d4a --- /dev/null +++ b/src/test_contracts/referral_storage_v2.cairo @@ -0,0 +1,159 @@ +use starknet::{ContractAddress,ClassHash}; +use zeroable::Zeroable; + + +#[starknet::interface] +trait IReferralStorageV2 { + fn get_referrer( + ref self: TContractState, + _account: ContractAddress, + ) -> felt252; + + fn get_code_owner( + ref self: TContractState, + _code: felt252, + ) -> ContractAddress; + + fn get_code_from_owner( + ref self: TContractState, + _account: ContractAddress, + ) -> felt252; + + fn set_referrer( + ref self: TContractState, + _code: felt252, + ); + + fn register_code( + ref self: TContractState, + _code: felt252, + ); + + fn upgrade(ref self: TContractState, new_class_hash: ClassHash); + +} + + +#[starknet::contract] +mod ReferralStorageV2 { + use starknet::ContractAddress; + use starknet::get_caller_address; + use zeroable::Zeroable; + use starknet::ClassHash; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::UpgradeableComponent; + use openzeppelin::upgrades::interface::IUpgradeable; + + + component!(path: UpgradeableComponent, storage: upgradeable_storage, event: UpgradeableEvent); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + SetReferrer: SetReferrer, + RegisterCode: RegisterCode, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + + #[derive(Drop, starknet::Event)] + struct SetReferrer { + account: ContractAddress, + code: felt252, + } + + #[derive(Drop, starknet::Event)] + struct RegisterCode { + code: felt252, + account: ContractAddress, + } + + #[storage] + struct Storage { + code_owner: LegacyMap::, + owner_to_code: LegacyMap::, + referrers: LegacyMap::, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable_storage: UpgradeableComponent::Storage, + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.ownable.initializer(owner); + } + + /// Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + /// Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[abi(embed_v0)] + impl ReferralStorageV2 of super::IReferralStorageV2 { + + fn get_referrer( + ref self: ContractState, + _account: ContractAddress, + ) -> felt252 { + self.referrers.read(_account) + } + + fn get_code_owner( + ref self: ContractState, + _code: felt252, + ) -> ContractAddress { + self.code_owner.read(_code) + } + + fn get_code_from_owner( + ref self: ContractState, + _account: ContractAddress, + ) -> felt252 { + self.owner_to_code.read(_account) + } + + fn set_referrer( + ref self: ContractState, + _code: felt252, + ){ + assert!(self.code_owner.read(_code).is_non_zero(), "ReferralStorage: code not found"); + assert!(self.code_owner.read(_code) != get_caller_address(), "ReferralStorage: referrer cannot refer themselves"); + + let _account = get_caller_address(); + self.referrers.write(_account, _code); + self.emit(SetReferrer{account:_account, code: _code}); + } + + fn register_code( + ref self: ContractState, + _code: felt252, + ){ + assert!(self.owner_to_code.read(get_caller_address()).is_non_zero(), "ReferralStorage: user already has a code"); + + assert!(!self.code_owner.read(_code).is_non_zero(), "ReferralStorage: code already registered"); + + self.code_owner.write(_code, get_caller_address()); + self.owner_to_code.write(get_caller_address(), _code); + + self.emit(RegisterCode{code:_code, account: get_caller_address()}); + } + + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + + self.upgradeable_storage._upgrade(new_class_hash); + } + } + + + +} + diff --git a/tests/lib.cairo b/tests/lib.cairo new file mode 100644 index 0000000..6e98ca5 --- /dev/null +++ b/tests/lib.cairo @@ -0,0 +1 @@ +mod referral_storage_test; \ No newline at end of file diff --git a/tests/referral_storage_test.cairo b/tests/referral_storage_test.cairo new file mode 100644 index 0000000..3fb4237 --- /dev/null +++ b/tests/referral_storage_test.cairo @@ -0,0 +1,77 @@ +use starknet::{ContractAddress,get_caller_address,contract_address_try_from_felt252}; +use zeroable::Zeroable; +use jediswap_referral::referral_storage::{ReferralStorage,IReferralStorageDispatcher,IReferralStorageDispatcherTrait}; +use snforge_std::{ + declare, ContractClassTrait, start_prank, stop_prank, CheatTarget, spy_events, + SpyOn, EventSpy, EventFetcher, Event, EventAssertions +}; +use jediswap_referral::test_contracts::referral_storage_v2::{ReferralStorageV2,IReferralStorageV2Dispatcher,IReferralStorageV2DispatcherTrait}; +use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + + +fn setup_referral_storage_dispatcher() -> (ContractAddress,IReferralStorageDispatcher,ContractAddress) { + let contract = declare("ReferralStorage"); + + let mut contract_constructor_calldata = Default::default(); + let owner = contract_address_try_from_felt252('owner').unwrap(); + + Serde::serialize(@owner, ref contract_constructor_calldata); + let contract_address = contract.deploy(@contract_constructor_calldata).unwrap(); + + (contract_address,IReferralStorageDispatcher { contract_address },owner) +} + +#[test] +fn test_set_referrer() { + let (contract_address,dispatcher,_owner) = setup_referral_storage_dispatcher(); + + let user = 123.try_into().unwrap(); + + let user2 = 124.try_into().unwrap(); + start_prank(CheatTarget::One(contract_address), user2); + dispatcher.set_referrer(user); + + let user2_referral_code = dispatcher.get_referrer(user2); + assert_eq!(user2_referral_code, user); +} + +#[test] +#[should_panic(expected: ("ReferralStorage: referrer cannot refer themselves",))] +fn test_set_referrer_self() { + let (contract_address,dispatcher,_owner) = setup_referral_storage_dispatcher(); + + let user = 123.try_into().unwrap(); + start_prank(CheatTarget::One(contract_address), user); + dispatcher.set_referrer(user); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_upgrade_referral_storage_by_not_onwer() { + let (_contract_address,dispatcher,_owner) = setup_referral_storage_dispatcher(); + + let new_storage_class_hash = declare("ReferralStorageV2").class_hash; + dispatcher.upgrade(new_storage_class_hash); +} + +#[test] +fn test_upgrade_referral_storage() { + let (contract_address,dispatcher,owner) = setup_referral_storage_dispatcher(); + + let new_storage_class_hash = declare("ReferralStorageV2").class_hash; + let mut spy = spy_events(SpyOn::One(contract_address)); + start_prank(CheatTarget::One(contract_address), owner); + dispatcher.upgrade(new_storage_class_hash); + + + spy.assert_emitted( + @array![ + ( + contract_address, + UpgradeableComponent::Event::Upgraded( + UpgradeableComponent::Upgraded { class_hash: new_storage_class_hash } + ) + ) + ] + ); +}