From d2bf065ae165684bad472f9debf2445ab0a28647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Galv=C3=A1n=20=28Dub=29?= Date: Wed, 4 Oct 2023 14:32:28 -0300 Subject: [PATCH] add setup for deploy and test (#119) * initial commit * set pool_state init params for testing * add YasMintCallback for testing * add mint() YASMintCallback * adding WALLET and POOL_ADDRESS constants * remove prints() from yas_pool * add caller into data arr param * add setup() for mint tests + refactor * remove prints * scarb fmt * fix event emit * uncomment tests libs * add Swap & SwapCallback * rename mod * rename YASRouter contract * adding tokens into script * remove unnecessary interface ERC20 * fix lock * move /tests/erc20 to /contracts/yas_erc20 * add steps into script * add mint() and swap() into script * fix u256 params * add pool balances * .gitignore add .idea * refactor initialize_pool() * refactor mint() * refactor balance_of() * add approve() and uncomment erc20 allowance assert * refactor + fix swap() * add swap rust docs * rename :P * remove unnecessary call * fix swap + modify script * fix tests * scarb fmt * fix merge error * add 'make demo-local' * remove unused imports * add readme script steps * fix readme err * run prettier --------- Co-authored-by: dpinones --- .gitignore | 1 + Cargo.toml | 4 + Makefile | 3 + README.md | 34 +- scripts/deploy.rs | 148 ++++- scripts/local.rs | 536 ++++++++++++++++++ src/contracts/yas_pool.cairo | 11 +- src/contracts/yas_router.cairo | 49 +- .../test_contracts/test_yas_factory.cairo | 1 - src/tests/test_contracts/test_yas_pool.cairo | 21 +- 10 files changed, 772 insertions(+), 36 deletions(-) create mode 100644 scripts/local.rs diff --git a/.gitignore b/.gitignore index a57d4a29..6c1ac45f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .starkli-wallets/** target .env +.idea \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b1d95477..a73c319f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,7 @@ url = "2.2.2" [[bin]] name = "deploy" path = "scripts/deploy.rs" + +[[bin]] +name = "local" +path = "scripts/local.rs" \ No newline at end of file diff --git a/Makefile b/Makefile index 25113923..1254805d 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ build: deploy: cargo run --bin deploy + +demo-local: + cargo run --bin local test: scarb test diff --git a/README.md b/README.md index 1c790bc5..b02fea7a 100644 --- a/README.md +++ b/README.md @@ -160,16 +160,18 @@ On Starknet, the deployment process is in two steps: - Deploying a contract or creating an instance of the previously declared code with the necessary parameters -1. Build the project: +1. Build the project ```bash make build ``` 2. Start Local Testnet + ```bash make start-katana ``` + 3. Declare and Deploy: Using [deploy.rs](./scripts/deploy.rs) script, we sequentially declare and deploy the contracts. Local deployment needs `katana` running. The account used for deployment is a pre-funded one. @@ -186,6 +188,36 @@ On Starknet, the deployment process is in two steps: make deploy ``` +## Run local demo in Katana + +This demo will perform the following steps: + +- Declaration of the following contracts: ERC20 Token, YASFactory, YASPool, and YASRouter. +- Deployment of 2 ERC20 Tokens, YASFactory, YASPool, and YASRouter. +- Initialization of YASPool with a 1:1 token price. +- Execute approve() for the router to use tokens from the user. +- Execute mint() within the range [-887220, 887220] with 2000000000000000000 tokens. +- Execute swap() exchanging 500000000000000000 of token 0 for token 1. +- Display current balances of both the pool and the user. + +1. Build the project + + ```bash + make build + ``` + +2. Start Local Testnet + + ```bash + make start-katana + ``` + +3. Run Local Demo + + ```bash + make demo-local + ``` + ## Override `.env` file To override environment variables in the `.env` file, you may pass them before diff --git a/scripts/deploy.rs b/scripts/deploy.rs index cf0a8fe7..7e9c39b3 100644 --- a/scripts/deploy.rs +++ b/scripts/deploy.rs @@ -3,9 +3,10 @@ use std::{env, fs}; use dotenv::dotenv; use eyre::Result; -use starknet::accounts::{Account, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount}; +use starknet::accounts::{Account, Call, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount}; use starknet::contract::ContractFactory; use starknet::core::types::contract::SierraClass; +use starknet::core::utils::get_selector_from_name; use starknet::core::types::{BlockId, BlockTag, FieldElement, StarknetError}; use starknet::providers::jsonrpc::{HttpTransport, JsonRpcClient}; use starknet::providers::{MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage}; @@ -15,6 +16,9 @@ const BUILD_PATH_PREFIX: &str = "target/dev/yas_"; // TODO: Update to New once account contracts are migrated to v1 const ENCODING: ExecutionEncoding = ExecutionEncoding::Legacy; +const POSITIVE: bool = false; +const NEGATIVE: bool = true; + /// Create a StarkNet provider. /// If the `STARKNET_RPC` environment variable is set, it will be used as the RPC URL. /// Otherwise, the default URL will be used. @@ -108,7 +112,7 @@ async fn declare_contract( // Declare the contract class if it is not already declared. if !is_already_declared(account.provider(), &class_hash).await? { - println!("==> Declaring Contract: {contract_name}"); + println!("\n==> Declaring Contract: {contract_name}"); let flattened_class = contract_artifact.flatten()?; account.declare(Arc::new(flattened_class), class_hash).send().await?; println!("Declared Class Hash: {}", format!("{:#064x}", class_hash)); @@ -117,8 +121,84 @@ async fn declare_contract( Ok(class_hash) } +/// Deploy ERC20 Contract. +/// +/// # Arguments +/// +/// * `name` - The name of the ERC20 token. +/// * `symbol` - The symbol of the ERC20 token. +/// * `total_supply` - The total supply of the ERC20 token. +/// * `recipient` - The initial recipient of the total supply. +/// +/// # Returns +/// +/// This function returns a `Result` indicating success or an error. +async fn deploy_erc20( + recipient: FieldElement, + account: &SingleOwnerAccount, LocalWallet>, + erc20_class_hash: FieldElement +) -> Result<(FieldElement, FieldElement)> { + // Instantiate the contract factory. + let erc20_factory = ContractFactory::new(erc20_class_hash, account); + let unique = true; + let salt = account.get_nonce().await?; + let erc20_token_0_contract_deployment = erc20_factory.deploy(vec![FieldElement::from_hex_be("0x5459415330").unwrap(), FieldElement::from_hex_be("0x2459415330").unwrap(), FieldElement::from_hex_be("0x3782dace9d900000").unwrap(), FieldElement::ZERO, recipient], salt, unique); + let erc20_token_0_deployed_address = erc20_token_0_contract_deployment.deployed_address(); + println!("Token TYAS0 Address: {}", format!("{:#064x}", erc20_token_0_deployed_address)); + let estimated_fee = erc20_token_0_contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; + erc20_token_0_contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + + let erc20_token_1_contract_deployment = erc20_factory.deploy(vec![FieldElement::from_hex_be("0x5459415331").unwrap(), FieldElement::from_hex_be("0x2459415331").unwrap(), FieldElement::from_hex_be("0x3782dace9d900000").unwrap(), FieldElement::ZERO, recipient], salt, unique); + let erc20_token_1_deployed_address = erc20_token_1_contract_deployment.deployed_address(); + println!("Token TYAS1 Address: {}", format!("{:#064x}", erc20_token_1_deployed_address)); + let estimated_fee = erc20_token_1_contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; + erc20_token_1_contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + + Ok((erc20_token_0_deployed_address, erc20_token_1_deployed_address)) +} + +/// Asynchronously initializes a liquidity pool using the provided parameters. +/// +/// # Arguments +/// +/// * `account` - The reference to a `SingleOwnerAccount` with a `JsonRpcClient` and `LocalWallet`. +/// * `pool_address` - The target address of the liquidity pool to be initialized. +/// * `price_sqrt_low` - The lower bound of the square root of the price in the liquidity pool. +/// * `price_sqrt_high` - The upper bound of the square root of the price in the liquidity pool. +/// * `sign` - A boolean flag indicating the sign of the price, where `true` represents negative and `false` represents positive. +/// +/// # Returns +/// +/// Returns a `Result` indicating success or failure. The `Ok(())` variant is returned on success, and the `Err` variant contains an error description. +async fn initialize_pool( + account: &SingleOwnerAccount, LocalWallet>, + pool_address: FieldElement, + price_sqrt_low: u128, + price_sqrt_high: u128, + sign: bool, +) -> Result<()> { + let invoke_result = account + .execute(vec![Call { + to: pool_address, + selector: get_selector_from_name("initialize").unwrap(), + calldata: vec![ + // fp mag + FieldElement::from(price_sqrt_low), + FieldElement::from(price_sqrt_high), + // sign + match sign { + NEGATIVE => FieldElement::from(1_u128), + POSITIVE => FieldElement::ZERO, + } + ], + }]).send().await?; + + println!("Transaction Hash: {}", format!("{:#064x}", invoke_result.transaction_hash)); + Ok(()) +} + #[tokio::main] -async fn main() -> Result<()> { +pub async fn main() -> Result<()> { dotenv().ok(); // Create signer from private key. @@ -131,28 +211,66 @@ async fn main() -> Result<()> { let account = initialize_starknet_account(signer, account_address).await?; // Declare the contract classes if they are not already declared. - let pool_class_hash = declare_contract(&account, "YASPool").await?; + let erc20_class_hash = declare_contract(&account, "ERC20").await?; let factory_class_hash = declare_contract(&account, "YASFactory").await?; + let pool_class_hash = declare_contract(&account, "YASPool").await?; + let router_class_hash = declare_contract(&account, "YASRouter").await?; - // Instantiate the contract factory. - let salt = account.get_nonce().await?; - let contract_factory = ContractFactory::new(factory_class_hash, account); + let unique = true; let owner_address = FieldElement::from_hex_be(&env::var("OWNER_ADDRESS").expect("OWNER_ADDRESS not set")) .expect("Invalid Owner Address"); - let unique = true; - println!( - "==> Deploying Factory Contract\nOWNER_ADDRESS: {:#064x}\nPOOL_CLASS_HASH: {:#064x}\nSALT: {}\nUNIQUE: {}", - owner_address, pool_class_hash, salt, unique - ); - let contract_deployment = contract_factory.deploy(vec![owner_address, pool_class_hash], salt, unique); - let deployed_address = contract_deployment.deployed_address(); - println!("Contract Address: {}", format!("{:#064x}", deployed_address)); + println!("\n==> Deploying ERC20 Contracts"); + let (token_0, token_1) = deploy_erc20(owner_address, &account, erc20_class_hash).await?; + + // Instantiate the contract factory. + println!("\n==> Deploying Factory Contract"); + let salt = account.get_nonce().await?; + let yas_factory_contract_factory = ContractFactory::new(factory_class_hash, &account); + let contract_deployment = yas_factory_contract_factory.deploy(vec![owner_address, pool_class_hash], salt, unique); + let factory_address = contract_deployment.deployed_address(); + println!("Factory Contract Address: {}", format!("{:#064x}", factory_address)); // Estimate the deployment fee and deploy the contract. let estimated_fee = contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; // add buffer let tx = contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; println!("Transaction Hash: {}", format!("{:#064x}", tx)); + // Instantiate the contract factory. + println!("\n==> Deploying Router Contract"); + let salt = account.get_nonce().await?; + let yas_router_contract_factory = ContractFactory::new(router_class_hash, &account); + let contract_deployment = yas_router_contract_factory.deploy(vec![], salt, unique); + let router_address = contract_deployment.deployed_address(); + println!("Router Contract Address: {}", format!("{:#064x}", router_address)); + + // Estimate the deployment fee and deploy the contract. + let estimated_fee = contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; // add buffer + let tx = contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + println!("Transaction Hash: {}", format!("{:#064x}", tx)); + + // Instantiate the contract factory. + println!("\n==> Deploying Pool Contract"); + let salt = account.get_nonce().await?; + let yas_pool_contract_factory = ContractFactory::new(pool_class_hash, &account); + let contract_deployment = yas_pool_contract_factory.deploy(vec![factory_address, token_0, token_1, FieldElement::from_hex_be("0x0bb8").unwrap(), FieldElement::from_hex_be("0x3c").unwrap(), FieldElement::ZERO], salt, unique); + let pool_address = contract_deployment.deployed_address(); + println!("Pool Contract Address: {}", format!("{:#064x}", pool_address)); + + // Estimate the deployment fee and deploy the contract. + let estimated_fee = contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; // add buffer + let tx = contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + println!("Transaction Hash: {}", format!("{:#064x}", tx)); + + println!("\n==> Initialize Pool"); + initialize_pool( + &account, + pool_address, + // The price of the initial tokens is 1:1 (encode_price_sqrt_1_1) + 79228162514264337593543950336, + 0, + POSITIVE + ).await?; + Ok(()) } diff --git a/scripts/local.rs b/scripts/local.rs new file mode 100644 index 00000000..ee0155f8 --- /dev/null +++ b/scripts/local.rs @@ -0,0 +1,536 @@ +use std::sync::Arc; +use std::{env, fs, thread, time}; +use core::time::Duration; + +use dotenv::dotenv; +use eyre::Result; +use starknet::accounts::{Account, Call, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount}; +use starknet::contract::ContractFactory; +use starknet::core::types::contract::SierraClass; +use starknet::core::utils::get_selector_from_name; +use starknet::core::types::{BlockId, BlockTag, FieldElement, FunctionCall, StarknetError}; +use starknet::providers::jsonrpc::{HttpTransport, JsonRpcClient}; +use starknet::providers::{MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage}; +use starknet::signers::{LocalWallet, SigningKey}; + +const BUILD_PATH_PREFIX: &str = "target/dev/yas_"; +// TODO: Update to New once account contracts are migrated to v1 +const ENCODING: ExecutionEncoding = ExecutionEncoding::Legacy; + +const POSITIVE: bool = false; +const NEGATIVE: bool = true; + +const HALF_SEC: Duration = time::Duration::from_millis(500); + +/// Create a StarkNet provider. +/// If the `STARKNET_RPC` environment variable is set, it will be used as the RPC URL. +/// Otherwise, the default URL will be used. +fn jsonrpc_client() -> JsonRpcClient { + let rpc_url = env::var("STARKNET_RPC").unwrap_or("https://rpc-goerli-1.starknet.rs/rpc/v0.4".into()); + JsonRpcClient::new(HttpTransport::new(url::Url::parse(&rpc_url).unwrap())) +} + +/// Get the contract artifact from the build directory. +/// # Arguments +/// * `path` - The path to the contract artifact. +/// # Returns +/// The contract artifact. +fn contract_artifact(contract_name: &str) -> Result { + let artifact_path = format!("{BUILD_PATH_PREFIX}{contract_name}.sierra.json"); + let file = fs::File::open(artifact_path) + .unwrap_or_else(|_| panic!("Compiled contract {} not found: run `make build`", contract_name)); + serde_json::from_reader(file).map_err(Into::into) +} + +/// Fetch the private key from the `PRIVATE_KEY` environment variable or prompt the user for input. +/// # Returns +/// The private key. +fn private_key_from_env_or_input() -> FieldElement { + if let Ok(pk) = env::var("PRIVATE_KEY") { + FieldElement::from_hex_be(&pk).expect("Invalid Private Key") + } else { + let input_key = rpassword::prompt_password("Enter private key: ").unwrap(); + FieldElement::from_hex_be(&input_key).expect("Invalid Private Key") + } +} + +/// Initialize a StarkNet account. +/// # Arguments +/// * `signer` - The StarkNet signer. +/// * `account_address` - The StarkNet account address. +/// # Returns +/// The StarkNet account. +async fn initialize_starknet_account( + signer: LocalWallet, + account_address: FieldElement, +) -> Result, LocalWallet>> { + let provider = jsonrpc_client(); + let chain_id = provider.chain_id().await?; + let mut account = SingleOwnerAccount::new(provider, signer, account_address, chain_id, ENCODING); + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + Ok(account) +} + +/// Check if a contract class is already declared. +/// # Arguments +/// * `provider` - The StarkNet provider. +/// * `class_hash` - The contract class hash. +/// # Returns +/// `true` if the contract class is already declared, `false` otherwise. +async fn is_already_declared

(provider: &P, class_hash: &FieldElement) -> Result +where + P: Provider, + P::Error: 'static, +{ + match provider.get_class(BlockId::Tag(BlockTag::Pending), class_hash).await { + Ok(_) => { + eprintln!("Not declaring class as it's already declared. Class hash:"); + println!("{}", format!("{:#064x}", class_hash)); + + Ok(true) + } + Err(ProviderError::StarknetError(StarknetErrorWithMessage { + code: MaybeUnknownErrorCode::Known(StarknetError::ClassHashNotFound), + .. + })) => Ok(false), + Err(err) => Err(err.into()), + } +} + +/// Declare a contract class. If the contract class is already declared, do nothing. +/// # Arguments +/// * `account` - The StarkNet account. +/// * `contract_name` - The contract name. +/// # Returns +/// The contract class hash. +async fn declare_contract( + account: &SingleOwnerAccount, LocalWallet>, + contract_name: &str, +) -> Result { + // Load the contract artifact. + let contract_artifact = contract_artifact(contract_name)?; + + // Compute the contract class hash. + let class_hash = contract_artifact.class_hash()?; + + // Declare the contract class if it is not already declared. + if !is_already_declared(account.provider(), &class_hash).await? { + println!("\n==> Declaring Contract: {contract_name}"); + let flattened_class = contract_artifact.flatten()?; + account.declare(Arc::new(flattened_class), class_hash).send().await?; + println!("Declared Class Hash: {}", format!("{:#064x}", class_hash)); + }; + + Ok(class_hash) +} + +/// Deploy ERC20 Contract. +/// +/// # Arguments +/// +/// * `name` - The name of the ERC20 token. +/// * `symbol` - The symbol of the ERC20 token. +/// * `total_supply` - The total supply of the ERC20 token. +/// * `recipient` - The initial recipient of the total supply. +/// +/// # Returns +/// +/// This function returns a `Result` indicating success or an error. +async fn deploy_erc20( + recipient: FieldElement, + account: &SingleOwnerAccount, LocalWallet>, + erc20_class_hash: FieldElement +) -> Result<(FieldElement, FieldElement)> { + // Instantiate the contract factory. + let erc20_factory = ContractFactory::new(erc20_class_hash, account); + let unique = true; + let salt = account.get_nonce().await?; + let erc20_token_0_contract_deployment = erc20_factory.deploy(vec![FieldElement::from_hex_be("0x5459415330").unwrap(), FieldElement::from_hex_be("0x2459415330").unwrap(), FieldElement::from_hex_be("0x3782dace9d900000").unwrap(), FieldElement::ZERO, recipient], salt, unique); + let erc20_token_0_deployed_address = erc20_token_0_contract_deployment.deployed_address(); + println!("Token TYAS0 Address: {}", format!("{:#064x}", erc20_token_0_deployed_address)); + let estimated_fee = erc20_token_0_contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; + erc20_token_0_contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + + let erc20_token_1_contract_deployment = erc20_factory.deploy(vec![FieldElement::from_hex_be("0x5459415331").unwrap(), FieldElement::from_hex_be("0x2459415331").unwrap(), FieldElement::from_hex_be("0x3782dace9d900000").unwrap(), FieldElement::ZERO, recipient], salt, unique); + let erc20_token_1_deployed_address = erc20_token_1_contract_deployment.deployed_address(); + println!("Token TYAS1 Address: {}", format!("{:#064x}", erc20_token_1_deployed_address)); + let estimated_fee = erc20_token_1_contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; + erc20_token_1_contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + + Ok((erc20_token_0_deployed_address, erc20_token_1_deployed_address)) +} + +/// Asynchronously initializes a liquidity pool using the provided parameters. +/// +/// # Arguments +/// +/// * `account` - The reference to a `SingleOwnerAccount` with a `JsonRpcClient` and `LocalWallet`. +/// * `pool_address` - The target address of the liquidity pool to be initialized. +/// * `price_sqrt_low` - The lower bound of the square root of the price in the liquidity pool. +/// * `price_sqrt_high` - The upper bound of the square root of the price in the liquidity pool. +/// * `sign` - A boolean flag indicating the sign of the price, where `true` represents negative and `false` represents positive. +/// +/// # Returns +/// +/// Returns a `Result` indicating success or failure. The `Ok(())` variant is returned on success, and the `Err` variant contains an error description. +async fn initialize_pool( + account: &SingleOwnerAccount, LocalWallet>, + pool_address: FieldElement, + price_sqrt_low: u128, + price_sqrt_high: u128, + sign: bool, +) -> Result<()> { + let invoke_result = account + .execute(vec![Call { + to: pool_address, + selector: get_selector_from_name("initialize").unwrap(), + calldata: vec![ + // fp mag + FieldElement::from(price_sqrt_low), + FieldElement::from(price_sqrt_high), + // sign + match sign { + NEGATIVE => FieldElement::from(1_u128), + POSITIVE => FieldElement::ZERO, + } + ], + }]).send().await?; + + println!("Transaction Hash: {}", format!("{:#064x}", invoke_result.transaction_hash)); + Ok(()) +} + +/// Asynchronously mints liquidity tokens by providing liquidity to a specified range in a YAS pool. +/// +/// # Arguments +/// +/// * `account` - The reference to a `SingleOwnerAccount` with a `JsonRpcClient` and `LocalWallet`. +/// * `pool_address` - The target address of the Uniswap V3 liquidity pool. +/// * `router_address` - The address of the Uniswap V3 router contract. +/// * `recipient` - The address where the minted liquidity tokens will be sent. +/// * `tick_lower` - The lower tick limit for the liquidity provision range. +/// * `tick_upper` - The upper tick limit for the liquidity provision range. +/// * `amount` - The amount of tokens to be provided as liquidity. +/// +/// # Returns +/// +/// Returns a `Result` indicating success or failure. The `Ok(())` variant is returned on success, and the `Err` variant contains an error description. +async fn mint( + account: &SingleOwnerAccount, LocalWallet>, + pool_address: FieldElement, + router_address: FieldElement, + recipient: FieldElement, + tick_lower: i32, + tick_upper: i32, + amount: u128, +) -> Result<()> { + let tick_lower_sign = match tick_lower.is_negative() { + true => FieldElement::from(1_u32), + false => FieldElement::ZERO, + }; + + let tick_upper_sign = match tick_upper.is_negative() { + true => FieldElement::from(1_u32), + false => FieldElement::ZERO, + }; + + let mint_result = account + .execute(vec![Call { + to: router_address, + selector: get_selector_from_name("mint").unwrap(), + calldata: vec![ + pool_address, + recipient, + FieldElement::from(tick_lower.abs() as u32), + tick_lower_sign, + FieldElement::from(tick_upper.abs() as u32), + tick_upper_sign, + FieldElement::from(amount), + ], + }]) + .send() + .await?; + + println!("Transaction Hash: {}", format!("{:#064x}", mint_result.transaction_hash)); + Ok(()) +} + +/// Asynchronously retrieves the balance of a specific token for a given wallet address. +/// +/// # Arguments +/// +/// * `token_address` - The address of the token for which the balance is to be retrieved. +/// * `wallet_address` - The address of the wallet for which the token balance is queried. +/// +/// # Returns +/// +/// Returns a `Result` containing a `FieldElement` representing the token balance. The `Ok` variant contains the balance on success, and the `Err` variant contains an error description. +async fn balance_of( + token_address: FieldElement, + wallet_address: FieldElement, +) -> Result { + let balance_result = jsonrpc_client() + .call( + FunctionCall { + contract_address: token_address, + entry_point_selector: get_selector_from_name("balanceOf").unwrap(), + calldata: vec![ + wallet_address + ], + }, + BlockId::Tag(BlockTag::Latest), + ) + .await?; + + if !balance_result.is_empty() { + Ok(balance_result[0]) + } else { + Ok(FieldElement::ZERO) + } +} + +/// Asynchronously approves spending of an unlimited amount of tokens by a specified account on behalf of the caller. +/// +/// # Arguments +/// +/// * `account` - The reference to a `SingleOwnerAccount` with a `JsonRpcClient` and `LocalWallet`. +/// * `token_address` - The address of the token contract for which approval is granted. +/// * `wallet_address` - The address of the wallet that is granted approval to spend tokens. +/// +/// # Returns +/// +/// Returns a `Result` indicating success or failure. The `Ok(())` variant is returned on success, and the `Err` variant contains an error description. +async fn approve_max( + account: &SingleOwnerAccount, LocalWallet>, + token_address: FieldElement, + wallet_address: FieldElement, +) -> Result<()> { + let approve_result = account + .execute(vec![Call { + to: token_address, + selector: get_selector_from_name("approve").unwrap(), + calldata: vec![ + wallet_address, + FieldElement::from(u128::MAX), + FieldElement::from(u128::MAX), + ], + }]) + .send() + .await?; + println!("Transaction Hash: {}", format!("{:#064x}", approve_result.transaction_hash)); + Ok(()) +} + +/// Asynchronously initiates a swap on a YAS router contract, exchanging one asset for another within a specified liquidity pool. +/// +/// # Arguments +/// +/// * `account` - The reference to a `SingleOwnerAccount` with a `JsonRpcClient` and `LocalWallet`. +/// * `router_address` - The address of the Uniswap V3 router contract. +/// * `pool_address` - The address of the Uniswap V3 liquidity pool. +/// * `recipient` - The address where the swapped assets will be sent. +/// * `zero_for_one` - A boolean flag indicating the direction of the swap: `true` for swapping token0 to token1, `false` for token1 to token0. +/// * `amount_specified_low` - The lower bound of the specified input or output amount for the swap. +/// * `amount_specified_high` - The upper bound of the specified input or output amount for the swap. +/// * `amount_specified_sign` - A boolean flag indicating the sign of the specified amount, where `true` represents negative and `false` represents positive. +/// * `sqrt_price_limit_x96_low` - The lower bound of the square root of the price limit for the swap. +/// * `sqrt_price_limit_x96_high` - The upper bound of the square root of the price limit for the swap. +/// * `sqrt_price_limit_x96_sign` - A boolean flag indicating the sign of the square root of the price limit, where `true` represents negative and `false` represents positive. +/// +/// # Returns +/// +/// Returns a `Result` indicating success or failure. The `Ok(())` variant is returned on success, and the `Err` variant contains an error description. +async fn swap( + account: &SingleOwnerAccount, LocalWallet>, + router_address: FieldElement, + pool_address: FieldElement, + recipient: FieldElement, + zero_for_one: bool, + amount_specified_low: u128, + amount_specified_high: u128, + amount_specified_sign: bool, + sqrt_price_limit_x96_low: u128, + sqrt_price_limit_x96_high: u128, + sqrt_price_limit_x96_sign: bool +) -> Result<()> { + let amount_specified_sign_felt = if amount_specified_sign { + FieldElement::from(1_u32) + } else { + FieldElement::ZERO + }; + + let zero_for_one_felt = if zero_for_one { + FieldElement::from(1_u32) + } else { + FieldElement::ZERO + }; + + let sqrt_price_limit_x96_sign_felt = if sqrt_price_limit_x96_sign { + FieldElement::from(1_u32) + } else { + FieldElement::ZERO + }; + + let swap_result = account + .execute(vec![Call { + to: router_address, + selector: get_selector_from_name("swap").unwrap(), + calldata: vec![ + pool_address, + recipient, + zero_for_one_felt, + // amount specified + FieldElement::from(amount_specified_low), + FieldElement::from(amount_specified_high), + amount_specified_sign_felt, + // sqrt_price_limit + FieldElement::from(sqrt_price_limit_x96_low), + FieldElement::from(sqrt_price_limit_x96_high), + sqrt_price_limit_x96_sign_felt, + ], + }]) + .send() + .await?; + println!("Transaction Hash: {}", format!("{:#064x}", swap_result.transaction_hash)); + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenv().ok(); + + // Create signer from private key. + let private_key = private_key_from_env_or_input(); + let signer = LocalWallet::from(SigningKey::from_secret_scalar(private_key)); + + // Create a StarkNet account. + let account_address = FieldElement::from_hex_be(&env::var("ACCOUNT_ADDRESS").expect("ACCOUNT_ADDRESS not set")) + .expect("Invalid Account Address"); + let account = initialize_starknet_account(signer, account_address).await?; + + // Declare the contract classes if they are not already declared. + let erc20_class_hash = declare_contract(&account, "ERC20").await?; + let factory_class_hash = declare_contract(&account, "YASFactory").await?; + let pool_class_hash = declare_contract(&account, "YASPool").await?; + let router_class_hash = declare_contract(&account, "YASRouter").await?; + + let unique = true; + let owner_address = FieldElement::from_hex_be(&env::var("OWNER_ADDRESS").expect("OWNER_ADDRESS not set")) + .expect("Invalid Owner Address"); + + println!("\n==> Deploying ERC20 Contracts"); + let (token_0, token_1) = deploy_erc20(owner_address, &account, erc20_class_hash).await?; + + // Instantiate the contract factory. + println!("\n==> Deploying Factory Contract"); + let salt = account.get_nonce().await?; + let yas_factory_contract_factory = ContractFactory::new(factory_class_hash, &account); + let contract_deployment = yas_factory_contract_factory.deploy(vec![owner_address, pool_class_hash], salt, unique); + let factory_address = contract_deployment.deployed_address(); + println!("Factory Contract Address: {}", format!("{:#064x}", factory_address)); + + // Estimate the deployment fee and deploy the contract. + let estimated_fee = contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; // add buffer + let tx = contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + println!("Transaction Hash: {}", format!("{:#064x}", tx)); + + // Instantiate the contract factory. + println!("\n==> Deploying Router Contract"); + let salt = account.get_nonce().await?; + let yas_router_contract_factory = ContractFactory::new(router_class_hash, &account); + let contract_deployment = yas_router_contract_factory.deploy(vec![], salt, unique); + let router_address = contract_deployment.deployed_address(); + println!("Router Contract Address: {}", format!("{:#064x}", router_address)); + + // Estimate the deployment fee and deploy the contract. + let estimated_fee = contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; // add buffer + let tx = contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + println!("Transaction Hash: {}", format!("{:#064x}", tx)); + + // Instantiate the contract factory. + println!("\n==> Deploying Pool Contract"); + let salt = account.get_nonce().await?; + let yas_pool_contract_factory = ContractFactory::new(pool_class_hash, &account); + let contract_deployment = yas_pool_contract_factory.deploy(vec![factory_address, token_0, token_1, FieldElement::from_hex_be("0x0bb8").unwrap(), FieldElement::from_hex_be("0x3c").unwrap(), FieldElement::ZERO], salt, unique); + let pool_address = contract_deployment.deployed_address(); + println!("Pool Contract Address: {}", format!("{:#064x}", pool_address)); + + // Estimate the deployment fee and deploy the contract. + let estimated_fee = contract_deployment.estimate_fee().await?.overall_fee * 3 / 2; // add buffer + let tx = contract_deployment.max_fee(estimated_fee.into()).send().await?.transaction_hash; + println!("Transaction Hash: {}", format!("{:#064x}", tx)); + + println!("\n==> Initialize Pool"); + initialize_pool( + &account, + pool_address, + // The price of the initial tokens is 1:1 (encode_price_sqrt_1_1) + 79228162514264337593543950336, + 0, + POSITIVE + ).await?; + + println!("\n==> Approve"); + approve_max(&account, token_0, router_address).await?; + approve_max(&account, token_1, router_address).await?; + thread::sleep(HALF_SEC); + + let owner_t0_balance_bf_mint = balance_of(token_0, owner_address).await?; + let owner_t1_balance_bf_mint = balance_of(token_1, owner_address).await?; + println!("\n==> Mint"); + mint( + &account, + pool_address, + router_address, + owner_address, + -887220, + 887220, + 2000000000000000000 + ).await?; + thread::sleep(HALF_SEC); + + let owner_t0_balance = balance_of(token_0, owner_address).await?; + let owner_t1_balance = balance_of(token_1, owner_address).await?; + println!("Owner balance before Mint"); + println!("$YAS0: {}", owner_t0_balance_bf_mint); + println!("$YAS1: {}", owner_t1_balance_bf_mint); + println!("Owner balance after Mint"); + println!("$YAS0: {}", owner_t0_balance); + println!("$YAS1: {}", owner_t1_balance); + + let owner_t0_balance_bf_swap = balance_of(token_0, owner_address).await?; + let owner_t1_balance_bf_swap = balance_of(token_1, owner_address).await?; + println!("\n==> Swap"); + println!("500000000000000000 $YAS0 to $YAS1"); + swap( + &account, + router_address, + pool_address, + owner_address, + true, + 500000000000000000, + 0, + true, + 4295128740, + 0, + POSITIVE + ).await?; + thread::sleep(HALF_SEC); + + let owner_t0_balance = balance_of(token_0, owner_address).await?; + let owner_t1_balance = balance_of(token_1, owner_address).await?; + println!("Owner balance before Swap"); + println!("$YAS0: {}", owner_t0_balance_bf_swap); + println!("$YAS1: {}", owner_t1_balance_bf_swap); + println!("Owner balance after Swap"); + println!("$YAS0: {}", owner_t0_balance); + println!("$YAS1: {}", owner_t1_balance); + + let pool_t0_balance = balance_of(token_0, pool_address).await?; + let pool_t1_balance = balance_of(token_1, pool_address).await?; + println!("\nPool balance"); + println!("$YAS0: {}", pool_t0_balance); + println!("$YAS1: {}", pool_t1_balance); + + Ok(()) +} diff --git a/src/contracts/yas_pool.cairo b/src/contracts/yas_pool.cairo index 5a8e029e..f9b45aad 100644 --- a/src/contracts/yas_pool.cairo +++ b/src/contracts/yas_pool.cairo @@ -182,7 +182,6 @@ mod YASPool { self.token_1.write(token_1); self.fee.write(fee); self.tick_spacing.write(tick_spacing); - //TODO: temporary component syntax let state = Tick::unsafe_new_contract_state(); self @@ -279,8 +278,8 @@ mod YASPool { // continue swapping as long as we haven't used the entire input/output and haven't reached the price limit loop { - if state.amount_specified_remaining.is_non_zero() - && state.sqrt_price_X96 != sqrt_price_limit_X96 { + if state.amount_specified_remaining.is_zero() + || state.sqrt_price_X96 == sqrt_price_limit_X96 { break; } @@ -508,6 +507,7 @@ mod YASPool { }; let callback_contract = get_caller_address(); + assert(is_valid_callback_contract(callback_contract), 'invalid callback_contract'); let dispatcher = IYASMintCallbackDispatcher { contract_address: callback_contract }; dispatcher.yas_mint_callback(amount_0, amount_1, data); @@ -589,7 +589,6 @@ mod YASPool { max_liquidity_per_tick ); } - if flipped_lower { TickBitmapImpl::flip_tick( ref tick_bitmap_state, position_key.tick_lower, self.tick_spacing.read() @@ -630,7 +629,6 @@ mod YASPool { TickImpl::clear(ref tick_state, position_key.tick_upper); } } - // read again to obtain Info with changes in the update step PositionImpl::get(@position_state, position_key) } @@ -652,7 +650,6 @@ mod YASPool { } let slot_0 = self.slot_0.read(); - let position = self .update_position(params.position_key, params.liquidity_delta, slot_0.tick); @@ -670,13 +667,13 @@ mod YASPool { ); } else if slot_0.tick < params.position_key.tick_upper { // current tick is inside the passed range - amount_0 = SqrtPriceMath::get_amount_0_delta_signed_token( slot_0.sqrt_price_X96, get_sqrt_ratio_at_tick(params.position_key.tick_upper), params.liquidity_delta ); + amount_1 = SqrtPriceMath::get_amount_1_delta_signed_token( get_sqrt_ratio_at_tick(params.position_key.tick_lower), diff --git a/src/contracts/yas_router.cairo b/src/contracts/yas_router.cairo index 83473f76..a6028c08 100644 --- a/src/contracts/yas_router.cairo +++ b/src/contracts/yas_router.cairo @@ -27,6 +27,20 @@ trait IYASRouter { fn yas_swap_callback( ref self: TContractState, amount_0_delta: i256, amount_1_delta: i256, data: Array ); + fn swap_exact_0_for_1( + self: @TContractState, + pool: ContractAddress, + amount_in: u256, + recipient: ContractAddress, + sqrt_price_limit_X96: FixedType + ) -> (i256, i256); + fn swap_exact_1_for_0( + self: @TContractState, + pool: ContractAddress, + amount_in: u256, + recipient: ContractAddress, + sqrt_price_limit_X96: FixedType + ) -> (i256, i256); } #[starknet::contract] @@ -37,8 +51,8 @@ mod YASRouter { use yas::contracts::yas_pool::{IYASPoolDispatcher, IYASPoolDispatcherTrait}; use yas::interfaces::interface_ERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; - use yas::numbers::signed_integer::{i32::i32, i256::i256}; use yas::numbers::fixed_point::implementations::impl_64x96::FixedType; + use yas::numbers::signed_integer::{i32::i32, i256::i256, integer_trait::IntegerTrait}; #[event] #[derive(Drop, starknet::Event)] @@ -153,5 +167,38 @@ mod YASRouter { ); } } + fn swap_exact_0_for_1( + self: @ContractState, + pool: ContractAddress, + amount_in: u256, + recipient: ContractAddress, + sqrt_price_limit_X96: FixedType + ) -> (i256, i256) { + IYASPoolDispatcher { contract_address: pool } + .swap( + recipient, + true, + IntegerTrait::::new(amount_in, false), + sqrt_price_limit_X96, + array![get_caller_address().into()] + ) + } + + fn swap_exact_1_for_0( + self: @ContractState, + pool: ContractAddress, + amount_in: u256, + recipient: ContractAddress, + sqrt_price_limit_X96: FixedType + ) -> (i256, i256) { + IYASPoolDispatcher { contract_address: pool } + .swap( + recipient, + true, + IntegerTrait::::new(amount_in, true), + sqrt_price_limit_X96, + array![get_caller_address().into()] + ) + } } } diff --git a/src/tests/test_contracts/test_yas_factory.cairo b/src/tests/test_contracts/test_yas_factory.cairo index 0f11813e..998f7b62 100644 --- a/src/tests/test_contracts/test_yas_factory.cairo +++ b/src/tests/test_contracts/test_yas_factory.cairo @@ -10,7 +10,6 @@ mod YASFactoryTests { }; use yas::numbers::signed_integer::i32::i32; - fn deploy(deployer: ContractAddress, pool_class_hash: ClassHash) -> IYASFactoryDispatcher { let (address, _) = deploy_syscall( YASFactory::TEST_CLASS_HASH.try_into().unwrap(), diff --git a/src/tests/test_contracts/test_yas_pool.cairo b/src/tests/test_contracts/test_yas_pool.cairo index 68b03b08..e4b25555 100644 --- a/src/tests/test_contracts/test_yas_pool.cairo +++ b/src/tests/test_contracts/test_yas_pool.cairo @@ -488,7 +488,7 @@ mod YASPoolTests { use yas::libraries::tick::{Tick, Tick::TickImpl}; use yas::libraries::tick_math::{TickMath::MIN_TICK, TickMath::MAX_TICK}; use yas::libraries::position::{Info, Position, Position::PositionImpl, PositionKey}; - use yas::tests::utils::constants::PoolConstants::{TOKEN_A, TOKEN_B}; + use yas::tests::utils::constants::PoolConstants::{TOKEN_A, TOKEN_B, WALLET}; use yas::tests::utils::constants::FactoryConstants::{FeeAmount, fee_amount, tick_spacing}; use yas::contracts::yas_erc20::{ERC20, ERC20::ERC20Impl, IERC20Dispatcher}; use yas::numbers::signed_integer::{ @@ -530,8 +530,8 @@ mod YASPoolTests { let balance_token_0 = token_0.balanceOf(yas_pool.contract_address); let balance_token_1 = token_1.balanceOf(yas_pool.contract_address); - assert(balance_token_0 == 9996, 'wrong balance token 0'); - assert(balance_token_1 == 1000, 'wrong balance token 1'); + assert(balance_token_0 == 2000000000000000000, 'wrong balance token 0'); + assert(balance_token_1 == 2000000000000000000, 'wrong balance token 1'); } // TODO: 'max tick with max leverage' // TODO: 'works for max tick' @@ -586,20 +586,19 @@ mod YASPoolTests { let yas_factory = deploy_factory(OWNER(), POOL_CLASS_HASH()); // 0x2 // Deploy ERC20 tokens with factory address - // set_contract_address(yas_factory.contract_address); - let token_0 = deploy_erc20('YAS0', '$YAS0', BoundedInt::max(), OWNER()); // 0x3 - let token_1 = deploy_erc20('YAS1', '$YAS1', BoundedInt::max(), OWNER()); // 0x4 + let token_0 = deploy_erc20('YAS0', '$YAS0', 4000000000000000000, OWNER()); // 0x3 + let token_1 = deploy_erc20('YAS1', '$YAS1', 4000000000000000000, OWNER()); // 0x4 set_contract_address(OWNER()); - token_0.transfer(WALLET(), 9996); - token_1.transfer(WALLET(), 1000); + token_0.transfer(WALLET(), 4000000000000000000); + token_1.transfer(WALLET(), 4000000000000000000); // Give permissions to expend WALLET() tokens set_contract_address(WALLET()); token_1.approve(mint_callback.contract_address, BoundedInt::max()); token_0.approve(mint_callback.contract_address, BoundedInt::max()); - let encode_price_sqrt_1_10 = FP64x96Impl::new(25054144837504793118641380156, false); + let encode_price_sqrt_1_1 = FP64x96Impl::new(79228162514264337593543950336, false); let yas_pool_address = yas_factory // 0x5 .create_pool( @@ -608,11 +607,11 @@ mod YASPoolTests { let yas_pool = IYASPoolDispatcher { contract_address: yas_pool_address }; set_contract_address(OWNER()); - yas_pool.initialize(encode_price_sqrt_1_10); + yas_pool.initialize(encode_price_sqrt_1_1); let (min_tick, max_tick) = get_min_tick_and_max_tick(); set_contract_address(WALLET()); - mint_callback.mint(yas_pool_address, WALLET(), min_tick, max_tick, 3161); + mint_callback.mint(yas_pool_address, WALLET(), min_tick, max_tick, 2000000000000000000); (yas_pool, token_0, token_1) }