From a7a729a2ea78dc628d549d151a8b1986533b821c Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Sat, 17 Feb 2024 09:32:29 -0500 Subject: [PATCH] create a lite router example --- src/components/clear.cairo | 28 ++++++ src/lib.cairo | 1 + src/router_lite.cairo | 187 +++++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 src/router_lite.cairo diff --git a/src/components/clear.cairo b/src/components/clear.cairo index 984a0f3..5c14f39 100644 --- a/src/components/clear.cairo +++ b/src/components/clear.cairo @@ -1,4 +1,5 @@ use ekubo::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; +use core::num::traits::{Zero}; use starknet::{ContractAddress, get_contract_address, get_caller_address}; // This component is embedded in the Router and Positions contracts @@ -13,3 +14,30 @@ pub trait IClear { self: @TContractState, token: IERC20Dispatcher, minimum: u256, recipient: ContractAddress ) -> u256; } + +#[starknet::embeddable] +pub impl ClearImpl of IClear { + #[inline(always)] + fn clear(self: @TContractState, token: IERC20Dispatcher) -> u256 { + self.clear_minimum_to_recipient(token, 0, get_caller_address()) + } + + #[inline(always)] + fn clear_minimum(self: @TContractState, token: IERC20Dispatcher, minimum: u256) -> u256 { + self.clear_minimum_to_recipient(token, minimum, get_caller_address()) + } + + #[inline(always)] + fn clear_minimum_to_recipient( + self: @TContractState, token: IERC20Dispatcher, minimum: u256, recipient: ContractAddress + ) -> u256 { + let balance = token.balanceOf(get_contract_address()); + if minimum.is_non_zero() { + assert(balance >= minimum, 'CLEAR_AT_LEAST_MINIMUM'); + } + if balance.is_non_zero() { + token.transfer(recipient, balance); + } + balance + } +} diff --git a/src/lib.cairo b/src/lib.cairo index 6a856a0..9681f0c 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -23,3 +23,4 @@ pub mod types { pub mod position; } +pub mod router_lite; \ No newline at end of file diff --git a/src/router_lite.cairo b/src/router_lite.cairo new file mode 100644 index 0000000..82f8bec --- /dev/null +++ b/src/router_lite.cairo @@ -0,0 +1,187 @@ +use ekubo::types::delta::{Delta}; +use ekubo::types::i129::{i129}; +use ekubo::types::keys::{PoolKey}; +use starknet::{ContractAddress}; + +#[derive(Serde, Copy, Drop)] +pub struct RouteNode { + pub pool_key: PoolKey, + pub sqrt_ratio_limit: u256, + pub skip_ahead: u128, +} + +#[derive(Serde, Copy, Drop)] +pub struct TokenAmount { + pub token: ContractAddress, + pub amount: i129, +} + +#[derive(Serde, Drop)] +pub struct Swap { + pub route: Array, + pub token_amount: TokenAmount, +} + +#[starknet::interface] +pub trait IRouterLite { + // Does a single swap against a single node using tokens held by this contract, and receives the output to this contract + fn swap(ref self: TContractState, node: RouteNode, token_amount: TokenAmount) -> Delta; + + // Does a multihop swap, where the output/input of each hop is passed as input/output of the next swap + // Note to do exact output swaps, the route must be given in reverse + fn multihop_swap( + ref self: TContractState, route: Array, token_amount: TokenAmount + ) -> Array; + + // Does multiple multihop swaps + fn multi_multihop_swap(ref self: TContractState, swaps: Array) -> Array>; +} + +#[starknet::contract] +pub mod RouterLite { + use core::array::{Array, ArrayTrait, SpanTrait}; + use core::cmp::{min, max}; + use core::integer::{u256_sqrt}; + use core::num::traits::{Zero}; + use core::option::{OptionTrait}; + use core::result::{ResultTrait}; + use core::traits::{Into}; + use ekubo::components::clear::{ClearImpl}; + use ekubo::components::shared_locker::{ + consume_callback_data, handle_delta, call_core_with_callback + }; + use ekubo::interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait, ILocker, SwapParameters}; + use ekubo::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use ekubo::types::i129::{i129, i129Trait}; + use starknet::syscalls::{call_contract_syscall}; + + use starknet::{get_caller_address, get_contract_address}; + + use super::{ContractAddress, PoolKey, Delta, IRouterLite, RouteNode, TokenAmount, Swap}; + + #[abi(embed_v0)] + impl Clear = ekubo::components::clear::ClearImpl; + + #[storage] + struct Storage { + core: ICoreDispatcher, + } + + #[constructor] + fn constructor(ref self: ContractState, core: ICoreDispatcher) { + self.core.write(core); + } + + #[abi(embed_v0)] + impl LockerImpl of ILocker { + fn locked(ref self: ContractState, id: u32, data: Span) -> Span { + let core = self.core.read(); + + let mut swaps = consume_callback_data::>(core, data); + let mut outputs: Array> = ArrayTrait::new(); + + loop { + match swaps.pop_front() { + Option::Some(swap) => { + let mut route = swap.route; + let mut token_amount = swap.token_amount; + + let mut deltas: Array = ArrayTrait::new(); + // we track this to know how much to pay in the case of exact input and how much to pull in the case of exact output + let mut first_swap_amount: Option = Option::None; + + loop { + match route.pop_front() { + Option::Some(node) => { + let is_token1 = token_amount.token == node.pool_key.token1; + + let delta = core + .swap( + node.pool_key, + SwapParameters { + amount: token_amount.amount, + is_token1: is_token1, + sqrt_ratio_limit: node.sqrt_ratio_limit, + skip_ahead: node.skip_ahead, + } + ); + + deltas.append(delta); + + if first_swap_amount.is_none() { + first_swap_amount = + if is_token1 { + Option::Some( + TokenAmount { + token: node.pool_key.token1, + amount: delta.amount1 + } + ) + } else { + Option::Some( + TokenAmount { + token: node.pool_key.token0, + amount: delta.amount0 + } + ) + } + } + + token_amount = + if (is_token1) { + TokenAmount { + amount: -delta.amount0, token: node.pool_key.token0 + } + } else { + TokenAmount { + amount: -delta.amount1, token: node.pool_key.token1 + } + }; + }, + Option::None => { break (); } + }; + }; + + let recipient = get_contract_address(); + + outputs.append(deltas); + + let first = first_swap_amount.unwrap(); + handle_delta(core, token_amount.token, -token_amount.amount, recipient); + handle_delta(core, first.token, first.amount, recipient); + }, + Option::None => { break (); } + }; + }; + + let mut serialized: Array = array![]; + + Serde::serialize(@outputs, ref serialized); + + serialized.span() + } + } + + + #[abi(embed_v0)] + impl RouterLiteImpl of IRouterLite { + fn swap(ref self: ContractState, node: RouteNode, token_amount: TokenAmount) -> Delta { + let mut deltas: Array = self.multihop_swap(array![node], token_amount); + deltas.pop_front().unwrap() + } + + #[inline(always)] + fn multihop_swap( + ref self: ContractState, route: Array, token_amount: TokenAmount + ) -> Array { + let mut result = self.multi_multihop_swap(array![Swap { route, token_amount }]); + + result.pop_front().unwrap() + } + + #[inline(always)] + fn multi_multihop_swap(ref self: ContractState, swaps: Array) -> Array> { + call_core_with_callback(self.core.read(), @swaps) + } + } +}