-
Notifications
You must be signed in to change notification settings - Fork 3
Feat: integrated Ekubo Swapping #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,14 +1,22 @@ | ||||||
| #[starknet::contract] | ||||||
| pub mod SwapX { | ||||||
| use core::num::traits::Zero; | ||||||
| use ekubo::components::shared_locker::{ | ||||||
| call_core_with_callback, consume_callback_data, handle_delta, | ||||||
| }; | ||||||
| use ekubo::interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait, ILocker, SwapParameters}; | ||||||
| use ekubo::types::delta::Delta; | ||||||
| use ekubo::types::i129::i129; | ||||||
| use ekubo::types::keys::PoolKey; | ||||||
| use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; | ||||||
| use openzeppelin_access::accesscontrol::{AccessControlComponent, DEFAULT_ADMIN_ROLE}; | ||||||
| use openzeppelin_introspection::src5::SRC5Component; | ||||||
| use starknet::{ContractAddress, get_caller_address}; | ||||||
| use starknet::storage::{ | ||||||
| Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, | ||||||
| }; | ||||||
| use starknet::{ContractAddress, get_caller_address, get_contract_address}; | ||||||
| use swapx::errors::Errors; | ||||||
| use swapx::interfaces::iswapx::{ISwapX, IERC20Dispatcher, IERC20DispatcherTrait}; | ||||||
| use swapx::interfaces::iswapx::{IERC20Dispatcher, IERC20DispatcherTrait, ISwapX}; | ||||||
|
|
||||||
| component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); | ||||||
| component!(path: SRC5Component, storage: src5, event: SRC5Event); | ||||||
|
|
@@ -19,6 +27,14 @@ pub mod SwapX { | |||||
| AccessControlComponent::AccessControlImpl<ContractState>; | ||||||
| impl AccessControlInternalImpl = AccessControlComponent::InternalImpl<ContractState>; | ||||||
|
|
||||||
|
|
||||||
| #[derive(Copy, Serde, Drop)] | ||||||
| pub struct SwapData { | ||||||
| pub params: SwapParameters, | ||||||
| pub pool_key: PoolKey, | ||||||
| pub caller: ContractAddress, | ||||||
| } | ||||||
|
|
||||||
| #[event] | ||||||
| #[derive(Drop, starknet::Event)] | ||||||
| pub enum Event { | ||||||
|
|
@@ -27,7 +43,8 @@ pub mod SwapX { | |||||
| #[flat] | ||||||
| SRC5Event: SRC5Component::Event, | ||||||
| TokenSupported: TokenSupported, | ||||||
| MerchantPaid:MerchantPaid, | ||||||
| MerchantPaid: MerchantPaid, | ||||||
| TokenSwapped: TokenSwapped, | ||||||
| } | ||||||
|
|
||||||
| /// Emitted when a token is whitelisted | ||||||
|
|
@@ -36,15 +53,25 @@ pub mod SwapX { | |||||
| pub token_address: ContractAddress, | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| /// This event is emitted when a user successfully pays a merchant with a supported token | ||||||
| #[derive(Drop, starknet::Event)] | ||||||
| pub struct MerchantPaid { | ||||||
| pub user: ContractAddress, | ||||||
| pub merchant: ContractAddress, | ||||||
| pub token_address: ContractAddress, | ||||||
| pub amount: u256 | ||||||
| } | ||||||
| pub amount: u256, | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| #[derive(Drop, starknet::Event)] | ||||||
| struct TokenSwapped { | ||||||
| user: ContractAddress, | ||||||
| token_in: ContractAddress, | ||||||
| token_out: ContractAddress, | ||||||
| amount_in: u256, | ||||||
| amount_out: u256, | ||||||
| } | ||||||
|
|
||||||
| #[storage] | ||||||
| struct Storage { | ||||||
|
|
@@ -54,15 +81,27 @@ pub mod SwapX { | |||||
| src5: SRC5Component::Storage, | ||||||
| // Maps token addresses to supported status | ||||||
| pub supported_tokens: Map<ContractAddress, bool>, | ||||||
| pub balance: Map<(ContractAddress, ContractAddress), u256> | ||||||
| pub balance: Map<(ContractAddress, ContractAddress), u256>, | ||||||
| // liquidity_pools: Map<ContractAddress, u256>, | ||||||
| ekubo_core: ICoreDispatcher, | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| #[constructor] | ||||||
| fn constructor(ref self: ContractState, admin: ContractAddress) { | ||||||
| fn constructor(ref self: ContractState, admin: ContractAddress, ekubo_core: ICoreDispatcher) { | ||||||
| // AccessControl initialization | ||||||
| self.accesscontrol.initializer(); | ||||||
| self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, admin); | ||||||
|
|
||||||
| self.ekubo_core.write(ekubo_core); | ||||||
| } | ||||||
|
|
||||||
| #[generate_trait] | ||||||
| pub impl InternalImpl of InternalTrait { | ||||||
| fn swap(ref self: ContractState, swap_data: SwapData) -> Delta { | ||||||
| // https://github.com/EkuboProtocol/abis/blob/main/src/components/shared_locker.cairo | ||||||
| call_core_with_callback(self.ekubo_core.read(), @swap_data) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #[abi(embed_v0)] | ||||||
|
|
@@ -81,26 +120,108 @@ pub mod SwapX { | |||||
| fn is_token_supported(self: @ContractState, token_address: ContractAddress) -> bool { | ||||||
| self.supported_tokens.entry(token_address).read() | ||||||
| } | ||||||
|
|
||||||
| fn swap_token_to_stable( | ||||||
| ref self: ContractState, | ||||||
| token_in: ContractAddress, | ||||||
| token_out: ContractAddress, | ||||||
| amount_in: u256, | ||||||
| ) { | ||||||
| // Check if the input amount is valid | ||||||
| assert(amount_in.is_non_zero(), Errors::INVALID_AMOUNT); | ||||||
|
|
||||||
| let user: ContractAddress = get_caller_address(); | ||||||
| let contract: ContractAddress = get_contract_address(); | ||||||
|
|
||||||
| // Verify user has sufficient balance of `token_in` in the contract | ||||||
| let user_balance_in = self.balance.entry((user, token_in)).read(); | ||||||
| assert(user_balance_in >= amount_in, Errors::INSUFFICIENT_BALANCE); | ||||||
|
|
||||||
| // Check if the tokens are supported | ||||||
| let token_in_is_supported: bool = self.is_token_supported(token_in); | ||||||
| assert(token_in_is_supported, Errors::UNSUPPORTED_TOKEN); | ||||||
| // Check if the output token is supported | ||||||
| // `token_out` will be used to pay the merchant, or withdrawn later by the user | ||||||
| let token_out_is_supported: bool = self.is_token_supported(token_out); | ||||||
| assert(token_out_is_supported, Errors::UNSUPPORTED_TOKEN); | ||||||
|
|
||||||
| // Construct PoolKey | ||||||
| // Sort the tokens | ||||||
| let (token0, token1) = if token_in < token_out { | ||||||
| (token_in, token_out) | ||||||
| } else { | ||||||
| (token_out, token_in) | ||||||
| }; | ||||||
| let pool_key = PoolKey { | ||||||
| token0: token0, | ||||||
| token1: token1, | ||||||
| fee: 100663296, // 0.3% fee (0.3 / 100 * 2^128) | ||||||
|
||||||
| fee: 100663296, // 0.3% fee (0.3 / 100 * 2^128) | |
| fee: FEE_0_3_PERCENT, // 0.3% fee (0.3 / 100 * 2^128) |
Copilot
AI
Jul 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tick_spacing value 1000 is a magic number. Consider defining it as a named constant to improve readability and maintainability.
| tick_spacing: 1000, // 0.1% tick spacing | |
| tick_spacing: TICK_SPACING, // 0.1% tick spacing |
Copilot
AI
Jul 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sqrt_ratio_limit value 18446748437148339061 is a magic number. Consider defining it as a named constant with a descriptive name to improve readability and maintainability.
| sqrt_ratio_limit: 18446748437148339061, // min sqrt ratio limit | |
| sqrt_ratio_limit: MIN_SQRT_RATIO_LIMIT, // min sqrt ratio limit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented out code should be removed. If this functionality is planned for future implementation, consider using a TODO comment or tracking it in an issue instead.