From 6df63d48e14ed1dfcb1da96a38f309437a682933 Mon Sep 17 00:00:00 2001 From: Khaeljy Date: Sat, 16 Sep 2023 22:23:26 +0200 Subject: [PATCH 1/4] feat: Implement router contract for JediSwap Fixes #7 --- Scarb.toml | 5 +- src/account/account.cairo | 27 +++-- src/lib.cairo | 6 ++ src/router/error.cairo | 5 + src/router/jedi_swap_router.cairo | 105 +++++++++++++++++++ src/router/router.cairo | 59 +++++++++++ src/tests.cairo | 6 ++ src/tests/router/test_jedi_swap_router.cairo | 93 ++++++++++++++++ src/tests/test_account.cairo | 7 ++ 9 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 src/router/error.cairo create mode 100644 src/router/jedi_swap_router.cairo create mode 100644 src/router/router.cairo create mode 100644 src/tests/router/test_jedi_swap_router.cairo diff --git a/Scarb.toml b/Scarb.toml index 3e58aff..90687c5 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -7,4 +7,7 @@ version = "0.0.1" [dependencies] starknet = ">=2.2.0" -[[target.starknet-contract]] \ No newline at end of file +[[target.starknet-contract]] + +[scripts] +test = "scarb fmt && scarb cairo-test" \ No newline at end of file diff --git a/src/account/account.cairo b/src/account/account.cairo index f824914..4bd734e 100644 --- a/src/account/account.cairo +++ b/src/account/account.cairo @@ -30,29 +30,44 @@ const SRC6_TRAIT_ID: felt252 = #[starknet::interface] trait IAccount { /// Initialize the contract - /// @param public_key The public key allowed to interact with the account. + /// + /// # Arguments + /// + /// * `public_key` - The public key allowed to interact with the account fn initialize(ref self: TContractState, public_key: felt252); /// Execute a transaction through the account - /// @param calls The list of calls to execute + /// + /// # Arguments + /// + /// * `calls` - The list of calls to execute /// @return The list of each call's serialized return value fn __execute__(ref self: TContractState, calls: Array) -> Array>; /// Assert whether the transaction is valid to be executed - /// @param calls The list of calls to execute + /// + /// # Arguments + /// + /// * `calls` - The list of calls to validate /// @return The string 'VALID' represented as felt when is valid fn __validate__(ref self: TContractState, calls: Array) -> felt252; /// Assert whether a given signature for a given hash is valid - /// @param hash The hash of the data - /// @param signature The signature to validate + /// + /// # Arguments + /// + /// * `hash` - The hash of the data + /// * `signature` - The signature to validate /// @return The string 'VALID' represented as felt when the signature is valid fn is_valid_signature( ref self: TContractState, hash: felt252, signature: Array ) -> felt252; /// Query if a contract implements an interface - /// @param interface_id The interface identifier, as specified in SRC-5 + /// + /// # Arguments + /// + /// * `interface_id` - The interface identifier, as specified in SRC-5 /// @return `true` if the contract implements `interface_id`, `false` otherwise fn supports_interface(ref self: TContractState, interface_id: felt252) -> bool; diff --git a/src/lib.cairo b/src/lib.cairo index 1427f4f..c35b07b 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -3,5 +3,11 @@ mod account { mod error; } +mod router { + mod router; + mod error; + mod jedi_swap_router; +} + #[cfg(test)] mod tests; diff --git a/src/router/error.cairo b/src/router/error.cairo new file mode 100644 index 0000000..3e5dbe5 --- /dev/null +++ b/src/router/error.cairo @@ -0,0 +1,5 @@ +mod RouterError { + const ALREADY_INITIALIZED: felt252 = 'ALREADY_INITIALIZED'; + const ONLY_OWNER: felt252 = 'ONLY_OWNER'; + const ROUTER_ADDRESS_UNDEFINED: felt252 = 'ROUTER_ADDRESS_UNDEFINED'; +} diff --git a/src/router/jedi_swap_router.cairo b/src/router/jedi_swap_router.cairo new file mode 100644 index 0000000..bb1a97e --- /dev/null +++ b/src/router/jedi_swap_router.cairo @@ -0,0 +1,105 @@ +#[starknet::contract] +mod JediSwapRouter { + // ************************************************************************* + // IMPORTS + // ************************************************************************* + + // Core lib imports. + use core::zeroable::Zeroable; + use starknet::{get_caller_address, ContractAddress}; + + use debug::PrintTrait; + + // Local imports. + use swappy::router::router::IRouter; + use swappy::router::error::RouterError; + + // ************************************************************************* + // STORAGE + // ************************************************************************* + #[storage] + struct Storage { + owner: ContractAddress, + router_address: ContractAddress + } + + // ************************************************************************* + // CONSTRUCTOR + // ************************************************************************* + + /// Constructor of the contract. + /// # Arguments + /// * `owner` - The owner address. + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.initialize(owner); + } + + // ************************************************************************* + // EXTERNAL FUNCTIONS + // ************************************************************************* + #[external(v0)] + impl JediSwapRouterImpl of IRouter { + fn initialize(ref self: ContractState, owner: ContractAddress) { + // Make sure the contract is not already initialized. + assert(self.owner.read().is_zero(), RouterError::ALREADY_INITIALIZED); + + self.owner.write(owner); + } + + fn get_owner(ref self: ContractState) -> ContractAddress { + self.owner.read() + } + + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress){ + self.only_owner(); + self.owner.write(new_owner); + } + + fn get_router_address(ref self: ContractState) -> ContractAddress { + self.router_address.read() + } + + fn set_router_address(ref self: ContractState, router_address: ContractAddress) { + self.only_owner(); + self.router_address.write(router_address) + } + + fn swap_exact_tokens_for_tokens( + ref self: ContractState, + amount_in: u256, + amount_out_min: u256, + path: Span, + to: felt252, + deadline: felt252 + ) { + self + .swap_exact_tokens_for_tokens_internal( + amount_in, amount_out_min, path, to, deadline + ); + } + } + + #[generate_trait] + impl JediSwapRouterInternalImpl of JediSwapRouterInternal { + fn only_owner(self: @ContractState) { + let sender = get_caller_address(); + let owner = self.owner.read(); + + assert(sender == owner, RouterError::ONLY_OWNER); + } + + fn swap_exact_tokens_for_tokens_internal( + ref self: ContractState, + amount_in: u256, + amount_out_min: u256, + path: Span, + to: felt252, + deadline: felt252 + ) { + let router_address = self.router_address.read(); + assert(router_address.is_non_zero(), RouterError::ROUTER_ADDRESS_UNDEFINED); + // TODO : Call router address swap_exact_tokens_for_tokens_internal + } + } +} diff --git a/src/router/router.cairo b/src/router/router.cairo new file mode 100644 index 0000000..948d8e8 --- /dev/null +++ b/src/router/router.cairo @@ -0,0 +1,59 @@ +// ************************************************************************* +// IMPORTS +// ************************************************************************* +use starknet::ContractAddress; + +// ************************************************************************* +// STRUCTS / CONST +// ************************************************************************* + +// ************************************************************************* +// Interfaces of the `Router` contract. +// ************************************************************************* +#[starknet::interface] +trait IRouter { + /// Initialize the contract + /// + /// # Arguments + /// + /// * `owner` - The contract owner address + fn initialize(ref self: TContractState, owner: ContractAddress); + + /// Get the owner address + fn get_owner(ref self: TContractState) -> ContractAddress; + + /// Transfer the ownership + /// + /// # Arguments + /// + /// * `new_owner` - The new owner address + fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress); + + /// Get the router address + fn get_router_address(ref self: TContractState) -> ContractAddress; + + /// Set the router address + /// + /// # Arguments + /// + /// * `router_address` - The router address + fn set_router_address(ref self: TContractState, router_address: ContractAddress); + + /// Swap exact tokens for tokens + /// + /// # Arguments + /// + /// * `amount_in` - Amount of token to swap + /// * `amount_out_min` - Min out token amount + /// * `path` - Route path + /// * `to` - The recipient of swap + /// * `deadline` - The deadline of the swap transaction + fn swap_exact_tokens_for_tokens( + ref self: TContractState, + amount_in: u256, + amount_out_min: u256, + path: Span, + to: felt252, + deadline: felt252 + ); +} diff --git a/src/tests.cairo b/src/tests.cairo index 7d1e9dc..4858aff 100644 --- a/src/tests.cairo +++ b/src/tests.cairo @@ -1,2 +1,8 @@ #[cfg(test)] mod test_account; + +#[cfg(test)] +mod router { + #[cfg(test)] + mod test_jedi_swap_router; +} diff --git a/src/tests/router/test_jedi_swap_router.cairo b/src/tests/router/test_jedi_swap_router.cairo new file mode 100644 index 0000000..a766ff8 --- /dev/null +++ b/src/tests/router/test_jedi_swap_router.cairo @@ -0,0 +1,93 @@ +// ************************************************************************* +// IMPORTS +// ************************************************************************* + +// Core lib imports. +use starknet::{ + deploy_syscall, get_contract_address, get_caller_address, ContractAddress, + contract_address_const +}; +use starknet::testing::{set_caller_address, set_contract_address, set_account_contract_address}; + +use debug::PrintTrait; + +// Local imports. +use swappy::router::router::{IRouterDispatcher, IRouterDispatcherTrait}; +use swappy::router::jedi_swap_router::JediSwapRouter; + +// Deploy the contract and return its dispatcher. +fn deploy(owner: ContractAddress) -> IRouterDispatcher { + // Set up constructor arguments. + let mut calldata = ArrayTrait::new(); + owner.serialize(ref calldata); + + // Declare and deploy + let (contract_address, _) = deploy_syscall( + JediSwapRouter::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false + ) + .unwrap(); + + // Return the dispatcher. + // The dispatcher allows to interact with the contract based on its interface. + IRouterDispatcher { contract_address } +} + +#[test] +#[available_gas(2000000000)] +fn test_deploy() { + // Given + let owner = contract_address_const::<12345>(); + + // When + let contract = deploy(owner); + + // Then + assert(contract.get_owner() == owner, 'wrong owner'); +} + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('ALREADY_INITIALIZED', 'ENTRYPOINT_FAILED',))] +fn test_initialize_twice() { + // Given + let owner = contract_address_const::<12345>(); + let contract = deploy(owner); + set_contract_address(owner); + + // When + contract.initialize(contract_address_const::<999999>()); +} + +#[test] +#[available_gas(2000000000)] +fn test_set_router_address_by_owner() { + // Given + let owner = contract_address_const::<12345>(); + let contract = deploy(owner); + set_contract_address(owner); + + let router_address = + contract_address_const::<0x02bcc885342ebbcbcd170ae6cafa8a4bed22bb993479f49806e72d96af94c965>(); + + // When + contract.set_router_address(router_address); + + // Then + assert(contract.get_router_address() == router_address, 'wrong router_address'); +} + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('ONLY_OWNER', 'ENTRYPOINT_FAILED',))] +fn test_set_router_address_by_non_owner_should_fail() { + // Given + let owner = contract_address_const::<12345>(); + let contract = deploy(owner); + set_contract_address(contract_address_const::<99999>()); + + let router_address = + contract_address_const::<0x02bcc885342ebbcbcd170ae6cafa8a4bed22bb993479f49806e72d96af94c965>(); + + // When + contract.set_router_address(router_address); +} diff --git a/src/tests/test_account.cairo b/src/tests/test_account.cairo index 9f089dc..aeebfff 100644 --- a/src/tests/test_account.cairo +++ b/src/tests/test_account.cairo @@ -28,9 +28,13 @@ fn deploy(public_key: felt252) -> IAccountDispatcher { #[test] #[available_gas(2000000000)] fn test_deploy() { + // Given let public_key: felt252 = 'test'; + + // When let contract = deploy(public_key); + // Then assert(contract.get_public_key() == public_key, 'wrong public key'); } @@ -38,7 +42,10 @@ fn test_deploy() { #[available_gas(2000000000)] #[should_panic(expected: ('ALREADY_INITIALIZED', 'ENTRYPOINT_FAILED',))] fn test_initialize_twice() { + // Given let public_key: felt252 = 'test'; let contract = deploy(public_key); + + // When contract.initialize(public_key); } From 878ce54da9292bdce334779788ec7a5f8a9bf82d Mon Sep 17 00:00:00 2001 From: Khaeljy Date: Sat, 16 Sep 2023 22:25:42 +0200 Subject: [PATCH 2/4] feat: Implement router contract for JediSwap Fixes #7 --- src/router/jedi_swap_router.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/router/jedi_swap_router.cairo b/src/router/jedi_swap_router.cairo index bb1a97e..f08db08 100644 --- a/src/router/jedi_swap_router.cairo +++ b/src/router/jedi_swap_router.cairo @@ -51,7 +51,7 @@ mod JediSwapRouter { self.owner.read() } - fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress){ + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { self.only_owner(); self.owner.write(new_owner); } From bc0ba55645d36c49d1e04a44bc28bfdcc3365308 Mon Sep 17 00:00:00 2001 From: khaeljy Date: Sun, 17 Sep 2023 16:16:29 +0000 Subject: [PATCH 3/4] Implement JediSwapRouter --- src/account/account.cairo | 9 +++-- src/router/jedi_swap_router.cairo | 16 ++++---- src/router/router.cairo | 6 +-- src/tests/router/test_jedi_swap_router.cairo | 41 ++++++++++++++++++++ src/tests/test_account.cairo | 0 5 files changed, 58 insertions(+), 14 deletions(-) mode change 100644 => 100755 src/account/account.cairo mode change 100644 => 100755 src/router/jedi_swap_router.cairo mode change 100644 => 100755 src/tests/test_account.cairo diff --git a/src/account/account.cairo b/src/account/account.cairo old mode 100644 new mode 100755 index 4bd734e..2f325b0 --- a/src/account/account.cairo +++ b/src/account/account.cairo @@ -9,10 +9,11 @@ use starknet::ContractAddress; // STRUCTS / CONST // ************************************************************************* -/// @title Represents a call to a target contract -/// @param to The target contract address -/// @param selector The target function selector -/// @param calldata The serialized function parameters +/// Represents a call to a target contract +/// +/// `to` - The target contract address +/// `selector` - The target function selector +/// `calldata` - The serialized function parameters #[derive(Drop, Serde)] struct Call { to: ContractAddress, diff --git a/src/router/jedi_swap_router.cairo b/src/router/jedi_swap_router.cairo old mode 100644 new mode 100755 index f08db08..0a7ca68 --- a/src/router/jedi_swap_router.cairo +++ b/src/router/jedi_swap_router.cairo @@ -69,9 +69,9 @@ mod JediSwapRouter { ref self: ContractState, amount_in: u256, amount_out_min: u256, - path: Span, - to: felt252, - deadline: felt252 + path: Span, + to: ContractAddress, + deadline: u32 ) { self .swap_exact_tokens_for_tokens_internal( @@ -93,13 +93,15 @@ mod JediSwapRouter { ref self: ContractState, amount_in: u256, amount_out_min: u256, - path: Span, - to: felt252, - deadline: felt252 + path: Span, + to: ContractAddress, + deadline: u32 ) { let router_address = self.router_address.read(); assert(router_address.is_non_zero(), RouterError::ROUTER_ADDRESS_UNDEFINED); - // TODO : Call router address swap_exact_tokens_for_tokens_internal + + // TODO : Call router address swap_exact_tokens_for_tokens + panic_with_felt252('NOT_IMPLEMENTED'); } } } diff --git a/src/router/router.cairo b/src/router/router.cairo index 948d8e8..7fc0c30 100644 --- a/src/router/router.cairo +++ b/src/router/router.cairo @@ -52,8 +52,8 @@ trait IRouter { ref self: TContractState, amount_in: u256, amount_out_min: u256, - path: Span, - to: felt252, - deadline: felt252 + path: Span, + to: ContractAddress, + deadline: u32 ); } diff --git a/src/tests/router/test_jedi_swap_router.cairo b/src/tests/router/test_jedi_swap_router.cairo index a766ff8..77a8cc9 100644 --- a/src/tests/router/test_jedi_swap_router.cairo +++ b/src/tests/router/test_jedi_swap_router.cairo @@ -91,3 +91,44 @@ fn test_set_router_address_by_non_owner_should_fail() { // When contract.set_router_address(router_address); } + +#[test] +#[available_gas(2000000000)] +fn test_transfert_ownership() { + // Given + let owner = contract_address_const::<12345>(); + let contract = deploy(owner); + set_contract_address(owner); + + // When + let new_owner = contract_address_const::<99999>(); + contract.transfer_ownership(new_owner); + + // Then + assert(contract.get_owner() == new_owner, 'transfert_ownership failed'); +} + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('NOT_IMPLEMENTED', 'ENTRYPOINT_FAILED',))] +fn test_swap_exact_tokens_for_tokens() { + // Given + let owner = contract_address_const::<12345>(); + let contract = deploy(owner); + set_contract_address(owner); + + let router_address = + contract_address_const::<0x02bcc885342ebbcbcd170ae6cafa8a4bed22bb993479f49806e72d96af94c965>(); + contract.set_router_address(router_address); + + // When + contract + .swap_exact_tokens_for_tokens( + amount_in: 1, + amount_out_min: 1, + path: array![contract_address_const::<11111>(), contract_address_const::<22222>()] + .span(), + to: owner, + deadline: 1 + ); +} diff --git a/src/tests/test_account.cairo b/src/tests/test_account.cairo old mode 100644 new mode 100755 From 1c8259d5d9cdb54d6ac1df875e04ce64be4e3be5 Mon Sep 17 00:00:00 2001 From: khaeljy Date: Sun, 17 Sep 2023 16:29:19 +0000 Subject: [PATCH 4/4] Add comment --- src/router/jedi_swap_router.cairo | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/router/jedi_swap_router.cairo b/src/router/jedi_swap_router.cairo index 0a7ca68..1d72b87 100755 --- a/src/router/jedi_swap_router.cairo +++ b/src/router/jedi_swap_router.cairo @@ -8,8 +8,6 @@ mod JediSwapRouter { use core::zeroable::Zeroable; use starknet::{get_caller_address, ContractAddress}; - use debug::PrintTrait; - // Local imports. use swappy::router::router::IRouter; use swappy::router::error::RouterError; @@ -101,6 +99,7 @@ mod JediSwapRouter { assert(router_address.is_non_zero(), RouterError::ROUTER_ADDRESS_UNDEFINED); // TODO : Call router address swap_exact_tokens_for_tokens + // Until JediSwap is updated to Cairo 1, panic with 'NOT_IMPLEMENTED' panic_with_felt252('NOT_IMPLEMENTED'); } }