diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9653134..ddf85ea 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -labels: +labels: iBriz assignees: '' --- @@ -21,4 +21,4 @@ Steps to reproduce the behavior: A clear and concise description of what you expected to happen. **Additional context** -Add any other context about the problem here. \ No newline at end of file +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 8a5b6e0..0805004 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest an idea for this project title: '' -labels: +labels: iBriz assignees: '' --- @@ -17,4 +17,4 @@ A clear and concise description of what you want to happen. A clear and concise description of any alternative solutions or features you've considered. **Additional context** -Add any other context or screenshots about the feature request here. \ No newline at end of file +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/task-template.md b/.github/ISSUE_TEMPLATE/task-template.md index e7c7aeb..901bca6 100644 --- a/.github/ISSUE_TEMPLATE/task-template.md +++ b/.github/ISSUE_TEMPLATE/task-template.md @@ -2,7 +2,7 @@ name: Task Template about: New tasks created should consist of the following information title: '' -labels: +labels: iBriz assignees: '' --- @@ -24,4 +24,4 @@ Ex: When restarting the relay, synchronization picks up from the most recently s ### Additional Information -Describe anything relevant that hasn't been mentioned yet. \ No newline at end of file +Describe anything relevant that hasn't been mentioned yet. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..7b083b8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,11 @@ +cicd: + - '.github/workflows/*' + +scripts: + - 'scripts/*' + +test: + - 'test/**' + +documentation: +- '**/*.md' \ No newline at end of file diff --git a/.github/workflows/build-test-soroban-contracts.yml b/.github/workflows/build-test-soroban-contracts.yml new file mode 100644 index 0000000..560223d --- /dev/null +++ b/.github/workflows/build-test-soroban-contracts.yml @@ -0,0 +1,35 @@ +name: Build +on: + push: + branches: + - '**' + pull_request: + branches: + - main + +jobs: + Build: + name: Build Soroban Contracts + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: true + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: 1.79.0 + target: wasm32-unknown-unknown + override: true + profile: minimal + + - name: Install wasm32 + run: | + rustup target add wasm32-unknown-unknown + cargo install --locked soroban-cli + + - name: Build & Test soroban Contracts + run: | + soroban contract build diff --git a/.github/workflows/soroban-codecov.yml b/.github/workflows/soroban-codecov.yml new file mode 100644 index 0000000..8ca37f4 --- /dev/null +++ b/.github/workflows/soroban-codecov.yml @@ -0,0 +1,43 @@ +name: Soroban contracts Codecov + +on: + pull_request: + branches: + - "main" + push: + branches: + - "**" + +jobs: + code-coverage: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Install Rust + run: rustup update stable + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Install wasm32 + run: | + rustup target add wasm32-unknown-unknown + cargo install --locked soroban-cli + - name: Build & Test soroban Contracts + run: | + soroban contract build + - name: Generate code coverage + run: + cargo llvm-cov --lcov --output-path lcov.info + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: lcov.info + flags: rust + fail_ci_if_error: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3cd37e2..75ea5e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,205 +1,14 @@ -### Java ### -# Compiled class file -*.class +target +.soroban -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -.idea - -.gradle -**/build/ -!gradle-wrapper.jar - -gradle.properties - -.project -.classpath -.settings -**/bin/ -**/pkg/ - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* - -### Rust ### -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ -artifacts/* - -report/** - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -#Cargo.lock -artifacts - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - -### Solidity ### -# Logs -logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files +# environment variables .env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# Code Coverage -lcov.info - -# vscode files -.vscode - -**/.DS_Store - - -vendor/** -scripts/download_buf.sh +.env.production -test/e2e-demo/ibc-config/** +# macOS-specific files +.DS_Store +Cargo.lock +test_snapshots +.VSCodeCounter -.xcall-multi \ No newline at end of file +artifacts/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..77b02ae --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +resolver = "2" +members = [ + "contracts/*", + "libs/*" +] + +[workspace.dependencies] +soroban-sdk = "21.6.0" + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true + +# For more information about this profile see https://soroban.stellar.org/docs/basic-tutorials/logging#cargotoml-profile +[profile.release-with-logs] +inherits = "release" +debug-assertions = true \ No newline at end of file diff --git a/README.md b/README.md index 61c1e4d..d371897 100644 --- a/README.md +++ b/README.md @@ -1 +1,104 @@ [![codecov](https://codecov.io/gh/balancednetwork/balanced-soroban-contracts/graph/badge.svg?token=6Epcv9Uek5)](https://codecov.io/gh/balancednetwork/balanced-soroban-contracts) ![build](https://github.com/balancednetwork/balanced-soroban-contracts/actions/workflows/build-test-soroban-contracts.yml/badge.svg) + +# Balanced Package Structure + +## Overview + +The Balanced contracts within the Stellar blockchain ecosystem is designed to manage various aspects of the decentralized application (dApp) including asset management, cross-chain communication, and stable coin operations. This structure ensures efficient handling of these operations through well-defined smart contracts and unique contract addresses. The smart contracts are written using Soroban framework of Rust programming language + +## Contracts + +The Balanced Stellar Spoke have three main smart contracts, each responsible for specific functionalities: + +### 1. Asset Manager: asset_manager +- **Purpose**: Manages assets within the Balanced ecosystem. + +### 2. xCall Manager: xcall_manager +- **Purpose**: Facilitates cross-chain administration from the icon side. + +### 3. Balanced Dollar: balanced_dollar +- **Purpose**: Includes the bnUSD token contract and Manages the crosschain bnUSD operations within the Balanced ecosystem. + +## Identifiers + +### Contract Addresses +- **Definition**: Addresses of each each smart contract on the stellar blockchain +- **Usage**: Used while called using RPC and cross contract calls, also used in cross-chain configuration + +```shell +soroban contract invoke --id $contract_address \ +--source-account $ACCOUNT --rpc-url $ENDPOINT \ +--network-passphrase "$PASSPHRASE" -- \ +#method_name --arg1 $arg1 --arg2 $arg2 +``` + +## Frontend Integration Interfaces + +This guide provides an overview of the key functions for interacting with the Stellar blockchain within your frontend application. These functions are part of the Asset Manager, Balanced Dollar, and XCall smart contracts, which allow for token deposits, cross-chain transfers, and cross-chain calls. + +### Important Note: Renting mechanism in Stellar Blockchain and TTL of storage + +Stellar has unique renting mechanism implemented on its smart contracts.. + +--- + +### Asset Manager Smart contract + +The Asset Manager Contract handles depositing Stellar tokens in balanced. + +#### `deposit` + +Deposits a specified amount of a token into the Sui blockchain. +``` typescript + deposit( + from: Address, //Address from which the transaction is initiated + token: Address, // Address of the token being deposited + amount: u128, // Amount of the token being deposited + to: Option,// (Optional) The recipient's address if needed. + data: Option, // (Optional) Any additional data you want to attach to the deposit. + ); +``` +The above method can be RPC called using the shell script with the following code: +```shell + soroban contract invoke --id $assetManagerContractAddress \ + --source-account $ACCOUNT --rpc-url $ENDPOINT \ + --network-passphrase "$PASSPHRASE" -- \ + deposit --from $fromAddress --token $tokenAddress \ + --amount $amount --to $toNetworkAddress +``` +Both native token and Other fungible tokens can be deposited using the `deposit` method + +### Balanced Dollar Module + +The Balanced Dollar Contract facilitates the transfer of `BALANCED_DOLLAR` tokens across chains. + +#### `cross_transfer` + +Transfers `BALANCED_DOLLAR` tokens across chains. +```typescript + function cross_transfer( + from: Address, //Address from which the transaction is initiated + amount: u128, // Amount of the balanced dollar being deposited + to: String, // The recipient's address on the destination chain. + data: Option // (Optional) Any additional data to attach to the transfer. + ) +``` +The above method can be called using following shell script: +```shell + soroban contract invoke --id $bnUSDContractAddress \ + --rpc-url $ENDPOINT --network-passphrase "$PASSPHRASE" \ + --source-account $ACCOUNT -- cross_transfer --from $fromAddress -- amount $amount --to $toNetworkAddress +``` + +### XCallManager Contract + +#### get_protocols +The `get_protocols` function retrieves the sources and destinations associated with a given configuration. + +```typescript +function get_protocols( +) -> Result<( // Returns a tuple containing two arrays: sources and destinations or error if occurred + Vec, + Vec +), ContractError> +``` diff --git a/contracts/asset_manager/Cargo.toml b/contracts/asset_manager/Cargo.toml new file mode 100644 index 0000000..a23ae57 --- /dev/null +++ b/contracts/asset_manager/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "asset-manager" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +testutils = ["soroban-sdk/testutils"] + +[dependencies] +soroban-sdk = { workspace = true } +soroban-rlp = { path = "../../libs/soroban-rlp" } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/contracts/asset_manager/src/config.rs b/contracts/asset_manager/src/config.rs new file mode 100644 index 0000000..5ae6f70 --- /dev/null +++ b/contracts/asset_manager/src/config.rs @@ -0,0 +1,22 @@ +use crate::storage_types::DataKey; +use soroban_sdk::{contracttype, unwrap::UnwrapOptimized, Address, Env, String}; + +#[derive(Clone)] +#[contracttype] +pub struct ConfigData { + pub xcall: Address, + pub xcall_manager: Address, + pub native_address: Address, + pub icon_asset_manager: String, + pub xcall_network_address: String, + pub upgrade_authority: Address, +} + +pub fn set_config(e: &Env, config: ConfigData) { + e.storage().instance().set(&DataKey::Config, &config); +} + +pub fn get_config(e: &Env) -> ConfigData { + let key = DataKey::Config; + e.storage().instance().get(&key).unwrap_optimized() +} diff --git a/contracts/asset_manager/src/contract.rs b/contracts/asset_manager/src/contract.rs new file mode 100644 index 0000000..d9ad8a5 --- /dev/null +++ b/contracts/asset_manager/src/contract.rs @@ -0,0 +1,354 @@ +use soroban_sdk::{ + contract, contractimpl, panic_with_error, token, Address, Bytes, BytesN, Env, String, Vec, +}; +mod xcall { + soroban_sdk::contractimport!(file = "../../wasm/xcall.wasm"); +} +use crate::errors::ContractError; +use crate::storage_types::TokenData; +use crate::{ + config::{self, get_config, set_config, ConfigData}, + states::{ + extent_ttl, has_registry, read_administrator, read_token_data, read_tokens, + write_administrator, write_registry, write_token_data, write_tokens, + }, + storage_types::POINTS, + xcall_manager_interface::XcallManagerClient, +}; +use soroban_rlp::balanced::address_utils::is_valid_string_address; +use soroban_rlp::balanced::messages::{ + deposit::Deposit, deposit_revert::DepositRevert, withdraw_to::WithdrawTo, +}; + +use xcall::{AnyMessage, CallMessageWithRollback, Client, Envelope}; + +const DEPOSIT_NAME: &str = "Deposit"; +const WITHDRAW_TO_NAME: &str = "WithdrawTo"; +const DEPOSIT_REVERT_NAME: &str = "DepositRevert"; + +#[contract] +pub struct AssetManager; + +#[contractimpl] +impl AssetManager { + pub fn initialize(env: Env, registry: Address, admin: Address, config: ConfigData) { + if has_registry(&env.clone()) { + panic_with_error!(&env, ContractError::ContractAlreadyInitialized) + } + write_registry(&env, ®istry); + write_administrator(&env, &admin); + Self::configure(env, config); + } + + pub fn get_config(env: Env) -> ConfigData { + get_config(&env) + } + + pub fn set_admin(env: Env, new_admin: Address) { + let admin = read_administrator(&env); + admin.require_auth(); + + write_administrator(&env, &new_admin); + } + + pub fn get_admin(env: Env) -> Address { + read_administrator(&env) + } + + pub fn configure(env: Env, config: ConfigData) { + let admin = read_administrator(&env); + admin.require_auth(); + + set_config(&env, config); + } + + pub fn configure_rate_limit( + env: Env, + token_address: Address, + period: u64, + percentage: u32, + ) -> Result<(), ContractError> { + let admin = read_administrator(&env); + admin.require_auth(); + let tokens = read_tokens(&env); + if tokens.contains(&token_address) { + return Err(ContractError::TokenExists); + } else { + write_tokens(&env, token_address.clone()); + } + + if percentage > POINTS as u32 { + return Err(ContractError::PercentageShouldBeLessThanOrEqualToPOINTS); + } + + write_token_data( + &env, + token_address, + TokenData { + period, + percentage, + last_update: env.ledger().timestamp(), + current_limit: 0, + }, + ); + Ok(()) + } + + pub fn get_rate_limit(env: Env, token_address: Address) -> (u64, u32, u64, u64) { + let data: TokenData = read_token_data(&env, token_address); + ( + data.period, + data.percentage, + data.last_update, + data.current_limit, + ) + } + + pub fn reset_limit(env: Env, token: Address) { + let balance = Self::get_token_balance(&env, token.clone()); + let mut data: TokenData = read_token_data(&env, token.clone()); + data.current_limit = (balance * data.percentage as u128 / POINTS) as u64; + write_token_data(&env, token, data); + } + + pub fn get_withdraw_limit(env: Env, token: Address) -> Result { + let balance = Self::get_token_balance(&env, token.clone()); + return Ok(Self::calculate_limit(&env, balance, token)?); + } + + fn get_token_balance(env: &Env, token: Address) -> u128 { + let token_client = token::Client::new(env, &token); + return token_client.balance(&env.current_contract_address()) as u128; + } + + pub fn verify_withdraw(env: Env, token: Address, amount: u128) -> Result { + let balance = Self::get_token_balance(&env, token.clone()); + let limit = Self::calculate_limit(&env, balance, token.clone())?; + if balance - amount < limit { + panic_with_error!(&env, ContractError::ExceedsWithdrawLimit); + }; + let mut data: TokenData = read_token_data(&env, token.clone()); + data.current_limit = limit as u64; + data.last_update = env.ledger().timestamp(); + write_token_data(&env, token, data); + Ok(true) + } + + pub fn calculate_limit( + env: &Env, + balance: u128, + token: Address, + ) -> Result { + let data: TokenData = read_token_data(&env, token); + let period: u128 = data.period as u128; + let percentage: u128 = data.percentage as u128; + if period == 0 { + return Ok(0); + } + + let min_reserve = (balance * percentage) / POINTS; + + let max_withdraw = balance - min_reserve; + let last_update: u64 = data.last_update; + let time_diff = (&env.ledger().timestamp() - last_update) / 1000; + + let allowed_withdrawal = (max_withdraw * time_diff as u128) / period; + let mut reserve: u128 = data.current_limit as u128; + + if reserve > allowed_withdrawal { + reserve = reserve - allowed_withdrawal; + } + + let reserve = if reserve > min_reserve { + reserve + } else { + min_reserve + }; + Ok(reserve) + } + + pub fn deposit( + e: Env, + from: Address, + token: Address, + amount: u128, + to: Option, + data: Option, + ) -> Result<(), ContractError> { + let deposit_to = to.unwrap_or(String::from_str(&e, "")); + let deposit_data = data.unwrap_or(Bytes::from_array(&e, &[0u8; 32])); + + Ok(Self::send_deposit_message( + e, + from, + token, + amount, + deposit_to, + deposit_data, + )?) + } + + fn send_deposit_message( + e: Env, + from: Address, + token: Address, + amount: u128, + to: String, + data: Bytes, + ) -> Result<(), ContractError> { + from.require_auth(); + let current_address = e.current_contract_address(); + Self::transfer_token_to( + &e, + from.clone(), + token.clone(), + current_address.clone(), + amount, + ); + + let xcall_message: Deposit = Deposit::new( + token.to_string(), + from.to_string(), + to.clone(), + amount, + data, + ); + + let rollback: DepositRevert = DepositRevert::new(token, from.clone(), amount); + let config = get_config(&e); + let rollback_bytes = rollback.encode(&e, String::from_str(&e, DEPOSIT_REVERT_NAME)); + let message_bytes = xcall_message.encode(&e, String::from_str(&e, DEPOSIT_NAME)); + let (sources, destinations) = + Self::xcall_manager(&e, &config.xcall_manager).get_protocols(); + let message = AnyMessage::CallMessageWithRollback(CallMessageWithRollback { + data: message_bytes, + rollback: rollback_bytes, + }); + let envelope: &Envelope = &Envelope { + destinations, + message, + sources, + }; + + Self::xcall_client(&e, &config.xcall).send_call( + &from, + ¤t_address, + envelope, + &config.icon_asset_manager, + ); + Ok(()) + } + + fn xcall_manager(e: &Env, xcall_manager: &Address) -> XcallManagerClient<'static> { + let client = XcallManagerClient::new(e, xcall_manager); + return client; + } + + fn xcall_client(e: &Env, xcall: &Address) -> Client<'static> { + return xcall::Client::new(e, xcall); + } + + pub fn handle_call_message( + e: Env, + from: String, + data: Bytes, + protocols: Vec, + ) -> Result<(), ContractError> { + let config = get_config(&e); + let xcall = config.xcall; + xcall.require_auth(); + + let method = Deposit::get_method(&e, data.clone()); + + let icon_asset_manager = config.icon_asset_manager; + let current_contract = e.current_contract_address(); + if method == String::from_str(&e, &WITHDRAW_TO_NAME) { + if from != icon_asset_manager { + return Err(ContractError::OnlyICONAssetManager); + } + let message = WithdrawTo::decode(&e, data); + if !is_valid_string_address(&message.to) + || !is_valid_string_address(&message.token_address) + { + return Err(ContractError::InvalidAddress); + } + Self::withdraw( + &e, + current_contract, + Address::from_string(&message.token_address), + Address::from_string(&message.to), + message.amount, + )?; + } else if method == String::from_str(&e, &DEPOSIT_REVERT_NAME) { + if config.xcall_network_address != from { + return Err(ContractError::OnlyCallService); + } + let message: DepositRevert = DepositRevert::decode(&e.clone(), data); + Self::withdraw( + &e, + current_contract, + message.token_address, + message.to, + message.amount, + )?; + } else { + return Err(ContractError::UnknownMessageType); + } + if !Self::xcall_manager(&e, &config.xcall_manager).verify_protocols(&protocols) { + return Err(ContractError::ProtocolMismatch); + } + Ok(()) + } + + pub fn withdraw( + e: &Env, + from: Address, + token: Address, + to: Address, + amount: u128, + ) -> Result<(), ContractError> { + if amount <= 0 { + return Err(ContractError::AmountIsLessThanMinimumAmount); + } + + let verified = Self::verify_withdraw(e.clone(), token.clone(), amount)?; + if verified { + Self::transfer_token_to(e, from, token, to, amount); + } + Ok(()) + } + + fn transfer_token_to(e: &Env, from: Address, token: Address, to: Address, amount: u128) { + let token_client = token::Client::new(e, &token); + token_client.transfer(&from, &to, &(amount as i128)); + } + + pub fn balance_of(e: Env, token: Address) -> i128 { + let token_client = token::Client::new(&e, &token); + return token_client.balance(&e.current_contract_address()); + } + + pub fn has_registry(e: Env) -> bool { + has_registry(&e) + } + + pub fn set_upgrade_authority(e: Env, upgrade_authority: Address) { + let mut config = config::get_config(&e); + + config.upgrade_authority.require_auth(); + + config.upgrade_authority = upgrade_authority; + config::set_config(&e, config); + } + + pub fn upgrade(e: Env, new_wasm_hash: BytesN<32>) { + let config = get_config(&e); + config.upgrade_authority.require_auth(); + + e.deployer().update_current_contract_wasm(new_wasm_hash); + } + + pub fn extend_ttl(e: Env) { + extent_ttl(&e); + } +} diff --git a/contracts/asset_manager/src/errors.rs b/contracts/asset_manager/src/errors.rs new file mode 100644 index 0000000..535c2da --- /dev/null +++ b/contracts/asset_manager/src/errors.rs @@ -0,0 +1,20 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ContractError { + InvalidRlpLength = 1, + InvalidRollbackMessage = 2, + ContractAlreadyInitialized = 3, + PercentageShouldBeLessThanOrEqualToPOINTS = 4, + ExceedsWithdrawLimit = 5, + AmountIsLessThanMinimumAmount = 6, + ProtocolMismatch = 7, + OnlyICONAssetManager = 8, + OnlyCallService = 9, + UnknownMessageType = 10, + AdminRequired = 11, + TokenExists = 12, + InvalidAddress = 13, +} diff --git a/contracts/asset_manager/src/lib.rs b/contracts/asset_manager/src/lib.rs new file mode 100644 index 0000000..b3ed8bd --- /dev/null +++ b/contracts/asset_manager/src/lib.rs @@ -0,0 +1,9 @@ +#![no_std] + +pub mod contract; +pub mod storage_types; +pub mod tests; +pub mod states; +mod config; +mod errors; +mod xcall_manager_interface; \ No newline at end of file diff --git a/contracts/asset_manager/src/states.rs b/contracts/asset_manager/src/states.rs new file mode 100644 index 0000000..e6fbd80 --- /dev/null +++ b/contracts/asset_manager/src/states.rs @@ -0,0 +1,91 @@ +use soroban_sdk::{Address, Env, Vec}; + +use crate::storage_types::{DataKey, TokenData}; + +pub(crate) const DAY_IN_LEDGERS: u32 = 17280; +pub(crate) const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; +pub(crate) const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +pub fn has_administrator(e: &Env) -> bool { + let key: DataKey = DataKey::Admin; + e.storage().instance().has(&key) +} + +pub fn read_administrator(e: &Env) -> Address { + let key = DataKey::Admin; + e.storage().instance().get(&key).unwrap() +} + +pub fn write_administrator(e: &Env, id: &Address) { + let key = DataKey::Admin; + e.storage().instance().set(&key, id); +} + +pub fn has_registry(e: &Env) -> bool { + let key = DataKey::Registry; + e.storage().instance().has(&key) +} + +pub fn read_registry(e: &Env) -> Address { + let key = DataKey::Registry; + e.storage().instance().get(&key).unwrap() +} + +pub fn write_registry(e: &Env, id: &Address) { + let key = DataKey::Registry; + e.storage().instance().set(&key, id); +} + +pub fn write_token_data(env: &Env, token_address: Address, data: TokenData) { + let key = DataKey::TokenData(token_address); + env.storage().persistent().set(&key, &data); +} + +pub fn read_token_data(env: &Env, token_address: Address) -> TokenData { + let default = TokenData{percentage: 0, period: 0, last_update: 0, current_limit: 0 }; + let key = DataKey::TokenData(token_address); + env.storage().persistent().get(&key).unwrap_or(default) +} + +pub fn write_tokens(e: &Env, token: Address) { + let key = DataKey::Tokens; + let mut tokens: Vec
= match e.storage().persistent().get(&key) { + Some(names) => names, + None => Vec::new(&e), + }; + + tokens.push_back(token); + e.storage().persistent().set(&key, &tokens); +} + +pub fn read_tokens(e: &Env) -> Vec
{ + let key = DataKey::Tokens; + let tokens: Vec
= match e.storage().persistent().get(&key) { + Some(names) => names, + None => Vec::new(&e), + }; + + tokens +} + +pub fn extent_ttl(e: &Env) { + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + let tokens = read_tokens(&e); + e.storage().persistent().extend_ttl( + &DataKey::Tokens, + INSTANCE_LIFETIME_THRESHOLD, + INSTANCE_BUMP_AMOUNT, + ); + for token in tokens { + + e.storage().persistent().extend_ttl( + &DataKey::TokenData(token.clone()), + INSTANCE_LIFETIME_THRESHOLD, + INSTANCE_BUMP_AMOUNT, + ); + + } +} diff --git a/contracts/asset_manager/src/storage_types.rs b/contracts/asset_manager/src/storage_types.rs new file mode 100644 index 0000000..2e1997b --- /dev/null +++ b/contracts/asset_manager/src/storage_types.rs @@ -0,0 +1,22 @@ +use soroban_sdk::{contracttype, Address}; + +pub(crate) const POINTS: u128 = 10000; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Registry, + Admin, + Config, + Tokens, + TokenData(Address) +} + +#[derive(Clone)] +#[contracttype] +pub struct TokenData { + pub period: u64, + pub percentage: u32, + pub last_update: u64, + pub current_limit: u64, +} diff --git a/contracts/asset_manager/src/tests/asset_manager_test.rs b/contracts/asset_manager/src/tests/asset_manager_test.rs new file mode 100644 index 0000000..a30949a --- /dev/null +++ b/contracts/asset_manager/src/tests/asset_manager_test.rs @@ -0,0 +1,565 @@ +#![cfg(test)] +extern crate std; + +use crate::{config, contract::AssetManagerClient, storage_types::DataKey}; +use soroban_sdk::{ + testutils::{storage::Persistent, Address as _, AuthorizedFunction, AuthorizedInvocation}, + token, Address, Bytes, IntoVal, String, Symbol, Vec, +}; + +use soroban_rlp::balanced::messages::{deposit_revert::DepositRevert, withdraw_to::WithdrawTo}; + +use super::setup::*; + +#[test] +fn test_initialize() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + + ctx.init_context(&client); + + let registry_exists = client.has_registry(); + assert_eq!(registry_exists, true) +} + +#[test] +fn test_set_admin() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + let new_admin: Address = Address::generate(&ctx.env); + client.set_admin(&new_admin); + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.registry.clone(), + Symbol::new(&ctx.env, "set_admin"), + (&new_admin,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(client.get_admin(), new_admin); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #4)")] +fn test_configure_rate_limit_panic() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + let period = &300; + let percentage = &10001; + client.configure_rate_limit(&ctx.token, period, percentage); + + let limit = client.get_withdraw_limit(&ctx.token); + let verified = client.verify_withdraw(&ctx.token, &limit); + assert_eq!(verified, true); +} + +#[test] +fn test_configure_rate_limit() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + let period = &300; + let percentage = &300; + client.configure_rate_limit(&ctx.token, period, percentage); + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.registry.clone(), + Symbol::new(&ctx.env, "configure_rate_limit"), + (&ctx.token, 300u64, 300u32).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + let token_data = client.get_rate_limit(&ctx.token); + assert_eq!(token_data.3, 0); + let limit = client.get_withdraw_limit(&ctx.token); + let verified = client.verify_withdraw(&ctx.token, &limit); + assert_eq!(verified, true); +} + +#[test] +fn test_deposit_without_to_and_data() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + client.configure_rate_limit(&ctx.token, &300, &300); + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + let amount_i128: i128 = 100000i128; + let amount = &(amount_i128 as u128); + let mint_amount = &(amount_i128 + amount_i128); + + stellar_asset_client.mint(&ctx.depositor, mint_amount); + + ctx.mint_native_token(&ctx.depositor, 500); + assert_eq!(ctx.get_native_token_balance(&ctx.depositor), 500); + + token_client.approve( + &ctx.depositor, + &ctx.registry, + &(amount_i128 + amount_i128), + &1312000, + ); + client.deposit( + &ctx.depositor, + &ctx.token, + &amount, + &Option::Some(String::from_str(&ctx.env, "")), + &Option::Some(Bytes::from_array(&ctx.env, &[0u8; 32])), + ); + + assert_eq!(ctx.get_native_token_balance(&ctx.depositor), 400); // why 300? +} + +#[test] +fn test_veryfy_rate_limit() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + let period = &300; + let percentage = &300; + client.configure_rate_limit(&ctx.token, period, percentage); + + //let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + let amount_i128: i128 = 100000i128; + let amount = &(amount_i128 as u128); + let mint_amount: &i128 = &(amount_i128 + amount_i128); + + stellar_asset_client.mint(&ctx.depositor, mint_amount); + + ctx.mint_native_token(&ctx.depositor, 500u128); + assert_eq!(ctx.get_native_token_balance(&ctx.depositor), 500u128); + + //token_client.approve(&ctx.depositor, &ctx.registry, &(amount_i128+amount_i128), &1312000); + client.deposit( + &ctx.depositor, + &ctx.token, + &amount, + &Option::Some(String::from_str(&ctx.env, "")), + &Option::Some(Bytes::from_array(&ctx.env, &[0u8; 32])), + ); + + let limit = client.get_withdraw_limit(&ctx.token); + assert_eq!(limit, 3000); + let verified = client.verify_withdraw(&ctx.token, &(amount - 3000 - 1)); + assert_eq!(verified, true); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn test_veryfy_rate_limit_panic_exceeds_withdraw_limit() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + let period = &300; + let percentage = &300; + client.configure_rate_limit(&ctx.token, period, percentage); + + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + let amount_i128: i128 = 100000i128; + let amount = &(amount_i128 as u128); + let mint_amount = &(amount_i128 + amount_i128); + + stellar_asset_client.mint(&ctx.depositor, mint_amount); + + ctx.mint_native_token(&ctx.depositor, 500u128); + assert_eq!(ctx.get_native_token_balance(&ctx.depositor), 500u128); + + token_client.approve( + &ctx.depositor, + &ctx.registry, + &(amount_i128 + amount_i128), + &1312000, + ); + client.deposit( + &ctx.depositor, + &ctx.token, + &amount, + &Option::Some(String::from_str(&ctx.env, "")), + &Option::Some(Bytes::from_array(&ctx.env, &[0u8; 32])), + ); + + let limit = client.get_withdraw_limit(&ctx.token); + assert_eq!(limit, 3000); + let verified = client.verify_withdraw(&ctx.token, &(amount - 3000 + 1)); + assert_eq!(verified, true); +} + +#[test] +fn test_deposit_with_to_and_without_data() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + client.configure_rate_limit(&ctx.token, &300, &300); + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + let amount_i128: i128 = 100000i128; + let amount = &(amount_i128 as u128); + let mint_amount = &(amount_i128 + amount_i128); + + stellar_asset_client.mint(&ctx.depositor, mint_amount); + + ctx.mint_native_token(&ctx.depositor, 500); + assert_eq!(ctx.get_native_token_balance(&ctx.depositor), 500); + + token_client.approve( + &ctx.depositor, + &ctx.registry, + &(amount_i128 + amount_i128), + &1312000, + ); + client.deposit( + &ctx.depositor, + &ctx.token, + &amount, + &Option::Some(String::from_str(&ctx.env, "icon01/hxjkdvhui")), + &Option::Some(Bytes::from_array(&ctx.env, &[0u8; 32])), + ); + + assert_eq!(ctx.get_native_token_balance(&ctx.depositor), 400) // why 300? +} + +#[test] +fn test_deposit_with_to_and_data() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + client.configure_rate_limit(&ctx.token, &300, &300); + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + let amount_i128: i128 = 100000i128; + let amount = &(amount_i128 as u128); + let mint_amount = &(amount_i128 + amount_i128); + + stellar_asset_client.mint(&ctx.depositor, mint_amount); + + ctx.mint_native_token(&ctx.depositor, 500); + assert_eq!(ctx.get_native_token_balance(&ctx.depositor), 500); + + token_client.approve( + &ctx.depositor, + &ctx.registry, + &(amount_i128 + amount_i128), + &1312000, + ); + + let data: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20, + ]; + client.deposit( + &ctx.depositor, + &ctx.token, + &amount, + &Option::Some(String::from_str(&ctx.env, "icon01/hxjkdvhui")), + &Option::Some(Bytes::from_array(&ctx.env, &data)), + ); + assert_eq!(ctx.get_native_token_balance(&ctx.depositor), 400); // why 300? +} + +#[test] +fn test_handle_call_message_for_withdraw_to() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + //client.configure_rate_limit(&ctx.token, &300, &300); + + let bnusd_amount = 100000u128; + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + stellar_asset_client.mint(&ctx.registry, &((bnusd_amount * 2) as i128)); + + let data = WithdrawTo::new( + ctx.token.to_string(), + ctx.withdrawer.to_string(), + bnusd_amount, + ) + .encode(&ctx.env, String::from_str(&ctx.env, "WithdrawTo")); + let decoded = WithdrawTo::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, ctx.withdrawer.to_string()); + + assert_eq!(token_client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_asset_manager, &data, &sources); + + assert_eq!(token_client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #13)")] +fn test_handle_call_message_for_withdraw_to_invalid_address() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + client.configure_rate_limit(&ctx.token, &300, &300); + + let bnusd_amount = 100000u128; + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + stellar_asset_client.mint(&ctx.registry, &((bnusd_amount * 2) as i128)); + + let data = WithdrawTo::new( + ctx.token.to_string(), + String::from_str(&ctx.env, "InvalidAddress"), + bnusd_amount, + ) + .encode(&ctx.env, String::from_str(&ctx.env, "WithdrawTo")); + let decoded = WithdrawTo::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, String::from_str(&ctx.env, "InvalidAddress")); + + assert_eq!(token_client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_asset_manager, &data, &sources); + + assert_eq!(token_client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #7)")] +fn test_handle_call_message_for_withdraw_to_panic_with_protocal_mismatch() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + client.configure_rate_limit(&ctx.token, &300, &300); + + let bnusd_amount = 100000u128; + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + stellar_asset_client.mint(&ctx.registry, &((bnusd_amount * 2) as i128)); + + let data = WithdrawTo::new( + ctx.token.to_string(), + ctx.withdrawer.to_string(), + bnusd_amount, + ) + .encode(&ctx.env, String::from_str(&ctx.env, "WithdrawTo")); + let decoded = WithdrawTo::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, ctx.withdrawer.to_string()); + + assert_eq!(token_client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.xcall.to_string()]); + client.handle_call_message(&ctx.icon_asset_manager, &data, &sources); + + assert_eq!(token_client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #8)")] +fn test_handle_call_message_for_withdraw_to_panic_with_not_icon_asset_manager() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + client.configure_rate_limit(&ctx.token, &300, &300); + + let bnusd_amount = 100000u128; + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + stellar_asset_client.mint(&ctx.registry, &((bnusd_amount * 2) as i128)); + + let data = WithdrawTo::new( + ctx.token.to_string(), + ctx.withdrawer.to_string(), + bnusd_amount, + ) + .encode(&ctx.env, String::from_str(&ctx.env, "WithdrawTo")); + let decoded = WithdrawTo::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, ctx.withdrawer.to_string()); + + assert_eq!(token_client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.centralized_connection.to_string(), &data, &sources); + + assert_eq!(token_client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #10)")] +fn test_handle_call_message_for_withdraw_to_panic_with_unknown_message_type() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + client.configure_rate_limit(&ctx.token, &300, &300); + + let bnusd_amount = 100000u128; + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + stellar_asset_client.mint(&ctx.registry, &((bnusd_amount * 2) as i128)); + + let data = WithdrawTo::new( + ctx.token.to_string(), + ctx.withdrawer.to_string(), + bnusd_amount, + ) + .encode(&ctx.env, String::from_str(&ctx.env, "WithdrawToUnknown")); + let decoded = WithdrawTo::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, ctx.withdrawer.to_string()); + + assert_eq!(token_client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_asset_manager, &data, &sources); + + assert_eq!(token_client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +fn test_handle_call_message_for_deposit_rollback() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + client.configure_rate_limit(&ctx.token, &300, &300); + + let bnusd_amount = 100000u128; + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + stellar_asset_client.mint(&ctx.registry, &((bnusd_amount * 2) as i128)); + + let data = DepositRevert::new(ctx.token, ctx.withdrawer.clone(), bnusd_amount) + .encode(&ctx.env, String::from_str(&ctx.env, "DepositRevert")); + let decoded = DepositRevert::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, ctx.withdrawer); + + assert_eq!(token_client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.xcall_client.get_network_address(), &data, &sources); + + assert_eq!(token_client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #9)")] +fn test_handle_call_message_for_deposit_rollback_panic_with_only_call_service() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + client.configure_rate_limit(&ctx.token, &300, &300); + + let bnusd_amount = 100000u128; + let token_client = token::Client::new(&ctx.env, &ctx.token); + let stellar_asset_client: token::StellarAssetClient = + token::StellarAssetClient::new(&ctx.env, &ctx.token); + stellar_asset_client.mint(&ctx.registry, &((bnusd_amount * 2) as i128)); + + let data = DepositRevert::new(ctx.token, ctx.withdrawer.clone(), bnusd_amount) + .encode(&ctx.env, String::from_str(&ctx.env, "DepositRevert")); + let decoded = DepositRevert::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, ctx.withdrawer); + + assert_eq!(token_client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + let wrong_network_address: String = String::from_str( + &ctx.env, + &std::format!( + "{}/{}", + "soroban", + "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL" + ), + ); + + std::println!( + "{}", + std::string::ToString::to_string(&wrong_network_address) + ); + client.handle_call_message(&wrong_network_address, &data, &sources); + + assert_eq!(token_client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +fn test_extend_ttl() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + client.configure_rate_limit(&ctx.token, &300, &300); + let token = ctx.token; + + client.extend_ttl(); + + ctx.env.as_contract(&client.address, || { + let key = DataKey::TokenData(token.clone()); + let before_ttl = ctx.env.storage().persistent().get_ttl(&key); + std::println!("before ttl is: {:?}", before_ttl); + }); +} + +#[test] +fn test_set_upgrade_authority() { + let ctx = TestContext::default(); + let client = AssetManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + let new_upgrade_authority = Address::generate(&ctx.env); + client.set_upgrade_authority(&new_upgrade_authority); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.upgrade_authority.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.registry.clone(), + Symbol::new(&ctx.env, "set_upgrade_authority"), + (&new_upgrade_authority,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + + ctx.env.as_contract(&client.address, || { + let config = config::get_config(&ctx.env); + assert_eq!(config.upgrade_authority, new_upgrade_authority) + }); +} diff --git a/contracts/asset_manager/src/tests/mod.rs b/contracts/asset_manager/src/tests/mod.rs new file mode 100644 index 0000000..06a7192 --- /dev/null +++ b/contracts/asset_manager/src/tests/mod.rs @@ -0,0 +1,2 @@ +pub mod setup; +pub mod asset_manager_test; \ No newline at end of file diff --git a/contracts/asset_manager/src/tests/setup.rs b/contracts/asset_manager/src/tests/setup.rs new file mode 100644 index 0000000..937231b --- /dev/null +++ b/contracts/asset_manager/src/tests/setup.rs @@ -0,0 +1,149 @@ +#![cfg(test)] +extern crate std; + +use crate::contract::{AssetManager, AssetManagerClient}; + +use crate::config::ConfigData; + +use soroban_sdk::Vec; +use soroban_sdk::{testutils::Address as _, token, Address, Env, String}; + +mod xcall { + soroban_sdk::contractimport!(file = "../../wasm/xcall.wasm"); +} + +mod connection { + soroban_sdk::contractimport!(file = "../../wasm/centralized_connection.wasm"); +} + +mod xcall_manager { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/xcall_manager.wasm" + ); +} + +use xcall_manager::ConfigData as XcallManagerConfigData; + +pub struct TestContext { + pub env: Env, + pub registry: Address, + pub admin: Address, + pub depositor: Address, + pub withdrawer: Address, + pub upgrade_authority: Address, + pub xcall: Address, + pub xcall_manager: Address, + pub icon_asset_manager: String, + pub icon_governance: String, + pub token: Address, + pub centralized_connection: Address, + pub nid: String, + pub native_token: Address, + pub xcall_client: xcall::Client<'static>, +} + +impl TestContext { + pub fn default() -> Self { + let env = Env::default(); + let token_admin = Address::generate(&env); + let token = env.register_stellar_asset_contract_v2(token_admin.clone()); + let asset_manager = env.register_contract(None, AssetManager); + let centralized_connection = env.register_contract_wasm(None, connection::WASM); + let xcall_manager = env.register_contract_wasm(None, xcall_manager::WASM); + let xcall = env.register_contract_wasm(None, xcall::WASM); + + Self { + registry: asset_manager, + admin: Address::generate(&env), + depositor: Address::generate(&env), + withdrawer: Address::generate(&env), + upgrade_authority: Address::generate(&env), + xcall: xcall.clone(), + xcall_manager: xcall_manager, + icon_asset_manager: String::from_str(&env, "icon01/hxjnfh4u"), + icon_governance: String::from_str(&env, "icon01/kjdnoi"), + token: token.address(), + centralized_connection: centralized_connection, + nid: String::from_str(&env, "stellar"), + native_token: env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(), + xcall_client: xcall::Client::new(&env, &xcall), + env, + } + } + + pub fn init_context(&self, client: &AssetManagerClient<'static>) { + self.env.mock_all_auths(); + self.init_xcall_manager_context(); + self.init_xcall_state(); + let config = ConfigData { + xcall: self.xcall.clone(), + xcall_manager: self.xcall_manager.clone(), + native_address: self.native_token.clone(), + icon_asset_manager: self.icon_asset_manager.clone(), + xcall_network_address: self.xcall_client.get_network_address(), + upgrade_authority: self.upgrade_authority.clone(), + }; + client.initialize(&self.registry, &self.admin, &config); + } + + pub fn init_xcall_manager_context(&self) { + let client = self::xcall_manager::Client::new(&self.env, &self.xcall_manager); + let config = XcallManagerConfigData { + xcall: self.xcall.clone(), + icon_governance: self.icon_governance.clone(), + upgrade_authority: self.upgrade_authority.clone(), + }; + let sources = Vec::from_array(&self.env, [self.centralized_connection.to_string()]); + let destinations = + Vec::from_array(&self.env, [String::from_str(&self.env, "icon/address")]); + client.initialize( + &self.xcall_manager, + &self.admin, + &config, + &sources, + &destinations, + ); + } + + pub fn init_xcall_state(&self) { + self.xcall_client.initialize(&xcall::InitializeMsg { + sender: self.admin.clone(), + network_id: self.nid.clone(), + native_token: self.native_token.clone(), + }); + + self.init_connection_state(); + self.xcall_client.set_protocol_fee(&100); + self.xcall_client + .set_default_connection(&self.nid, &self.centralized_connection); + } + + pub fn init_connection_state(&self) { + let connection_client = connection::Client::new(&self.env, &self.centralized_connection); + + let initialize_msg = connection::InitializeMsg { + native_token: self.native_token.clone(), + relayer: self.admin.clone(), + xcall_address: self.xcall.clone(), + }; + connection_client.initialize(&initialize_msg); + + let message_fee = 100; + let response_fee = 100; + connection_client.set_fee(&self.nid, &message_fee, &response_fee); + } + + pub fn mint_native_token(&self, address: &Address, amount: u128) { + let native_token_client = token::StellarAssetClient::new(&self.env, &self.native_token); + native_token_client.mint(&address, &(*&amount as i128)); + } + + pub fn get_native_token_balance(&self, address: &Address) -> u128 { + let native_token_client = token::TokenClient::new(&self.env, &self.native_token); + let balance = native_token_client.balance(address); + + *&balance as u128 + } +} diff --git a/contracts/asset_manager/src/xcall_manager_interface.rs b/contracts/asset_manager/src/xcall_manager_interface.rs new file mode 100644 index 0000000..17c7698 --- /dev/null +++ b/contracts/asset_manager/src/xcall_manager_interface.rs @@ -0,0 +1,15 @@ +use soroban_sdk::{contractclient, Env, String, Vec}; + +use crate::errors::ContractError; + +#[contractclient(name = "XcallManagerClient")] +pub trait XcallManagerInterface { + + fn verify_protocols( + e: Env, + protocols: Vec + ) -> Result; + + fn get_protocols(e: Env) -> Result<(Vec, Vec), ContractError>; + +} \ No newline at end of file diff --git a/contracts/balanced_doller/Cargo.toml b/contracts/balanced_doller/Cargo.toml new file mode 100644 index 0000000..efa6b9f --- /dev/null +++ b/contracts/balanced_doller/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "balanced-dollar" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +testutils = ["soroban-sdk/testutils"] + +[dependencies] +soroban-sdk = { workspace = true } +soroban-token-sdk = { version = "21.6.0" } +soroban-rlp = { path = "../../libs/soroban-rlp" } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/contracts/balanced_doller/src/allowance.rs b/contracts/balanced_doller/src/allowance.rs new file mode 100644 index 0000000..ad74687 --- /dev/null +++ b/contracts/balanced_doller/src/allowance.rs @@ -0,0 +1,63 @@ +use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; +use soroban_sdk::{Address, Env}; + +pub fn read_allowance(e: &Env, from: Address, spender: Address) -> AllowanceValue { + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + if let Some(allowance) = e.storage().temporary().get::<_, AllowanceValue>(&key) { + if allowance.expiration_ledger < e.ledger().sequence() { + AllowanceValue { + amount: 0, + expiration_ledger: allowance.expiration_ledger, + } + } else { + allowance + } + } else { + AllowanceValue { + amount: 0, + expiration_ledger: 0, + } + } +} + +pub fn write_allowance( + e: &Env, + from: Address, + spender: Address, + amount: i128, + expiration_ledger: u32, +) { + let allowance = AllowanceValue { + amount, + expiration_ledger, + }; + + if amount > 0 && expiration_ledger < e.ledger().sequence() { + panic!("expiration_ledger is less than ledger seq when amount > 0") + } + + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + e.storage().temporary().set(&key.clone(), &allowance); + + if amount > 0 { + let live_for = expiration_ledger + .checked_sub(e.ledger().sequence()) + .unwrap(); + + e.storage().temporary().extend_ttl(&key, live_for, live_for) + } +} + +pub fn spend_allowance(e: &Env, from: Address, spender: Address, amount: i128) { + let allowance = read_allowance(e, from.clone(), spender.clone()); + if allowance.amount < amount { + panic!("insufficient allowance"); + } + write_allowance( + e, + from, + spender, + allowance.amount - amount, + allowance.expiration_ledger, + ); +} diff --git a/contracts/balanced_doller/src/balance.rs b/contracts/balanced_doller/src/balance.rs new file mode 100644 index 0000000..66184d4 --- /dev/null +++ b/contracts/balanced_doller/src/balance.rs @@ -0,0 +1,35 @@ +use crate::storage_types::{DataKey, BALANCE_BUMP_AMOUNT, BALANCE_LIFETIME_THRESHOLD}; +use soroban_sdk::{Address, Env}; + +pub fn read_balance(e: &Env, addr: Address) -> i128 { + let key = DataKey::Balance(addr); + if let Some(balance) = e.storage().persistent().get::(&key) { + e.storage() + .persistent() + .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); + balance + } else { + 0 + } +} + +fn write_balance(e: &Env, addr: Address, amount: i128) { + let key = DataKey::Balance(addr); + e.storage().persistent().set(&key, &amount); + e.storage() + .persistent() + .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); +} + +pub fn receive_balance(e: &Env, addr: Address, amount: i128) { + let balance = read_balance(e, addr.clone()); + write_balance(e, addr, balance + amount); +} + +pub fn spend_balance(e: &Env, addr: Address, amount: i128) { + let balance = read_balance(e, addr.clone()); + if balance < amount { + panic!("insufficient balance"); + } + write_balance(e, addr, balance - amount); +} \ No newline at end of file diff --git a/contracts/balanced_doller/src/balanced_dollar.rs b/contracts/balanced_doller/src/balanced_dollar.rs new file mode 100644 index 0000000..6f7df89 --- /dev/null +++ b/contracts/balanced_doller/src/balanced_dollar.rs @@ -0,0 +1,160 @@ +use crate::balance::{receive_balance, spend_balance}; +use soroban_sdk::{xdr::ToXdr, Address, Bytes, Env, String, Vec}; +mod xcall { + soroban_sdk::contractimport!(file = "../../wasm/xcall.wasm"); +} + +use crate::contract; +use crate::errors::ContractError; +use crate::states::read_administrator; +use crate::{ + config::{get_config, set_config, ConfigData}, + xcall_manager_interface::XcallManagerClient, +}; +use soroban_rlp::balanced::address_utils::{get_address_from, is_valid_bytes_address}; +use soroban_rlp::balanced::messages::{ + cross_transfer::CrossTransfer, cross_transfer_revert::CrossTransferRevert, +}; +use soroban_token_sdk::TokenUtils; +use xcall::{AnyMessage, CallMessageWithRollback, Client, Envelope}; +const CROSS_TRANSFER: &str = "xCrossTransfer"; +const CROSS_TRANSFER_REVERT: &str = "xCrossTransferRevert"; + +pub fn configure(env: Env, config: ConfigData) { + set_config(&env, config); +} + +pub fn _cross_transfer( + e: Env, + from: Address, + amount: u128, + to: String, + data: Bytes, +) -> Result<(), ContractError> { + _burn(&e, from.clone(), amount as i128); + let xcall_message = CrossTransfer::new(from.clone().to_string(), to, amount, data); + let rollback = CrossTransferRevert::new(from.clone(), amount); + let config = get_config(&e); + let icon_bn_usd = config.icon_bn_usd; + + let rollback_bytes = rollback.encode(&e, String::from_str(&e, CROSS_TRANSFER_REVERT)); + let message_bytes = xcall_message.encode(&e, String::from_str(&e, CROSS_TRANSFER)); + + let (sources, destinations) = xcall_manager_client(&e, &config.xcall_manager).get_protocols(); + + let message = AnyMessage::CallMessageWithRollback(CallMessageWithRollback { + data: message_bytes, + rollback: rollback_bytes, + }); + let envelope: &Envelope = &Envelope { + message, + sources, + destinations, + }; + + let current_address = e.current_contract_address(); + xcall_client(&e, &config.xcall).send_call(&from, ¤t_address, envelope, &icon_bn_usd); + Ok(()) +} + +fn verify_protocol( + e: &Env, + xcall_manager: &Address, + protocols: Vec, +) -> Result<(), ContractError> { + let verified: bool = xcall_manager_client(e, xcall_manager).verify_protocols(&protocols); + if !verified { + return Err(ContractError::ProtocolMismatch); + } + Ok(()) +} + +pub fn _handle_call_message( + e: Env, + from: String, + data: Bytes, + protocols: Vec, +) -> Result<(), ContractError> { + let config: ConfigData = get_config(&e); + let xcall = config.xcall; + xcall.require_auth(); + + let method = CrossTransfer::get_method(&e, data.clone()); + let icon_bn_usd: String = config.icon_bn_usd; + if method == String::from_str(&e, &CROSS_TRANSFER) { + if from != icon_bn_usd { + return Err(ContractError::OnlyIconBnUSD); + } + let message = CrossTransfer::decode(&e, data); + let to_network_address: Address = get_address(message.to, &e)?; + _mint(&e, to_network_address, message.amount as i128); + } else if method == String::from_str(&e, &CROSS_TRANSFER_REVERT) { + if config.xcall_network_address != from { + return Err(ContractError::OnlyCallService); + } + let message = CrossTransferRevert::decode(&e, data); + _mint(&e, message.to, message.amount as i128); + } else { + return Err(ContractError::UnknownMessageType); + } + verify_protocol(&e, &config.xcall_manager, protocols)?; + Ok(()) +} + +pub fn get_address(network_address: String, env: &Env) -> Result { + let bytes = network_address.to_xdr(&env); + + if bytes.get(6).unwrap() > 0 { + return Err(ContractError::InvalidNetworkAddressLength); + } + + let value_len = bytes.get(7).unwrap(); + let slice = bytes.slice(8..value_len as u32 + 8); + let mut nid = Bytes::new(&env); + let mut account = Bytes::new(&env); + + let mut has_seperator = false; + for (index, value) in slice.clone().iter().enumerate() { + if has_seperator { + account.append(&slice.slice(index as u32..slice.len())); + break; + } else if value == 47 { + has_seperator = true; + } else { + nid.push_back(value) + } + } + + if !has_seperator { + return Err(ContractError::InvalidNetworkAddress); + } + + if !is_valid_bytes_address(&account) { + return Err(ContractError::InvalidAddress); + } + Ok(Address::from_string_bytes(&account)) +} + +fn _mint(e: &Env, to: Address, amount: i128) { + contract::check_nonnegative_amount(amount); + let admin: Address = read_administrator(&e); + + receive_balance(e, to.clone(), amount); + TokenUtils::new(e).events().mint(admin, to, amount); +} + +pub fn _burn(e: &Env, from: Address, amount: i128) { + contract::check_nonnegative_amount(amount); + + spend_balance(e, from.clone(), amount); + TokenUtils::new(e).events().burn(from, amount); +} + +fn xcall_client(e: &Env, xcall: &Address) -> Client<'static> { + return xcall::Client::new(e, xcall); +} + +fn xcall_manager_client(e: &Env, xcall_manager: &Address) -> XcallManagerClient<'static> { + let client = XcallManagerClient::new(e, xcall_manager); + return client; +} diff --git a/contracts/balanced_doller/src/config.rs b/contracts/balanced_doller/src/config.rs new file mode 100644 index 0000000..edb5190 --- /dev/null +++ b/contracts/balanced_doller/src/config.rs @@ -0,0 +1,22 @@ +use crate::storage_types::DataKey; +use soroban_sdk::{contracttype, unwrap::UnwrapOptimized, Address, Env, String}; + +#[derive(Clone)] +#[contracttype] +pub struct ConfigData { + pub xcall: Address, + pub xcall_manager: Address, + pub nid: String, + pub icon_bn_usd: String, + pub xcall_network_address: String, + pub upgrade_authority: Address, +} + +pub fn set_config(e: &Env, config: ConfigData) { + e.storage().instance().set(&DataKey::Config, &config); +} + +pub fn get_config(e: &Env) -> ConfigData { + let key = DataKey::Config; + e.storage().instance().get(&key).unwrap_optimized() +} diff --git a/contracts/balanced_doller/src/contract.rs b/contracts/balanced_doller/src/contract.rs new file mode 100644 index 0000000..8802007 --- /dev/null +++ b/contracts/balanced_doller/src/contract.rs @@ -0,0 +1,162 @@ +//! This contract demonstrates a sample implementation of the Soroban token +//! interface. +use crate::allowance::{read_allowance, spend_allowance, write_allowance}; +use crate::balance::{read_balance, receive_balance, spend_balance}; +use crate::balanced_dollar; +use crate::config::{self, ConfigData}; +use crate::errors::ContractError; +use crate::metadata::{read_decimal, read_name, read_symbol, write_metadata}; +use crate::states::{has_administrator, read_administrator, write_administrator}; +use crate::storage_types::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; +use soroban_sdk::{ + contract, contractimpl, panic_with_error, Address, Bytes, BytesN, Env, String, Vec, +}; +use soroban_token_sdk::metadata::TokenMetadata; +use soroban_token_sdk::TokenUtils; +pub fn check_nonnegative_amount(amount: i128) { + if amount < 0 { + panic!("negative amount is not allowed: {}", amount) + } +} + +#[contract] +pub struct BalancedDollar; + +#[contractimpl] +impl BalancedDollar { + pub fn initialize(e: Env, admin: Address, config: ConfigData) { + if has_administrator(&e) { + panic_with_error!(e, ContractError::ContractAlreadyInitialized) + } + write_administrator(&e, &admin); + + //initialize token properties + let decimal = 18; + let name = String::from_str(&e, "Balanced Dollar"); + let symbol = String::from_str(&e, "bnUSD"); + + if decimal > u8::MAX.into() { + panic_with_error!(e, ContractError::DecimalMustFitInAu8) + } + + write_metadata( + &e, + TokenMetadata { + decimal, + name, + symbol, + }, + ); + balanced_dollar::configure(e, config); + } + + pub fn set_admin(e: Env, new_admin: Address) { + let admin = read_administrator(&e); + admin.require_auth(); + + write_administrator(&e, &new_admin); + TokenUtils::new(&e).events().set_admin(admin, new_admin); + } + + pub fn get_admin(e: Env) -> Address { + read_administrator(&e) + } + + pub fn cross_transfer( + e: Env, + from: Address, + amount: u128, + to: String, + data: Option, + ) -> Result<(), ContractError> { + from.require_auth(); + let transfer_data = data.unwrap_or(Bytes::from_array(&e, &[0u8; 32])); + return balanced_dollar::_cross_transfer(e.clone(), from, amount, to, transfer_data); + } + + pub fn handle_call_message( + e: Env, + from: String, + data: Bytes, + protocols: Vec, + ) -> Result<(), ContractError> { + return balanced_dollar::_handle_call_message(e, from, data, protocols); + } + + pub fn is_initialized(e: Env) -> bool { + has_administrator(&e) + } + + pub fn set_upgrade_authority(e: Env, upgrade_authority: Address) { + let mut config = config::get_config(&e); + + config.upgrade_authority.require_auth(); + + config.upgrade_authority = upgrade_authority; + config::set_config(&e, config); + } + + pub fn upgrade(e: Env, new_wasm_hash: BytesN<32>) { + let config = config::get_config(&e); + config.upgrade_authority.require_auth(); + + e.deployer().update_current_contract_wasm(new_wasm_hash); + } + + pub fn extend_ttl(e: Env) { + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + } + + pub fn allowance(e: Env, from: Address, spender: Address) -> i128 { + read_allowance(&e, from, spender).amount + } + + pub fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32) { + from.require_auth(); + + check_nonnegative_amount(amount); + + write_allowance(&e, from.clone(), spender.clone(), amount, expiration_ledger); + TokenUtils::new(&e) + .events() + .approve(from, spender, amount, expiration_ledger); + } + + pub fn balance(e: Env, id: Address) -> i128 { + read_balance(&e, id) + } + + pub fn transfer(e: Env, from: Address, to: Address, amount: i128) { + from.require_auth(); + + check_nonnegative_amount(amount); + spend_balance(&e, from.clone(), amount); + receive_balance(&e, to.clone(), amount); + TokenUtils::new(&e).events().transfer(from, to, amount); + } + + pub fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) { + spender.require_auth(); + + check_nonnegative_amount(amount); + + spend_allowance(&e, from.clone(), spender, amount); + spend_balance(&e, from.clone(), amount); + receive_balance(&e, to.clone(), amount); + TokenUtils::new(&e).events().transfer(from, to, amount) + } + + pub fn decimals(e: Env) -> u32 { + read_decimal(&e) + } + + pub fn name(e: Env) -> String { + read_name(&e) + } + + pub fn symbol(e: Env) -> String { + read_symbol(&e) + } +} diff --git a/contracts/balanced_doller/src/errors.rs b/contracts/balanced_doller/src/errors.rs new file mode 100644 index 0000000..a1cc30b --- /dev/null +++ b/contracts/balanced_doller/src/errors.rs @@ -0,0 +1,17 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ContractError { + InvalidRlpLength = 1, + ContractAlreadyInitialized = 2, + DecimalMustFitInAu8 = 3, + ProtocolMismatch = 4, + OnlyIconBnUSD = 5, + OnlyCallService = 6, + UnknownMessageType = 7, + InvalidAddress = 8, + InvalidNetworkAddressLength = 9, + InvalidNetworkAddress = 10, +} diff --git a/contracts/balanced_doller/src/lib.rs b/contracts/balanced_doller/src/lib.rs new file mode 100644 index 0000000..7948e23 --- /dev/null +++ b/contracts/balanced_doller/src/lib.rs @@ -0,0 +1,13 @@ +#![no_std] + +mod allowance; +mod balance; +pub mod balanced_dollar; +mod config; +pub mod contract; +mod errors; +mod metadata; +mod states; +mod storage_types; +mod tests; +mod xcall_manager_interface; diff --git a/contracts/balanced_doller/src/metadata.rs b/contracts/balanced_doller/src/metadata.rs new file mode 100644 index 0000000..715feee --- /dev/null +++ b/contracts/balanced_doller/src/metadata.rs @@ -0,0 +1,22 @@ +use soroban_sdk::{Env, String}; +use soroban_token_sdk::{metadata::TokenMetadata, TokenUtils}; + +pub fn read_decimal(e: &Env) -> u32 { + let util = TokenUtils::new(e); + util.metadata().get_metadata().decimal +} + +pub fn read_name(e: &Env) -> String { + let util = TokenUtils::new(e); + util.metadata().get_metadata().name +} + +pub fn read_symbol(e: &Env) -> String { + let util = TokenUtils::new(e); + util.metadata().get_metadata().symbol +} + +pub fn write_metadata(e: &Env, metadata: TokenMetadata) { + let util = TokenUtils::new(e); + util.metadata().set_metadata(&metadata); +} diff --git a/contracts/balanced_doller/src/states.rs b/contracts/balanced_doller/src/states.rs new file mode 100644 index 0000000..a820bf0 --- /dev/null +++ b/contracts/balanced_doller/src/states.rs @@ -0,0 +1,18 @@ +use soroban_sdk::{Address, Env}; + +use crate::storage_types::DataKey; + +pub fn has_administrator(e: &Env) -> bool { + let key = DataKey::Admin; + e.storage().instance().has(&key) +} + +pub fn read_administrator(e: &Env) -> Address { + let key = DataKey::Admin; + e.storage().instance().get(&key).unwrap() +} + +pub fn write_administrator(e: &Env, id: &Address) { + let key = DataKey::Admin; + e.storage().instance().set(&key, id); +} diff --git a/contracts/balanced_doller/src/storage_types.rs b/contracts/balanced_doller/src/storage_types.rs new file mode 100644 index 0000000..069bbab --- /dev/null +++ b/contracts/balanced_doller/src/storage_types.rs @@ -0,0 +1,30 @@ +use soroban_sdk::{contracttype, Address}; + +pub(crate) const DAY_IN_LEDGERS: u32 = 17280; +pub(crate) const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; +pub(crate) const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +pub(crate) const BALANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; +pub(crate) const BALANCE_LIFETIME_THRESHOLD: u32 = BALANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +#[derive(Clone)] +#[contracttype] +pub struct AllowanceDataKey { + pub from: Address, + pub spender: Address, +} + +#[contracttype] +pub struct AllowanceValue { + pub amount: i128, + pub expiration_ledger: u32, +} + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Allowance(AllowanceDataKey), + Balance(Address), + Admin, + Config, +} diff --git a/contracts/balanced_doller/src/tests/balanced_dollar_test.rs b/contracts/balanced_doller/src/tests/balanced_dollar_test.rs new file mode 100644 index 0000000..0bc4a54 --- /dev/null +++ b/contracts/balanced_doller/src/tests/balanced_dollar_test.rs @@ -0,0 +1,404 @@ +#![cfg(test)] +extern crate std; + +use crate::{config, contract::BalancedDollarClient}; + +use super::setup::*; +use soroban_rlp::balanced::messages::{ + cross_transfer::CrossTransfer, cross_transfer_revert::CrossTransferRevert, +}; +use soroban_sdk::{ + symbol_short, + testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, + Address, Bytes, IntoVal, String, Symbol, Vec, +}; + +#[test] +fn test_initialize() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + + ctx.init_context(&client); + + let initialized = client.is_initialized(); + assert_eq!(initialized, true) +} + +#[test] +fn test_set_admin() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + let new_admin: Address = Address::generate(&ctx.env); + client.set_admin(&new_admin); + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.registry.clone(), + symbol_short!("set_admin"), + (&new_admin,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(client.get_admin(), new_admin); +} + +#[test] +fn test_cross_transfer_with_to_and_data() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + let amount_i128: i128 = 100000i128; + let amount = &(amount_i128 as u128); + + let bnusd_amount = 1000000u128; + + let items: [u8; 32] = [0; 32]; + let to = String::from_str( + &ctx.env, + "stellar/CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + ); + let from_address = &Address::from_string(&String::from_str( + &ctx.env, + "CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + )); + + std::println!("to address is: {:?}", to); + let data = CrossTransfer::new( + ctx.depositor.to_string(), + to.clone(), + bnusd_amount, + Bytes::from_array(&ctx.env, &items), + ) + .encode(&ctx.env, String::from_str(&ctx.env, "xCrossTransfer")); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_bn_usd, &data, &sources); + + ctx.mint_native_token(&from_address, 500u128); + assert_eq!(ctx.get_native_token_balance(&from_address), 500u128); + + client.approve( + &from_address, + &ctx.registry, + &(amount_i128 + amount_i128), + &1312000, + ); + let data: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20, + ]; + client.cross_transfer( + &from_address, + &amount, + &String::from_str(&ctx.env, "icon01/hxjkdvhui"), + &Option::Some(Bytes::from_array(&ctx.env, &data)), + ); + assert_eq!(ctx.get_native_token_balance(&from_address), 400u128) // why 300? +} + +#[test] +fn test_handle_call_message_for_cross_transfer() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + + let bnusd_amount = 100000u128; + + let items: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20, + ]; + let withdrawer = String::from_str( + &ctx.env, + "stellar/CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + ); + let data = CrossTransfer::new( + ctx.depositor.to_string(), + withdrawer.clone(), + bnusd_amount, + Bytes::from_array(&ctx.env, &items), + ) + .encode(&ctx.env, String::from_str(&ctx.env, "xCrossTransfer")); + let decoded = CrossTransfer::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, withdrawer); + + let withdrawer_address = &Address::from_string(&String::from_str( + &ctx.env, + "CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + )); + assert_eq!(client.balance(withdrawer_address), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_bn_usd, &data, &sources); + assert_eq!(client.balance(withdrawer_address), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #8)")] +fn test_handle_call_message_for_cross_transfer_invalid_addres_fail() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + + let bnusd_amount = 100000u128; + + let items: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20, + ]; + let withdrawer = String::from_str(&ctx.env, "stellar/InvalidAddress"); + let data = CrossTransfer::new( + ctx.depositor.to_string(), + withdrawer.clone(), + bnusd_amount, + Bytes::from_array(&ctx.env, &items), + ) + .encode(&ctx.env, String::from_str(&ctx.env, "xCrossTransfer")); + let decoded = CrossTransfer::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, withdrawer); + + // let withdrawer_address = &Address::from_string(&String::from_str(&ctx.env, "InvalidAddress")); + // assert_eq!(client.balance(withdrawer_address), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_bn_usd, &data, &sources); + // assert_eq!(client.balance(withdrawer_address), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #4)")] +fn test_handle_call_message_for_cross_transfer_panic_for_protocol_mismatch() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + + let bnusd_amount = 100000u128; + + let items: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20, + ]; + let withdrawer = String::from_str( + &ctx.env, + "stellar/CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + ); + let data = CrossTransfer::new( + ctx.depositor.to_string(), + withdrawer.clone(), + bnusd_amount, + Bytes::from_array(&ctx.env, &items), + ) + .encode(&ctx.env, String::from_str(&ctx.env, "xCrossTransfer")); + let decoded = CrossTransfer::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, withdrawer); + + let withdrawer_address = &Address::from_string(&String::from_str( + &ctx.env, + "CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + )); + assert_eq!(client.balance(withdrawer_address), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.xcall.to_string()]); + client.handle_call_message(&ctx.icon_bn_usd, &data, &sources); + + assert_eq!(client.balance(withdrawer_address), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn test_handle_call_message_for_cross_transfer_panic_for_icon_bnusd() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + + let bnusd_amount = 100000u128; + + let items: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20, + ]; + let withdrawer = String::from_str( + &ctx.env, + "stellar/CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + ); + let data = CrossTransfer::new( + ctx.depositor.to_string(), + withdrawer.clone(), + bnusd_amount, + Bytes::from_array(&ctx.env, &items), + ) + .encode(&ctx.env, String::from_str(&ctx.env, "xCrossTransfer")); + let decoded = CrossTransfer::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, withdrawer); + + let withdrawer_address = &Address::from_string(&String::from_str( + &ctx.env, + "CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + )); + assert_eq!(client.balance(withdrawer_address), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_governance, &data, &sources); + + assert_eq!(client.balance(withdrawer_address), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #7)")] +fn test_handle_call_message_for_cross_transfer_panic_for_wront_message_type() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + + let bnusd_amount = 100000u128; + + let items: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20, + ]; + let withdrawer = String::from_str( + &ctx.env, + "stellar/CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + ); + let data = CrossTransfer::new( + ctx.depositor.to_string(), + withdrawer.clone(), + bnusd_amount, + Bytes::from_array(&ctx.env, &items), + ) + .encode(&ctx.env, String::from_str(&ctx.env, "xCrossTransferPanic")); + + let decoded: CrossTransfer = CrossTransfer::decode(&ctx.env, data.clone()); + let withdrawer_address = &Address::from_string(&String::from_str( + &ctx.env, + "CA36FQITV33RO5SJFPTNLRQBD6ZNAEJG7F7J5KWCV4OP7SQHDMIZCT33", + )); + assert_eq!(decoded.to, withdrawer); + + assert_eq!(client.balance(withdrawer_address), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_bn_usd, &data, &sources); + + assert_eq!(client.balance(withdrawer_address), bnusd_amount as i128) +} + +#[test] +fn test_handle_call_message_for_cross_transfer_revert() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + + let bnusd_amount = 100000u128; + + let data = CrossTransferRevert::new(ctx.withdrawer.clone(), bnusd_amount) + .encode(&ctx.env, String::from_str(&ctx.env, "xCrossTransferRevert")); + let decoded = CrossTransferRevert::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, ctx.withdrawer); + + assert_eq!(client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.xcall_client.get_network_address(), &data, &sources); + + assert_eq!(client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_handle_call_message_for_cross_transfer_revert_panic_for_xcall() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + + let bnusd_amount = 100000u128; + + let data = CrossTransferRevert::new(ctx.withdrawer.clone(), bnusd_amount) + .encode(&ctx.env, String::from_str(&ctx.env, "xCrossTransferRevert")); + let decoded = CrossTransferRevert::decode(&ctx.env, data.clone()); + assert_eq!(decoded.to, ctx.withdrawer); + + assert_eq!(client.balance(&ctx.withdrawer), 0); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + let wrong_network_address: String = String::from_str( + &ctx.env, + &std::format!( + "{}/{}", + "soroban", + "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL" + ), + ); + client.handle_call_message(&wrong_network_address, &data, &sources); + + assert_eq!(client.balance(&ctx.withdrawer), bnusd_amount as i128) +} + +#[test] +fn test_extend_ttl() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + + ctx.init_context(&client); + + client.extend_ttl() +} + +#[test] +fn test_set_upgrade_authority() { + let ctx = TestContext::default(); + let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + let new_upgrade_authority = Address::generate(&ctx.env); + client.set_upgrade_authority(&new_upgrade_authority); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.upgrade_authority.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.registry.clone(), + Symbol::new(&ctx.env, "set_upgrade_authority"), + (&new_upgrade_authority,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + + ctx.env.as_contract(&client.address, || { + let config = config::get_config(&ctx.env); + assert_eq!(config.upgrade_authority, new_upgrade_authority) + }); +} diff --git a/contracts/balanced_doller/src/tests/mod.rs b/contracts/balanced_doller/src/tests/mod.rs new file mode 100644 index 0000000..7918a54 --- /dev/null +++ b/contracts/balanced_doller/src/tests/mod.rs @@ -0,0 +1,2 @@ +pub mod setup; +pub mod balanced_dollar_test; \ No newline at end of file diff --git a/contracts/balanced_doller/src/tests/setup.rs b/contracts/balanced_doller/src/tests/setup.rs new file mode 100644 index 0000000..2528889 --- /dev/null +++ b/contracts/balanced_doller/src/tests/setup.rs @@ -0,0 +1,155 @@ +#![cfg(test)] +extern crate std; + +use crate::contract::{BalancedDollar, BalancedDollarClient}; + +use crate::config::ConfigData; + +use soroban_sdk::{testutils::Address as _, token, Address, Env, String, Vec}; + +mod xcall { + soroban_sdk::contractimport!(file = "../../wasm/xcall.wasm"); +} + +mod connection { + soroban_sdk::contractimport!(file = "../../wasm/centralized_connection.wasm"); +} + +mod xcall_manager { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/xcall_manager.wasm" + ); +} + +use xcall_manager::ConfigData as XcallManagerConfigData; + +pub struct TestContext { + pub env: Env, + pub registry: Address, + pub admin: Address, + pub depositor: Address, + pub withdrawer: Address, + pub upgrade_authority: Address, + pub xcall: Address, + pub xcall_manager: Address, + pub icon_bn_usd: String, + pub icon_governance: String, + pub token: Address, + pub centralized_connection: Address, + pub nid: String, + pub native_token: Address, + pub xcall_client: xcall::Client<'static>, +} + +impl TestContext { + pub fn default() -> Self { + let env = Env::default(); + let token_admin = Address::generate(&env); + let token = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + let balanced_dollar = env.register_contract(None, BalancedDollar); + let centralized_connection = env.register_contract_wasm(None, connection::WASM); + let xcall_manager = env.register_contract_wasm(None, xcall_manager::WASM); + let xcall = env.register_contract_wasm(None, xcall::WASM); + std::println!("asset manager {:?}", balanced_dollar); + std::println!("xcall manager{:?}", xcall_manager); + std::println!("xcall {:?}", xcall); + std::println!("centralized {:?}", centralized_connection); + + Self { + registry: balanced_dollar, + admin: Address::generate(&env), + depositor: Address::generate(&env), + withdrawer: Address::generate(&env), + upgrade_authority: Address::generate(&env), + xcall: xcall.clone(), + xcall_manager, + icon_bn_usd: String::from_str(&env, "icon01/hxjnfh4u"), + icon_governance: String::from_str(&env, "icon01/kjdnoi"), + token, + centralized_connection, + nid: String::from_str(&env, "stellar"), + native_token: env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(), + xcall_client: xcall::Client::new(&env, &xcall), + env, + } + } + + pub fn init_context(&self, client: &BalancedDollarClient<'static>) { + self.env.mock_all_auths(); + self.init_xcall_manager_context(); + self.init_xcall_state(); + let config = ConfigData { + xcall: self.xcall.clone(), + xcall_manager: self.xcall_manager.clone(), + nid: self.nid.clone(), + icon_bn_usd: self.icon_bn_usd.clone(), + xcall_network_address: self.xcall_client.get_network_address(), + upgrade_authority: self.upgrade_authority.clone(), + }; + client.initialize(&self.admin, &config); + } + + pub fn init_xcall_manager_context(&self) { + let client = self::xcall_manager::Client::new(&self.env, &self.xcall_manager); + let config = XcallManagerConfigData { + xcall: self.xcall.clone(), + icon_governance: self.icon_governance.clone(), + upgrade_authority: self.upgrade_authority.clone(), + }; + let sources = Vec::from_array(&self.env, [self.centralized_connection.to_string()]); + let destinations = + Vec::from_array(&self.env, [String::from_str(&self.env, "icon/address")]); + client.initialize( + &self.xcall_manager, + &self.admin, + &config, + &sources, + &destinations, + ); + } + + pub fn init_xcall_state(&self) { + let xcall_client = xcall::Client::new(&self.env, &self.xcall); + + xcall_client.initialize(&xcall::InitializeMsg { + sender: self.admin.clone(), + network_id: self.nid.clone(), + native_token: self.native_token.clone(), + }); + + self.init_connection_state(); + xcall_client.set_protocol_fee(&100); + xcall_client.set_default_connection(&self.nid, &self.centralized_connection); + } + + pub fn init_connection_state(&self) { + let connection_client = connection::Client::new(&self.env, &self.centralized_connection); + + let initialize_msg = connection::InitializeMsg { + native_token: self.native_token.clone(), + relayer: self.admin.clone(), + xcall_address: self.xcall.clone(), + }; + connection_client.initialize(&initialize_msg); + + let message_fee = 100; + let response_fee = 100; + connection_client.set_fee(&self.nid, &message_fee, &response_fee); + } + + pub fn mint_native_token(&self, address: &Address, amount: u128) { + let native_token_client = token::StellarAssetClient::new(&self.env, &self.native_token); + native_token_client.mint(&address, &(*&amount as i128)); + } + + pub fn get_native_token_balance(&self, address: &Address) -> u128 { + let native_token_client = token::TokenClient::new(&self.env, &self.native_token); + let balance = native_token_client.balance(address); + + *&balance as u128 + } +} diff --git a/contracts/balanced_doller/src/tests/test.rs b/contracts/balanced_doller/src/tests/test.rs new file mode 100644 index 0000000..264425a --- /dev/null +++ b/contracts/balanced_doller/src/tests/test.rs @@ -0,0 +1,219 @@ +// #![cfg(test)] +// extern crate std; + +// use crate::contract::BalancedDollarClient; + + +// use soroban_sdk::{ +// symbol_short, +// testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, +// Address, IntoVal, Symbol, +// }; + +// use super::setup::*; + +// #[test] +// fn test() { +// let ctx = TestContext::default(); +// let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); +// let e = &ctx.env; +// e.mock_all_auths(); +// ctx.init_context(&client); + + +// let admin2 = Address::generate(&e); +// let user1 = Address::generate(&e); +// let user2 = Address::generate(&e); +// let user3 = Address::generate(&e); + +// client.mint(&ctx.admin, &1000); +// assert_eq!( +// e.auths(), +// std::vec![( +// ctx.admin.clone(), +// AuthorizedInvocation { +// function: AuthorizedFunction::Contract(( +// client.address.clone(), +// symbol_short!("mint"), +// (&ctx.admin, 1000_i128).into_val(e), +// )), +// sub_invocations: std::vec![] +// } +// )] +// ); +// assert_eq!(client.balance(&ctx.admin), 1000); + +// client.approve(&ctx.admin, &user3, &500, &200); +// assert_eq!( +// e.auths(), +// std::vec![( +// ctx.admin.clone(), +// AuthorizedInvocation { +// function: AuthorizedFunction::Contract(( +// client.address.clone(), +// symbol_short!("approve"), +// (&ctx.admin, &user3, 500_i128, 200_u32).into_val(e), +// )), +// sub_invocations: std::vec![] +// } +// )] +// ); +// assert_eq!(client.allowance(&ctx.admin, &user3), 500); + +// client.transfer(&ctx.admin, &user2, &600); +// assert_eq!( +// e.auths(), +// std::vec![( +// ctx.admin.clone(), +// AuthorizedInvocation { +// function: AuthorizedFunction::Contract(( +// client.address.clone(), +// symbol_short!("transfer"), +// (&ctx.admin, &user2, 600_i128).into_val(e), +// )), +// sub_invocations: std::vec![] +// } +// )] +// ); +// assert_eq!(client.balance(&ctx.admin), 400); +// assert_eq!(client.balance(&user2), 600); + +// client.transfer_from(&user3, &ctx.admin, &user1, &400); +// assert_eq!( +// e.auths(), +// std::vec![( +// user3.clone(), +// AuthorizedInvocation { +// function: AuthorizedFunction::Contract(( +// client.address.clone(), +// Symbol::new(&e, "transfer_from"), +// (&user3, &ctx.admin, &user1, 400_i128).into_val(e), +// )), +// sub_invocations: std::vec![] +// } +// )] +// ); +// assert_eq!(client.balance(&user1), 400); +// assert_eq!(client.balance(&user2), 600); + +// client.transfer(&user1, &user3, &300); +// assert_eq!(client.balance(&user1), 100); +// assert_eq!(client.balance(&user3), 300); + +// client.set_admin(&admin2); +// assert_eq!( +// e.auths(), +// std::vec![( +// ctx.admin.clone(), +// AuthorizedInvocation { +// function: AuthorizedFunction::Contract(( +// client.address.clone(), +// symbol_short!("set_admin"), +// (&admin2,).into_val(e), +// )), +// sub_invocations: std::vec![] +// } +// )] +// ); + +// // Increase to 500 +// client.approve(&user2, &user3, &500, &200); +// assert_eq!(client.allowance(&user2, &user3), 500); +// client.approve(&user2, &user3, &0, &200); +// assert_eq!( +// e.auths(), +// std::vec![( +// user2.clone(), +// AuthorizedInvocation { +// function: AuthorizedFunction::Contract(( +// client.address.clone(), +// symbol_short!("approve"), +// (&user2, &user3, 0_i128, 200_u32).into_val(e), +// )), +// sub_invocations: std::vec![] +// } +// )] +// ); +// assert_eq!(client.allowance(&user2, &user3), 0); +// } + +// #[test] +// fn test_burn() { +// let ctx = TestContext::default(); +// let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); +// let e = &ctx.env; +// e.mock_all_auths(); +// ctx.init_context(&client); + + +// let user2 = Address::generate(&e); + +// client.mint(&ctx.admin, &1000); +// assert_eq!(client.balance(&ctx.admin), 1000); + +// client.approve(&ctx.admin, &user2, &500, &200); +// assert_eq!(client.allowance(&ctx.admin, &user2), 500); + +// client.burn_from(&user2, &ctx.admin, &500); +// assert_eq!( +// e.auths(), +// std::vec![( +// user2.clone(), +// AuthorizedInvocation { +// function: AuthorizedFunction::Contract(( +// client.address.clone(), +// symbol_short!("burn_from"), +// (&user2, &ctx.admin, 500_i128).into_val(e), +// )), +// sub_invocations: std::vec![] +// } +// )] +// ); + +// assert_eq!(client.allowance(&ctx.admin, &user2), 0); +// assert_eq!(client.balance(&ctx.admin), 500); +// assert_eq!(client.balance(&user2), 0); + +// client.burn(&ctx.admin, &500); + +// assert_eq!(client.balance(&ctx.admin), 0); +// assert_eq!(client.balance(&user2), 0); +// } + +// #[test] +// #[should_panic(expected = "insufficient balance")] +// fn transfer_insufficient_balance() { +// let ctx = TestContext::default(); +// let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); +// let e = &ctx.env; +// e.mock_all_auths(); +// ctx.init_context(&client); +// e.mock_all_auths(); +// let user2 = Address::generate(&e); + +// client.mint(&ctx.admin, &1000); +// assert_eq!(client.balance(&ctx.admin), 1000); + +// client.transfer(&ctx.admin, &user2, &1001); +// } + +// #[test] +// #[should_panic(expected = "insufficient allowance")] +// fn transfer_from_insufficient_allowance() { +// let ctx = TestContext::default(); +// let client = BalancedDollarClient::new(&ctx.env, &ctx.registry); +// let e = &ctx.env; +// e.mock_all_auths(); +// ctx.init_context(&client); +// let user2 = Address::generate(&e); +// let user3 = Address::generate(&e); + +// client.mint(&ctx.admin, &1000); +// assert_eq!(client.balance(&ctx.admin), 1000); + +// client.approve(&ctx.admin, &user3, &100, &200); +// assert_eq!(client.allowance(&ctx.admin, &user3), 100); + +// client.transfer_from(&user3, &ctx.admin, &user2, &101); +// } + diff --git a/contracts/balanced_doller/src/xcall_manager_interface.rs b/contracts/balanced_doller/src/xcall_manager_interface.rs new file mode 100644 index 0000000..224bb14 --- /dev/null +++ b/contracts/balanced_doller/src/xcall_manager_interface.rs @@ -0,0 +1,10 @@ +use soroban_sdk::{contractclient, Env, String, Vec}; + +use crate::errors::ContractError; + +#[contractclient(name = "XcallManagerClient")] +pub trait IXcallManager { + fn verify_protocols(e: Env, protocols: Vec) -> Result; + + fn get_protocols(e: Env) -> Result<(Vec, Vec), ContractError>; +} diff --git a/contracts/xcall_manager/Cargo.toml b/contracts/xcall_manager/Cargo.toml new file mode 100644 index 0000000..8e93829 --- /dev/null +++ b/contracts/xcall_manager/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "xcall-manager" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +testutils = ["soroban-sdk/testutils"] + +[dependencies] +soroban-sdk = { workspace = true } +soroban-rlp = { path = "../../libs/soroban-rlp" } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/contracts/xcall_manager/src/config.rs b/contracts/xcall_manager/src/config.rs new file mode 100644 index 0000000..e4eb4ad --- /dev/null +++ b/contracts/xcall_manager/src/config.rs @@ -0,0 +1,19 @@ +use crate::storage_types::DataKey; +use soroban_sdk::{contracttype, unwrap::UnwrapOptimized, Address, Env, String}; + +#[derive(Clone)] +#[contracttype] +pub struct ConfigData { + pub xcall: Address, + pub icon_governance: String, + pub upgrade_authority: Address, +} + +pub fn set_config(e: &Env, config: ConfigData) { + e.storage().instance().set(&DataKey::Config, &config); +} + +pub fn get_config(e: &Env) -> ConfigData { + let key = DataKey::Config; + e.storage().instance().get(&key).unwrap_optimized() +} diff --git a/contracts/xcall_manager/src/contract.rs b/contracts/xcall_manager/src/contract.rs new file mode 100644 index 0000000..335ae0e --- /dev/null +++ b/contracts/xcall_manager/src/contract.rs @@ -0,0 +1,217 @@ +use soroban_sdk::{ + contract, contractimpl, panic_with_error, Address, Bytes, BytesN, Env, String, Vec, +}; +mod xcall { + soroban_sdk::contractimport!(file = "../../wasm/xcall.wasm"); +} + +use crate::{ + config::{self, get_config, set_config, ConfigData}, + states::{ + extend_ttl, has_proposed_removed, has_registry, read_administrator, read_destinations, + read_proposed_removed, read_sources, write_administrator, write_destinations, + write_proposed_removed, write_registry, write_sources, + }, + storage_types::DataKey, + white_list_actions::WhiteListActions, +}; +use soroban_rlp::balanced::messages::configure_protocols::ConfigureProtocols; + +use crate::errors::ContractError; + +const CONFIGURE_PROTOCOLS_NAME: &str = "ConfigureProtocols"; + +#[contract] +pub struct XcallManager; + +#[contractimpl] +impl XcallManager { + pub fn initialize( + env: Env, + registry: Address, + admin: Address, + config: ConfigData, + sources: Vec, + destinations: Vec, + ) { + if has_registry(env.clone()) { + panic_with_error!(env, ContractError::ContractAlreadyInitialized) + } + write_registry(&env, ®istry); + write_administrator(&env, &admin); + set_config(&env, config); + write_sources(&env, &sources); + write_destinations(&env, &destinations); + } + + pub fn get_config(env: Env) -> ConfigData { + get_config(&env) + } + + pub fn set_admin(e: Env, new_admin: Address) { + let admin = read_administrator(&e); + admin.require_auth(); + + write_administrator(&e, &new_admin); + } + + pub fn get_admin(e: Env) -> Address { + read_administrator(&e) + } + + pub fn propose_removal(e: Env, protocol: String) { + let admin = read_administrator(&e); + admin.require_auth(); + + write_proposed_removed(&e, &protocol); + } + + pub fn get_proposed_removal(e: Env) -> String { + read_proposed_removed(&e) + } + + pub fn white_list_actions(e: Env, action: Bytes) { + let actions = WhiteListActions::new(DataKey::WhiteListedActions); + actions.add(&e, action); + } + + pub fn remove_action(e: Env, action: Bytes) -> Result { + let actions = WhiteListActions::new(DataKey::WhiteListedActions); + if !actions.contains(&e, action.clone()) { + return Err(ContractError::NotWhiteListed); + } + actions.remove(&e, action); + Ok(true) + } + + pub fn verify_protocols(e: Env, protocols: Vec) -> Result { + let sources: Vec = read_sources(&e); + + let verified = Self::verify_protocols_unordered(protocols, sources)?; + return Ok(verified); + } + + pub fn get_protocols(e: Env) -> Result<(Vec, Vec), ContractError> { + let sources: Vec = read_sources(&e); + let destinations = read_destinations(&e); + Ok((sources, destinations)) + } + + pub fn verify_protocols_unordered( + array1: Vec, + array2: Vec, + ) -> Result { + // Check if the arrays have the same length + if array1.len() != array2.len() { + return Ok(false); + } + for p in array2.iter() { + let mut j = 0; + for s in array1.iter() { + j = j + 1; + if p.eq(&s) { + break; + } else { + if j == array1.len() { + return Ok(false); + } + continue; + } + } + } + return Ok(true); + } + + pub fn handle_call_message( + e: Env, + from: String, + data: Bytes, + protocols: Vec, + ) -> Result<(), ContractError> { + let config = get_config(&e.clone()); + let xcall = config.xcall; + xcall.require_auth(); + + if from != config.icon_governance { + return Err(ContractError::OnlyICONGovernance); + } + + let actions = WhiteListActions::new(DataKey::WhiteListedActions); + if !actions.contains(&e, data.clone()) { + return Err(ContractError::NotWhiteListed); + } + actions.remove(&e, data.clone()); + + if !Self::verify_protocols(e.clone(), protocols.clone())? { + return Err(ContractError::ProtocolMismatch); + }; + + let method = ConfigureProtocols::get_method(&e.clone(), data.clone()); + + let sources = read_sources(&e); + if !Self::verify_protocols_unordered(protocols.clone(), sources).unwrap() { + if method != String::from_str(&e.clone(), CONFIGURE_PROTOCOLS_NAME) { + return Err(ContractError::UnknownMessageType); + } + Self::verify_protocol_recovery(&e, protocols)?; + } + + if method == String::from_str(&e, CONFIGURE_PROTOCOLS_NAME) { + let message = ConfigureProtocols::decode(&e, data); + let sources = message.sources; + let destinations = message.destinations; + write_sources(&e, &sources); + write_destinations(&e, &destinations); + } else { + return Err(ContractError::UnknownMessageType); + } + Ok(()) + } + + pub fn verify_protocol_recovery(e: &Env, protocols: Vec) -> Result<(), ContractError> { + let modified_sources = Self::get_modified_protocols(e)?; + let verify_unordered = + Self::verify_protocols_unordered(modified_sources, protocols).unwrap(); + if !verify_unordered { + return Err(ContractError::ProtocolMismatch); + } + Ok(()) + } + + pub fn get_modified_protocols(e: &Env) -> Result, ContractError> { + if !has_proposed_removed(e.clone()) { + return Err(ContractError::NoProposalForRemovalExists); + } + + let sources = read_sources(&e); + let protocol_to_remove = read_proposed_removed(&e); + let mut new_array = Vec::new(&e); + for s in sources.iter() { + if !s.eq(&protocol_to_remove) { + new_array.push_back(s); + } + } + + return Ok(new_array); + } + + pub fn set_upgrade_authority(e: Env, upgrade_authority: Address) { + let mut config = config::get_config(&e); + + config.upgrade_authority.require_auth(); + + config.upgrade_authority = upgrade_authority; + config::set_config(&e, config); + } + + pub fn upgrade(e: Env, new_wasm_hash: BytesN<32>) { + let config = get_config(&e); + config.upgrade_authority.require_auth(); + + e.deployer().update_current_contract_wasm(new_wasm_hash); + } + + pub fn extend_ttl(e: Env) { + extend_ttl(&e); + } +} diff --git a/contracts/xcall_manager/src/errors.rs b/contracts/xcall_manager/src/errors.rs new file mode 100644 index 0000000..deeba2b --- /dev/null +++ b/contracts/xcall_manager/src/errors.rs @@ -0,0 +1,17 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ContractError { + InvalidRlpLength = 1, + ContractAlreadyInitialized = 3, + AmountIsLessThanMinimumAmount = 6, + ProtocolMismatch = 7, + OnlyICONGovernance = 8, + OnlyCallService = 9, + UnknownMessageType = 10, + AdminRequired = 11, + NoProposalForRemovalExists = 12, + NotWhiteListed = 13, +} diff --git a/contracts/xcall_manager/src/lib.rs b/contracts/xcall_manager/src/lib.rs new file mode 100644 index 0000000..d000c39 --- /dev/null +++ b/contracts/xcall_manager/src/lib.rs @@ -0,0 +1,8 @@ +#![no_std] +mod config; +pub mod contract; +mod errors; +mod states; +mod storage_types; +mod tests; +mod white_list_actions; diff --git a/contracts/xcall_manager/src/states.rs b/contracts/xcall_manager/src/states.rs new file mode 100644 index 0000000..7947f4f --- /dev/null +++ b/contracts/xcall_manager/src/states.rs @@ -0,0 +1,66 @@ +use soroban_sdk::{Address, Env, String, Vec}; + +use crate::storage_types::DataKey; + +pub(crate) const DAY_IN_LEDGERS: u32 = 17280; +pub(crate) const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; +pub(crate) const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +pub fn read_administrator(e: &Env) -> Address { + let key = DataKey::Admin; + e.storage().instance().get(&key).unwrap() +} + +pub fn write_administrator(e: &Env, id: &Address) { + let key = DataKey::Admin; + e.storage().instance().set(&key, id); +} + +pub fn has_registry(env:Env) -> bool { + env.storage().instance().has(&DataKey::Registry) +} + +pub fn write_registry(e: &Env, id: &Address) { + let key = DataKey::Registry; + e.storage().instance().set(&key, id); +} + +pub fn has_proposed_removed(env:Env) -> bool { + env.storage().instance().has(&DataKey::ProposedProtocolToRemove) +} + +pub fn write_proposed_removed(e: &Env, id: &String) { + let key = DataKey::ProposedProtocolToRemove; + e.storage().instance().set(&key, id); +} + +pub fn read_proposed_removed(e: &Env) -> String { + let key = DataKey::ProposedProtocolToRemove; + e.storage().instance().get(&key).unwrap() +} + +pub fn write_sources(e: &Env, id: &Vec) { + let key = DataKey::Sources; + e.storage().instance().set(&key, id); +} + +pub fn read_sources(e: &Env) -> Vec { + let key = DataKey::Sources; + e.storage().instance().get(&key).unwrap() +} + +pub fn write_destinations(e: &Env, id: &Vec) { + let key = DataKey::Destinations; + e.storage().instance().set(&key, id); +} + +pub fn read_destinations(e: &Env) -> Vec { + let key = DataKey::Destinations; + e.storage().instance().get(&key).unwrap() +} + +pub fn extend_ttl (e: &Env){ + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); +} \ No newline at end of file diff --git a/contracts/xcall_manager/src/storage_types.rs b/contracts/xcall_manager/src/storage_types.rs new file mode 100644 index 0000000..ab85e14 --- /dev/null +++ b/contracts/xcall_manager/src/storage_types.rs @@ -0,0 +1,13 @@ +use soroban_sdk::contracttype; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Registry, + Admin, + ProposedProtocolToRemove, + Config, + Sources, + Destinations, + WhiteListedActions +} diff --git a/contracts/xcall_manager/src/tests/mod.rs b/contracts/xcall_manager/src/tests/mod.rs new file mode 100644 index 0000000..2def534 --- /dev/null +++ b/contracts/xcall_manager/src/tests/mod.rs @@ -0,0 +1,2 @@ +pub mod setup; +pub mod xcall_manager_test; diff --git a/contracts/xcall_manager/src/tests/setup.rs b/contracts/xcall_manager/src/tests/setup.rs new file mode 100644 index 0000000..ece3055 --- /dev/null +++ b/contracts/xcall_manager/src/tests/setup.rs @@ -0,0 +1,112 @@ +#![cfg(test)] +extern crate std; + +use crate::contract::{XcallManager, XcallManagerClient}; + +use crate::config::ConfigData; + +use soroban_sdk::Vec; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; + +mod xcall { + soroban_sdk::contractimport!(file = "../../wasm/xcall.wasm"); +} + +mod connection { + soroban_sdk::contractimport!(file = "../../wasm/centralized_connection.wasm"); +} + +pub struct TestContext { + pub env: Env, + pub registry: Address, + pub admin: Address, + pub depositor: Address, + pub upgrade_authority: Address, + pub xcall: Address, + pub icon_governance: String, + pub xcall_network_address: String, + pub token: Address, + pub centralized_connection: Address, + pub nid: String, + pub native_token: Address, +} + +impl TestContext { + pub fn default() -> Self { + let env = Env::default(); + let token_admin = Address::generate(&env); + let token = env.register_stellar_asset_contract_v2(token_admin.clone()); + let xcall_manager = env.register_contract(None, XcallManager); + let centralized_connection = env.register_contract_wasm(None, connection::WASM); + let xcall = env.register_contract_wasm(None, xcall::WASM); + std::println!("xcall manager{:?}", xcall_manager); + std::println!("xcall {:?}", xcall); + std::println!("centralized {:?}", centralized_connection); + + Self { + registry: xcall_manager, + admin: Address::generate(&env), + depositor: Address::generate(&env), + upgrade_authority: Address::generate(&env), + xcall, + icon_governance: String::from_str(&env, "icon01/kjdnoi"), + xcall_network_address: String::from_str(&env, "stellar/address"), + token: token.address(), + centralized_connection, + nid: String::from_str(&env, "stellar"), + native_token: env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(), + env, + } + } + + pub fn init_context(&self, client: &XcallManagerClient<'static>) { + self.env.mock_all_auths(); + let config = ConfigData { + xcall: self.xcall.clone(), + icon_governance: self.icon_governance.clone(), + upgrade_authority: self.upgrade_authority.clone(), + }; + let sources = Vec::from_array(&self.env, [self.centralized_connection.to_string()]); + let destinations = + Vec::from_array(&self.env, [String::from_str(&self.env, "icon/address")]); + client.initialize( + &self.registry, + &self.admin, + &config, + &sources, + &destinations, + ); + self.init_xcall_state(); + } + + pub fn init_xcall_state(&self) { + let xcall_client = xcall::Client::new(&self.env, &self.xcall); + + xcall_client.initialize(&xcall::InitializeMsg { + sender: self.admin.clone(), + network_id: self.nid.clone(), + native_token: self.native_token.clone(), + }); + + self.init_connection_state(); + xcall_client.set_protocol_fee(&100); + xcall_client.set_default_connection(&self.nid, &self.centralized_connection); + } + + pub fn init_connection_state(&self) { + let connection_client = connection::Client::new(&self.env, &self.centralized_connection); + + let initialize_msg = connection::InitializeMsg { + native_token: self.native_token.clone(), + relayer: self.admin.clone(), + xcall_address: self.xcall.clone(), + }; + connection_client.initialize(&initialize_msg); + + let message_fee = 100; + let response_fee = 100; + connection_client.set_fee(&self.nid, &message_fee, &response_fee); + } +} diff --git a/contracts/xcall_manager/src/tests/xcall_manager_test.rs b/contracts/xcall_manager/src/tests/xcall_manager_test.rs new file mode 100644 index 0000000..c89e290 --- /dev/null +++ b/contracts/xcall_manager/src/tests/xcall_manager_test.rs @@ -0,0 +1,445 @@ +#![cfg(test)] +extern crate std; + +use crate::{config, contract::XcallManagerClient}; + +use super::setup::*; +use soroban_rlp::balanced::messages::configure_protocols::ConfigureProtocols; +use soroban_sdk::{ + symbol_short, + testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, + vec, Address, IntoVal, String, Symbol, Vec, +}; + +#[test] +fn test_initialize() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + + ctx.init_context(&client); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + let destinations = Vec::from_array(&ctx.env, [String::from_str(&ctx.env, "icon/address")]); + let (s, d) = client.get_protocols(); + assert_eq!(s, sources); + assert_eq!(d, destinations); +} + +#[test] +fn test_set_admin() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + let new_admin: Address = Address::generate(&ctx.env); + client.set_admin(&new_admin); + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.registry.clone(), + symbol_short!("set_admin"), + (&new_admin,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(client.get_admin(), new_admin); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn test_initialize_panic_already_initialized() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + + ctx.init_context(&client); + ctx.init_context(&client); + + let sources = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + let destinations = Vec::from_array(&ctx.env, [String::from_str(&ctx.env, "icon/address")]); + let (s, d) = client.get_protocols(); + assert_eq!(s, sources); + assert_eq!(d, destinations); +} + +#[test] +fn test_whitelist_action() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + + ctx.env.mock_all_auths(); + ctx.init_context(&client); + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()) + .encode(&ctx.env, String::from_str(&ctx.env, "ConfigureProtocols")); + client.white_list_actions(&data); + + let result = client.remove_action(&data); + assert!(result == true) +} + +#[test] +fn test_verify_protocols() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + + ctx.init_context(&client); + let protocols = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.verify_protocols(&protocols); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #13)")] +fn test_handle_call_message_for_configure_protocols_panic_for_action_not_whitelisted() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()) + .encode(&ctx.env, String::from_str(&ctx.env, "ConfigureProtocols")); + let decoded: ConfigureProtocols = ConfigureProtocols::decode(&ctx.env, data.clone()); + assert_eq!(decoded.sources, sources); + assert_eq!(decoded.destinations, destinations); + let (s, _) = client.get_protocols(); + client.handle_call_message(&ctx.icon_governance, &data, &s); + + let (s, d) = client.get_protocols(); + assert_eq!(s, sources); + assert_eq!(d, destinations); +} + +#[test] +fn test_handle_call_message_for_configure_protocols() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()) + .encode(&ctx.env, String::from_str(&ctx.env, "ConfigureProtocols")); + let decoded: ConfigureProtocols = ConfigureProtocols::decode(&ctx.env, data.clone()); + client.white_list_actions(&data); + assert_eq!(decoded.sources, sources); + assert_eq!(decoded.destinations, destinations); + let (s, _) = client.get_protocols(); + client.handle_call_message(&ctx.icon_governance, &data, &s); + + let (s, d) = client.get_protocols(); + assert_eq!(s, sources); + assert_eq!(d, destinations); + + //verify multiple protocols + let wrong_sources = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address"), + ]; + let verifiy_false = client.verify_protocols(&Vec::from_array(&ctx.env, wrong_sources)); + assert_eq!(verifiy_false, false); + + let wrong_sources_second = [ + String::from_str(&ctx.env, "stellar/address1"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let verifiy_false_second = + client.verify_protocols(&Vec::from_array(&ctx.env, wrong_sources_second)); + assert_eq!(verifiy_false_second, false); + + let correct_sources = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let verifiy_true = client.verify_protocols(&Vec::from_array(&ctx.env, correct_sources)); + assert_eq!(verifiy_true, true); + + let correct_sources_second: [String; 2] = [ + String::from_str(&ctx.env, "stellar/address1"), + String::from_str(&ctx.env, "stellar/address"), + ]; + let verifiy_true = client.verify_protocols(&Vec::from_array(&ctx.env, correct_sources_second)); + assert_eq!(verifiy_true, true); + + //verify protocol recovery + client.propose_removal(&String::from_str(&ctx.env, "stellar/address1")); + let with_protocol_remove: [String; 1] = [String::from_str(&ctx.env, "stellar/address")]; + client.verify_protocol_recovery(&Vec::from_array(&ctx.env, with_protocol_remove)); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #12)")] +fn test_verify_protocol_recovery_without_removing_protocol() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()) + .encode(&ctx.env, String::from_str(&ctx.env, "ConfigureProtocols")); + let decoded: ConfigureProtocols = ConfigureProtocols::decode(&ctx.env, data.clone()); + client.white_list_actions(&data); + assert_eq!(decoded.sources, sources); + assert_eq!(decoded.destinations, destinations); + let (s, _) = client.get_protocols(); + client.handle_call_message(&ctx.icon_governance, &data, &s); + + //verify protocol recovery + let without_protocol_remove: [String; 2] = [ + String::from_str(&ctx.env, "stellar/address1"), + String::from_str(&ctx.env, "stellar/address"), + ]; + client.verify_protocol_recovery(&Vec::from_array(&ctx.env, without_protocol_remove)); +} + +#[test] +fn test_proposal_removal() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + client.propose_removal(&String::from_str(&ctx.env, "stellar/address")); + assert_eq!( + String::from_str(&ctx.env, "stellar/address"), + client.get_proposed_removal() + ) +} + +#[test] +fn test_get_modified_proposals() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()) + .encode(&ctx.env, String::from_str(&ctx.env, "ConfigureProtocols")); + client.white_list_actions(&data); + let (s, _) = client.get_protocols(); + client.handle_call_message(&ctx.icon_governance, &data, &s); + + client.propose_removal(&String::from_str(&ctx.env, "stellar/address")); + + let updated_protocal = vec![&ctx.env, String::from_str(&ctx.env, "stellar/address1")]; + assert_eq!(updated_protocal, client.get_modified_protocols()); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #12)")] +fn test_get_modified_proposals_panic_no_proposed_removal() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()) + .encode(&ctx.env, String::from_str(&ctx.env, "ConfigureProtocols")); + client.white_list_actions(&data); + let (s, _) = client.get_protocols(); + client.handle_call_message(&ctx.icon_governance, &data, &s); + + let updated_protocal = vec![&ctx.env, String::from_str(&ctx.env, "stellar/address1")]; + assert_eq!(updated_protocal, client.get_modified_protocols()); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #8)")] +fn test_handle_call_message_for_configure_protocols_panic_for_only_icon_governance() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()) + .encode(&ctx.env, String::from_str(&ctx.env, "ConfigureProtocols")); + let decoded: ConfigureProtocols = ConfigureProtocols::decode(&ctx.env, data.clone()); + client.white_list_actions(&data); + assert_eq!(decoded.sources, sources); + assert_eq!(decoded.destinations, destinations); + let (s, _) = client.get_protocols(); + client.handle_call_message(&ctx.xcall_network_address, &data, &s); + + let (s, d) = client.get_protocols(); + assert_eq!(s, sources); + assert_eq!(d, destinations); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #7)")] +fn test_handle_call_message_for_configure_protocols_panic_for_protocol_mismatch() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()) + .encode(&ctx.env, String::from_str(&ctx.env, "ConfigureProtocols")); + let decoded: ConfigureProtocols = ConfigureProtocols::decode(&ctx.env, data.clone()); + client.white_list_actions(&data); + assert_eq!(decoded.sources, sources); + assert_eq!(decoded.destinations, destinations); + let s = Vec::from_array(&ctx.env, [ctx.xcall.to_string()]); + client.handle_call_message(&ctx.icon_governance, &data, &s); + + let (s, d) = client.get_protocols(); + assert_eq!(s, sources); + assert_eq!(d, destinations); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #10)")] +fn test_handle_call_message_for_configure_protocols_panic_for_unknown_mesage_type() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + let source_items = [ + String::from_str(&ctx.env, "stellar/address"), + String::from_str(&ctx.env, "stellar/address1"), + ]; + let destination_items = [ + String::from_str(&ctx.env, "icon/address"), + String::from_str(&ctx.env, "icon/address1"), + ]; + let sources = Vec::from_array(&ctx.env, source_items); + let destinations = Vec::from_array(&ctx.env, destination_items); + let data = ConfigureProtocols::new(sources.clone(), destinations.clone()).encode( + &ctx.env, + String::from_str(&ctx.env, "ConfigureProtocolsPanic"), + ); + client.white_list_actions(&data); + let decoded: ConfigureProtocols = ConfigureProtocols::decode(&ctx.env, data.clone()); + + assert_eq!(decoded.sources, sources); + assert_eq!(decoded.destinations, destinations); + let s = Vec::from_array(&ctx.env, [ctx.centralized_connection.to_string()]); + client.handle_call_message(&ctx.icon_governance, &data, &s); + + let (s, d) = client.get_protocols(); + assert_eq!(s, sources); + assert_eq!(d, destinations); +} + +#[test] +fn test_extend_ttl() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.env.mock_all_auths(); + ctx.init_context(&client); + + client.extend_ttl(); +} + +#[test] +fn test_set_upgrade_authority() { + let ctx = TestContext::default(); + let client = XcallManagerClient::new(&ctx.env, &ctx.registry); + ctx.init_context(&client); + + let new_upgrade_authority = Address::generate(&ctx.env); + client.set_upgrade_authority(&new_upgrade_authority); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.upgrade_authority.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.registry.clone(), + Symbol::new(&ctx.env, "set_upgrade_authority"), + (&new_upgrade_authority,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + + ctx.env.as_contract(&client.address, || { + let config = config::get_config(&ctx.env); + assert_eq!(config.upgrade_authority, new_upgrade_authority) + }); +} diff --git a/contracts/xcall_manager/src/white_list_actions.rs b/contracts/xcall_manager/src/white_list_actions.rs new file mode 100644 index 0000000..36d1759 --- /dev/null +++ b/contracts/xcall_manager/src/white_list_actions.rs @@ -0,0 +1,37 @@ +use soroban_sdk::{Bytes, Env, Vec}; + +use crate::storage_types::DataKey; + +#[derive(Clone)] +pub struct WhiteListActions { + pub key: DataKey, +} + +impl WhiteListActions { + pub fn new(key: DataKey) -> Self { + Self { key } + } + + pub fn add(&self, env: &Env, value: Bytes) { + let mut list = self.get(env); + list.push_back(value); + env.storage().instance().set(&self.key, &list); + } + + pub fn remove(&self, env: &Env, value: Bytes) { + let mut list = self.get(env); + if let Some(pos) = list.iter().position(|x| x == value) { + list.remove(pos as u32); + env.storage().instance().set(&self.key, &list); + } + } + + pub fn contains(&self, env: &Env, value: Bytes) -> bool { + let list = self.get(env); + list.contains(&value) + } + + fn get(&self, env: &Env) -> Vec { + env.storage().instance().get(&self.key).unwrap_or_else(|| Vec::new(env)) + } +} diff --git a/docs.md b/docs.md new file mode 100644 index 0000000..73c1457 --- /dev/null +++ b/docs.md @@ -0,0 +1,27 @@ + +**Balanced Stellar spoke contracts** + +This document outlines the major changes in the implementation of Balanced in the Stellar blockchain and the rationale behind these changes. Key updates includes the merge of `deposit native` and `deposit` method on AssetManager contract, introduction of `extend_ttl` method on all contracts, introduction of `upgrade` method to upgrade the contract etc + +For more details on each spoke contracts see: [Balanced Crosschain Docs](https://github.com/balancednetwork/balanced-java-contracts/blob/420-balanced-docs/docs/crosschain.md) + + +For more details on the Balanced Protocol see [Balanced Docs](https://github.com/balancednetwork/balanced-java-contracts/blob/420-balanced-docs/docs/docs.md) or [Balanced Network](https://balanced.network/) + + + +1. **Merge of `deposit native` and `deposit` method** +**Change:** The methods deposit and deposit native are commonly available on AssetManager contract in other languages, but in stellar(also in SUI) both are merge and deposit method has been used as merged method. + +**Rationale:** +* The stellar native token can be accessed by Stellar Asset Contract (SAC) and the SAC contract address can be used for the token activities, the same way the Stellar Token Contract (STC) is used for the token activities + +2. **Introduction of `extend_ttl` method** +**Change:** New method extend_ttl has been introduced on stellar contracts, which will be used to extend ttl of the storages by the contract admin by paying the required rent periodically (however there is not authentication, anyone can extend the ttl) + +**Rationale:** There are three types of the storages in stellar, Temporary Storage, Instance Storage and Persistence Storage. Balanced has used Instance Storage and Persistence Storage, Temporary Storage has not been required in balanced. The rent paying by user does not seem logical as the applicability of rent is not per transaction but for specified period, In which many users can make transactions. For the reason, it is designed in the balanced such that rent will be paid by the admin periodically + +3. **Introduction of `upgrade` method** +**Change:** `upgrade` method has been introduced in the balanced stellar contracts + +**Rationale:** The upgrade mechanism of stellar contract is different from upgrade mechanism of contracts written on other languages. Stellar contracts can be upgraded simply sending the reference of the hash of the newly installed WASM to the `upgrade` method of the stellar contract. \ No newline at end of file diff --git a/libs/soroban-rlp/Cargo.toml b/libs/soroban-rlp/Cargo.toml new file mode 100644 index 0000000..d1fb1a1 --- /dev/null +++ b/libs/soroban-rlp/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "soroban-rlp" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } \ No newline at end of file diff --git a/libs/soroban-rlp/src/balanced/address_utils.rs b/libs/soroban-rlp/src/balanced/address_utils.rs new file mode 100644 index 0000000..e45995d --- /dev/null +++ b/libs/soroban-rlp/src/balanced/address_utils.rs @@ -0,0 +1,96 @@ +use soroban_sdk::{xdr::ToXdr, xdr::FromXdr, Bytes, Env, String}; + +pub fn is_valid_string_address(address: &String) -> bool { + if address.len() != 56 { + return false; + } + + let mut address_bytes = [0u8; 56]; + address.copy_into_slice(&mut address_bytes); + + let mut is_valid = true; + + if address_bytes[0] != b'G' && address_bytes[0] != b'C' { + is_valid = false; + } + + for &byte in &address_bytes { + if !is_valid_base32(byte) { + is_valid = false; + break; + } + } + + is_valid +} + +pub fn is_valid_bytes_address(address: &Bytes) -> bool { + if address.len() != 56 { + return false; + } + if address.get(0).unwrap() != b'G' && address.get(0).unwrap() != b'C' { + return false; + } + + for i in 0..56 { + let byte = address.get(i).unwrap(); + if !is_valid_base32(byte) { + return false; + } + } + + true +} + +fn is_valid_base32(byte: u8) -> bool { + match byte { + b'A'..=b'Z' | b'2'..=b'7' => true, + _ => false, + } +} + +pub fn get_address_from(network_address: &String, env: &Env) -> String { + let mut nid = Bytes::new(&env); + let mut account = Bytes::new(&env); + + let addr_slice = get_bytes_from_string(&env,network_address.clone()); + + let mut has_seperator = false; + for (index, value) in addr_slice.clone().iter().enumerate() { + if has_seperator { + account.append(&addr_slice.slice(index as u32..addr_slice.len())); + break; + } else if value == 47 { + has_seperator = true; + } else { + nid.push_back(value) + } + } + + if !has_seperator { + panic!("Invalid network address") + } + + + get_string_from_bytes(&env, account) + +} + +pub fn get_bytes_from_string(env: &Env, value: String) -> Bytes { + let bytes = value.to_xdr(&env); + + if bytes.get(6).unwrap() > 0 { + panic!("Invalid network address length") + } + + let value_len = bytes.get(7).unwrap(); + let slice = bytes.slice(8..value_len as u32 + 8); + slice +} + +pub fn get_string_from_bytes(e: &Env, bytes: Bytes) -> String { + let mut bytes_xdr = bytes.to_xdr(&e); + bytes_xdr.set(3, 14); + + String::from_xdr(&e, &bytes_xdr).unwrap() +} \ No newline at end of file diff --git a/libs/soroban-rlp/src/balanced/messages/configure_protocols.rs b/libs/soroban-rlp/src/balanced/messages/configure_protocols.rs new file mode 100644 index 0000000..2c7505c --- /dev/null +++ b/libs/soroban-rlp/src/balanced/messages/configure_protocols.rs @@ -0,0 +1,57 @@ +use crate::decoder; +use crate::encoder; +use soroban_sdk::{contracttype, Bytes, Env, String, Vec}; + +#[derive(Clone)] +#[contracttype] +pub struct ConfigureProtocols { + pub sources: Vec, + pub destinations: Vec, +} + +impl ConfigureProtocols { + pub fn new(sources: Vec, destinations: Vec) -> Self { + Self { + sources, + destinations, + } + } + + pub fn sources(&self) -> &Vec { + &self.sources + } + + pub fn destinations(&self) -> &Vec { + &self.destinations + } + + pub fn encode(&self, e: &Env, method: String) -> Bytes { + let mut list: Vec = Vec::new(&e); + list.push_back(encoder::encode_string(&e, method)); + list.push_back(encoder::encode_strings(&e, self.sources.clone())); + list.push_back(encoder::encode_strings(&e, self.destinations.clone())); + + let encoded = encoder::encode_list(&e, list, false); + encoded + } + + pub fn decode(e: &Env, bytes: Bytes) -> ConfigureProtocols { + let decoded = decoder::decode_list(&e, bytes); + if decoded.len() != 3 { + panic!("InvalidRlpLength"); + } + + let sources = decoder::decode_strings(e, decoded.get(1).unwrap()); + let destinations = decoder::decode_strings(e, decoded.get(2).unwrap()); + Self { + sources, + destinations, + } + } + + pub fn get_method(e: &Env, bytes: Bytes) -> String { + let decoded = decoder::decode_list(&e, bytes); + let method = decoder::decode_string(e, decoded.get(0).unwrap()); + method + } +} diff --git a/libs/soroban-rlp/src/balanced/messages/cross_transfer.rs b/libs/soroban-rlp/src/balanced/messages/cross_transfer.rs new file mode 100644 index 0000000..743277e --- /dev/null +++ b/libs/soroban-rlp/src/balanced/messages/cross_transfer.rs @@ -0,0 +1,67 @@ +use crate::decoder; +use crate::encoder; +use soroban_sdk::{contracttype, Bytes, Env, String, Vec}; + +#[derive(Clone)] +#[contracttype] +pub struct CrossTransfer { + pub from: String, + pub to: String, + pub amount: u128, + pub data: Bytes, +} + +impl CrossTransfer { + pub fn new(from: String, to: String, amount: u128, data: Bytes) -> Self { + Self { + from, + to, + amount, + data, + } + } + + pub fn from(&self) -> &String { + &self.from + } + + pub fn to(&self) -> &String { + &self.to + } + + pub fn encode(&self, e: &Env, method: String) -> Bytes { + let mut list: Vec = Vec::new(&e); + list.push_back(encoder::encode_string(&e, method)); + list.push_back(encoder::encode_string(&e, self.from.clone())); + list.push_back(encoder::encode_string(&e, self.to.clone())); + list.push_back(encoder::encode_u128(&e, self.amount.clone())); + list.push_back(encoder::encode(&e, self.data.clone())); + + let encoded = encoder::encode_list(&e, list, false); + encoded + } + + pub fn decode(e: &Env, bytes: Bytes) -> CrossTransfer { + let decoded = decoder::decode_list(&e, bytes); + if decoded.len() != 5 { + panic!("InvalidRlpLength"); + } + + let from = decoder::decode_string(e, decoded.get(1).unwrap()); + let to = decoder::decode_string(e, decoded.get(2).unwrap()); + let amount = decoder::decode_u128(e, decoded.get(3).unwrap()); + let data = decoded.get(4).unwrap(); + Self { + from, + to, + amount, + data, + } + } + + pub fn get_method(e: &Env, bytes: Bytes) -> String { + let decoded = decoder::decode_list(&e, bytes); + let method = decoder::decode_string(e, decoded.get(0).unwrap()); + method + } +} diff --git a/libs/soroban-rlp/src/balanced/messages/cross_transfer_revert.rs b/libs/soroban-rlp/src/balanced/messages/cross_transfer_revert.rs new file mode 100644 index 0000000..9e2fef5 --- /dev/null +++ b/libs/soroban-rlp/src/balanced/messages/cross_transfer_revert.rs @@ -0,0 +1,53 @@ +use soroban_sdk::{contracttype, Env, String, Address, Bytes, Vec}; +use crate::encoder; +use crate::decoder; + +#[derive(Clone)] +#[contracttype] +pub struct CrossTransferRevert { + pub to: Address, + pub amount: u128 +} + +impl CrossTransferRevert{ + pub fn new(to: Address, amount: u128) -> Self { + Self { + to, + amount, + } + } + + pub fn to(&self) -> &Address { + &self.to + } + + pub fn amount(&self) -> &u128 { + &self.amount + } + + pub fn encode(&self, e: &Env, method: String) -> Bytes { + let mut list: Vec = Vec::new(&e); + + list.push_back(encoder::encode_string(&e, method)); + list.push_back(encoder::encode_string(&e, self.to.clone().to_string())); + list.push_back(encoder::encode_u128(&e, self.amount.clone())); + + let encoded = encoder::encode_list(&e, list, false); + encoded + } + + pub fn decode(e: &Env, bytes: Bytes) -> CrossTransferRevert{ + let decoded = decoder::decode_list(&e, bytes); + if decoded.len() != 3 { + panic!("InvalidRlpLength"); + } + + let to = Address::from_string(&decoder::decode_string(e, decoded.get(1).unwrap())); + let amount = decoder::decode_u128(e, decoded.get(2).unwrap()); + + Self { + to, + amount + } + } +} \ No newline at end of file diff --git a/libs/soroban-rlp/src/balanced/messages/deposit.rs b/libs/soroban-rlp/src/balanced/messages/deposit.rs new file mode 100644 index 0000000..720db75 --- /dev/null +++ b/libs/soroban-rlp/src/balanced/messages/deposit.rs @@ -0,0 +1,85 @@ +use crate::decoder; +use crate::encoder; +use soroban_sdk::{contracttype, Bytes, Env, String, Vec}; + +#[derive(Clone)] +#[contracttype] +pub struct Deposit { + pub token_address: String, + pub from: String, + pub to: String, + pub amount: u128, + pub data: Bytes, +} + +impl Deposit { + pub fn new(token_address: String, from: String, to: String, amount: u128, data: Bytes) -> Self { + Self { + token_address, + from, + to, + amount, + data, + } + } + + pub fn token_address(&self) -> &String { + &self.token_address + } + + pub fn from(&self) -> &String { + &self.from + } + + pub fn to(&self) -> &String { + &self.to + } + + pub fn amount(&self) -> &u128 { + &self.amount + } + + pub fn data(&self) -> &Bytes { + &self.data + } + + pub fn encode(&self, e: &Env, method: String) -> Bytes { + let mut list: Vec = Vec::new(&e); + list.push_back(encoder::encode_string(&e, method)); + list.push_back(encoder::encode_string(&e, self.token_address.clone())); + list.push_back(encoder::encode_string(&e, self.from.clone())); + list.push_back(encoder::encode_string(&e, self.to.clone())); + list.push_back(encoder::encode_u128(&e, self.amount.clone())); + list.push_back(encoder::encode(&e, self.data.clone())); + + let encoded = encoder::encode_list(&e, list, false); + encoded + } + + pub fn decode(e: &Env, bytes: Bytes) -> Deposit { + let decoded = decoder::decode_list(&e, bytes); + if decoded.len() != 6 { + panic!("InvalidRlpLength"); + } + + let token_address = decoder::decode_string(e, decoded.get(1).unwrap()); + let from = decoder::decode_string(e, decoded.get(2).unwrap()); + let to = decoder::decode_string(e, decoded.get(3).unwrap()); + let amount = decoder::decode_u128(e, decoded.get(4).unwrap()); + let data = decoded.get(5).unwrap(); + + Self { + token_address, + from, + to, + amount, + data, + } + } + + pub fn get_method(e: &Env, bytes: Bytes) -> String { + let decoded = decoder::decode_list(&e, bytes); + let method = decoder::decode_string(e, decoded.get(0).unwrap()); + method + } +} diff --git a/libs/soroban-rlp/src/balanced/messages/deposit_revert.rs b/libs/soroban-rlp/src/balanced/messages/deposit_revert.rs new file mode 100644 index 0000000..8ad1742 --- /dev/null +++ b/libs/soroban-rlp/src/balanced/messages/deposit_revert.rs @@ -0,0 +1,61 @@ +use soroban_sdk::{contracttype, Env, String, Address, Bytes, Vec}; +use crate::encoder; +use crate::decoder; + +#[derive(Clone)] +#[contracttype] +pub struct DepositRevert { + pub token_address: Address, + pub to: Address, + pub amount: u128 +} + +impl DepositRevert{ + pub fn new(token_address: Address, to: Address, amount: u128) -> Self { + Self { + token_address, + to, + amount, + } + } + + pub fn token_address(&self) -> &Address { + &self.token_address + } + + pub fn to(&self) -> &Address { + &self.to + } + + pub fn amount(&self) -> &u128 { + &self.amount + } + + pub fn encode(&self, e: &Env, method: String) -> Bytes { + let mut list: Vec = Vec::new(&e); + list.push_back(encoder::encode_string(&e, method)); + list.push_back(encoder::encode_string(&e, self.token_address.to_string().clone())); + list.push_back(encoder::encode_string(&e, self.to.to_string().clone())); + list.push_back(encoder::encode_u128(&e, self.amount.clone())); + + let encoded = encoder::encode_list(&e, list, false); + encoded + } + + pub fn decode(e: &Env, bytes: Bytes) -> DepositRevert { + let decoded = decoder::decode_list(&e, bytes); + if decoded.len() != 4 { + panic!("InvalidRlpLength"); + } + + let token_address = Address::from_string(&decoder::decode_string(e, decoded.get(1).unwrap())); + let to = Address::from_string(&decoder::decode_string(e, decoded.get(2).unwrap())); + let amount = decoder::decode_u128(e, decoded.get(3).unwrap()); + + Self { + token_address, + to, + amount + } + } +} \ No newline at end of file diff --git a/libs/soroban-rlp/src/balanced/messages/mod.rs b/libs/soroban-rlp/src/balanced/messages/mod.rs new file mode 100644 index 0000000..c11ac92 --- /dev/null +++ b/libs/soroban-rlp/src/balanced/messages/mod.rs @@ -0,0 +1,7 @@ +pub mod deposit_revert; +pub mod deposit; +pub mod withdraw_to; +pub mod cross_transfer; +pub mod cross_transfer_revert; +pub mod configure_protocols; + diff --git a/libs/soroban-rlp/src/balanced/messages/withdraw_to.rs b/libs/soroban-rlp/src/balanced/messages/withdraw_to.rs new file mode 100644 index 0000000..ca06dd9 --- /dev/null +++ b/libs/soroban-rlp/src/balanced/messages/withdraw_to.rs @@ -0,0 +1,61 @@ +use soroban_sdk::{contracttype, Env, String, Bytes, Vec}; +use crate::encoder; +use crate::decoder; + +#[derive(Clone)] +#[contracttype] +pub struct WithdrawTo { + pub token_address: String, + pub to: String, + pub amount: u128 +} + +impl WithdrawTo{ + pub fn new(token_address: String, to: String, amount: u128) -> Self { + Self { + token_address, + to, + amount + } + } + + pub fn token_address(&self) -> &String { + &self.token_address + } + + pub fn to(&self) -> &String { + &self.to + } + + pub fn amount(&self) -> &u128 { + &self.amount + } + + pub fn encode(&self, e: &Env, method: String) -> Bytes { + let mut list: Vec = Vec::new(&e); + list.push_back(encoder::encode_string(&e, method)); + list.push_back(encoder::encode_string(&e, self.token_address.clone())); + list.push_back(encoder::encode_string(&e, self.to.clone())); + list.push_back(encoder::encode_u128(&e, self.amount.clone())); + + let encoded = encoder::encode_list(&e, list, false); + encoded + } + + pub fn decode(e: &Env, bytes: Bytes) -> WithdrawTo { + let decoded = decoder::decode_list(&e, bytes); + if decoded.len() != 4 { + panic!("InvalidRlpLength"); + } + + let token_address = decoder::decode_string(e, decoded.get(1).unwrap()); + let to = decoder::decode_string(e, decoded.get(2).unwrap()); + let amount = decoder::decode_u128(e, decoded.get(3).unwrap()); + + Self { + token_address, + to, + amount + } + } +} \ No newline at end of file diff --git a/libs/soroban-rlp/src/balanced/mod.rs b/libs/soroban-rlp/src/balanced/mod.rs new file mode 100644 index 0000000..2f5876d --- /dev/null +++ b/libs/soroban-rlp/src/balanced/mod.rs @@ -0,0 +1,2 @@ +pub mod address_utils; +pub mod messages; diff --git a/libs/soroban-rlp/src/decoder.rs b/libs/soroban-rlp/src/decoder.rs new file mode 100644 index 0000000..37b8340 --- /dev/null +++ b/libs/soroban-rlp/src/decoder.rs @@ -0,0 +1,152 @@ +use super::utils::*; +use soroban_sdk::{vec, Bytes, Env, String, Vec}; + +pub fn decode(env: &Env, bytes: Bytes) -> Bytes { + assert!(bytes.len() > 0); + + let rlp_byte = bytes.get(0).unwrap(); + + #[allow(unused_comparisons)] + let decoded = if rlp_byte == 0x80 || rlp_byte == 0xc0 { + Bytes::new(&env) + } else if rlp_byte < 0x80 { + bytes + } else if rlp_byte < 0xb8 { + let data_len = rlp_byte - 0x80; + slice_vector(&env, bytes, 1, data_len as u64) + } else if rlp_byte > 0xb7 && rlp_byte < 0xc0 { + let data_bytes_len = rlp_byte - 0xb7; + let len_bytes = slice_vector(&env, bytes.clone(), 1, data_bytes_len as u64); + + let data_len = bytes_to_u64(len_bytes.clone()); + let data_start = len_bytes.len() + 1; + + slice_vector(&env, bytes, data_start as u64, data_len) + } else if rlp_byte > 0xc0 && rlp_byte <= 0xf7 { + let data_len = rlp_byte - 0xc0; + slice_vector(&env, bytes, 1, data_len as u64) + } else if rlp_byte > 0xf7 && rlp_byte <= 0xff { + let data_bytes_len = rlp_byte - 0xf7; + let len_bytes = slice_vector(&env, bytes.clone(), 1, data_bytes_len as u64); + + let data_len = bytes_to_u64(len_bytes.clone()); + let data_start = len_bytes.len() + 1; + + slice_vector(&env, bytes, data_start as u64, data_len) + } else { + panic!("invalid rlp byte length") + }; + + decoded +} + +pub fn decode_list(env: &Env, list: Bytes) -> Vec { + let data_len = decode_length(&env, list.clone(), 0xc0); + let start = list.len() as u64 - data_len; + let encoded = slice_vector(&env, list, start, data_len); + + let mut decoded: Vec = Vec::new(&env); + let mut i = 0; + while i < encoded.len() { + let byte = encoded.get(i).unwrap(); + + #[allow(unused_comparisons)] + if byte == 0x80 || byte == 0xc0 { + decoded.push_back(Bytes::new(&env)); + i = i + 1; + } else if byte < 0x80 { + let mut singleton = Bytes::new(&env); + singleton.push_back(byte); + decoded.push_back(singleton); + i = i + 1; + } else if byte > 0x80 && byte < 0xB8 { + let len = (byte - 0x80) as u64; + decoded.push_back(slice_vector(&env, encoded.clone(), i as u64 + 1, len)); + i = i + (len as u32 + 1); + } else if byte > 0xc0 && byte < 0xf7 { + let len = (byte - 0xc0) as u64; + decoded.push_back(slice_vector(&env, encoded.clone(), i as u64, len + 1)); + i = i + (len as u32 + 1) + } else if byte > 0xb7 && byte < 0xc0 { + let data_bytes_len = (byte - 0xb7) as u64; + let len_bytes = slice_vector(&env, encoded.clone(), i as u64 + 1, data_bytes_len); + let len = bytes_to_u64(len_bytes); + decoded.push_back(slice_vector( + &env, + encoded.clone(), + i as u64 + data_bytes_len + 1, + len, + )); + i = i + (data_bytes_len + len + 1) as u32 + } else if byte > 0xf7 && byte <= 0xff { + let data_bytes_len = (byte - 0xf7) as u64; + let len_bytes = slice_vector(&env, encoded.clone(), i as u64 + 1, data_bytes_len); + let len = bytes_to_u64(len_bytes); + if byte == 0xf8 && len == 0 { + decoded.push_back(Bytes::new(&env)); + } else { + decoded.push_back(slice_vector( + &env, + encoded.clone(), + i as u64, + data_bytes_len + len + 1, + )); + } + i = i + (data_bytes_len + len + 1) as u32 + } else { + panic!("invalid rlp byte length") + } + } + decoded +} + +pub fn decode_length(env: &Env, bytes: Bytes, offset: u8) -> u64 { + let bytes_len = bytes.len(); + + let len = if bytes_len == 0 { + 0 + } else if bytes_len < 56 { + (bytes.get(0).unwrap() - offset) as u64 + } else { + let len = bytes.get(0).unwrap() - offset - 55; + let len_bytes = slice_vector(env, bytes, 1, len as u64); + bytes_to_u64(len_bytes) + }; + + len +} + +pub fn decode_u8(env: &Env, bytes: Bytes) -> u8 { + decode(&env, bytes).get(0).unwrap_or(0) +} + +pub fn decode_u32(env: &Env, bytes: Bytes) -> u32 { + let decoded = decode(&env, bytes); + bytes_to_u32(decoded) +} + +pub fn decode_u64(env: &Env, bytes: Bytes) -> u64 { + let decoded = decode(&env, bytes); + bytes_to_u64(decoded) +} + +pub fn decode_u128(env: &Env, bytes: Bytes) -> u128 { + let decoded = decode(&env, bytes); + bytes_to_u128(decoded) +} + +pub fn decode_string(env: &Env, bytes: Bytes) -> String { + let decoded = decode(&env, bytes); + bytes_to_string(&env, decoded) +} + +pub fn decode_strings(env: &Env, bytes: Bytes) -> Vec { + let list = decode_list(&env, bytes); + + let mut strings: Vec = vec![&env]; + for byte in list { + strings.push_back(bytes_to_string(&env, byte)) + } + + strings +} diff --git a/libs/soroban-rlp/src/encoder.rs b/libs/soroban-rlp/src/encoder.rs new file mode 100644 index 0000000..3b55bf7 --- /dev/null +++ b/libs/soroban-rlp/src/encoder.rs @@ -0,0 +1,92 @@ +use super::utils::*; +use soroban_sdk::{bytes, vec, Bytes, Env, String, Vec}; + +pub fn encode(env: &Env, bytes: Bytes) -> Bytes { + let len = bytes.len(); + + let encoded = if len == 0 { + bytes!(&env, 0x80) + } else if len == 1 && bytes.get(0).unwrap() < 128 { + bytes + } else { + let mut res = encode_length(&env, len as u64, 0x80); + res.append(&bytes); + res + }; + + encoded +} + +pub fn encode_list(env: &Env, list: Vec, raw: bool) -> Bytes { + let mut res = Bytes::new(&env); + if list.len() == 0 { + res.push_back(0xc0); + } else { + for bytes in list { + if raw == true { + res.append(&encode(&env, bytes.clone())) + } else { + res.append(&bytes) + } + } + let len = res.len(); + let mut len_buffer = encode_length(&env, len as u64, 0xc0); + + len_buffer.append(&res); + res = len_buffer + } + res +} + +pub fn encode_length(env: &Env, len: u64, offset: u8) -> Bytes { + let mut len_info = Bytes::new(&env); + + if len < 56 { + let len_u8 = len as u8; + len_info.push_back(len_u8 + offset) + } else { + let mut bytes_length = u64_to_bytes(&env, len); + let rlp_bytes_len = bytes_length.len() as u8; + len_info.push_back(rlp_bytes_len + offset + 55); + len_info.append(&mut bytes_length); + } + + len_info +} + +pub fn encode_u8(env: &Env, num: u8) -> Bytes { + let mut bytes = Bytes::new(&env); + bytes.push_back(num); + + encode(&env, bytes) +} + +pub fn encode_u32(env: &Env, num: u32) -> Bytes { + let bytes = u32_to_bytes(&env, num); + encode(&env, bytes) +} + +pub fn encode_u64(env: &Env, num: u64) -> Bytes { + let bytes = u64_to_bytes(&env, num); + encode(&env, bytes) +} + +pub fn encode_u128(env: &Env, num: u128) -> Bytes { + let bytes = u128_to_bytes(&env, num); + encode(&env, bytes) +} + +pub fn encode_string(env: &Env, value: String) -> Bytes { + let bytes = string_to_bytes(&env, value); + encode(&env, bytes) +} + +pub fn encode_strings(env: &Env, values: Vec) -> Bytes { + let mut list: Vec = vec![&env]; + + for value in values { + list.push_back(encode_string(&env, value)); + } + + encode_list(&env, list, false) +} diff --git a/libs/soroban-rlp/src/lib.rs b/libs/soroban-rlp/src/lib.rs new file mode 100644 index 0000000..514036a --- /dev/null +++ b/libs/soroban-rlp/src/lib.rs @@ -0,0 +1,5 @@ +#![no_std] +pub mod decoder; +pub mod encoder; +mod utils; +pub mod balanced; diff --git a/libs/soroban-rlp/src/utils.rs b/libs/soroban-rlp/src/utils.rs new file mode 100644 index 0000000..366309d --- /dev/null +++ b/libs/soroban-rlp/src/utils.rs @@ -0,0 +1,111 @@ +use soroban_sdk::{ + bytes, + xdr::{FromXdr, ToXdr}, + Bytes, Env, String, +}; + +pub fn u32_to_bytes(env: &Env, number: u32) -> Bytes { + let mut bytes = bytes!(&env, 0x00); + let mut i = 3; + let mut leading_zero = true; + while i >= 0 { + let val = (number >> (i * 8) & 0xff) as u8; + if val > 0 || !leading_zero { + leading_zero = false; + bytes.push_back(val); + } + + i -= 1; + } + bytes +} + +pub fn bytes_to_u32(bytes: Bytes) -> u32 { + let mut num = 0; + for byte in bytes.iter() { + num = (num << 8) | byte as u32; + } + num +} + +pub fn u64_to_bytes(env: &Env, number: u64) -> Bytes { + let mut bytes = bytes!(&env, 0x00); + let mut i = 7; + let mut leading_zero = true; + while i >= 0 { + let val = (number >> (i * 8) & 0xff) as u8; + if val > 0 || !leading_zero { + leading_zero = false; + bytes.push_back(val); + } + + i -= 1; + } + bytes +} + +pub fn bytes_to_u64(bytes: Bytes) -> u64 { + let mut num = 0; + for byte in bytes.iter() { + num = (num << 8) | byte as u64; + } + num +} + +pub fn u128_to_bytes(env: &Env, number: u128) -> Bytes { + let mut bytes = bytes!(&env, 0x00); + let mut i = 15; + let mut leading_zero = true; + while i >= 0 { + let val = (number >> (i * 8) & 0xff) as u8; + if val > 0 || !leading_zero { + leading_zero = false; + bytes.push_back(val); + } + + i -= 1; + } + + bytes +} + +pub fn bytes_to_u128(bytes: Bytes) -> u128 { + let mut num = 0; + for byte in bytes.iter() { + num = (num << 8) | byte as u128 + } + num +} + +pub fn slice_vector(env: &Env, arr: Bytes, start: u64, length: u64) -> Bytes { + let mut sliced = Bytes::new(&env); + let mut start = start; + let end = start + length; + + while start < end { + let item = arr.get(start as u32).unwrap(); + sliced.push_back(item); + start += 1; + } + sliced +} + +pub fn string_to_bytes(env: &Env, value: String) -> Bytes { + let mut start_index = 8; + let end_index = start_index + value.len(); + let string_xdr = value.to_xdr(&env); + + let mut bytes = Bytes::new(&env); + while start_index < end_index { + bytes.push_back(string_xdr.get(start_index).unwrap()); + start_index += 1; + } + bytes +} + +pub fn bytes_to_string(env: &Env, bytes: Bytes) -> String { + let mut bytes_xdr = bytes.to_xdr(&env); + bytes_xdr.set(3, 14); + + String::from_xdr(&env, &bytes_xdr).unwrap() +} \ No newline at end of file diff --git a/wasm/centralized_connection.wasm b/wasm/centralized_connection.wasm new file mode 100644 index 0000000..9a4fb1c Binary files /dev/null and b/wasm/centralized_connection.wasm differ diff --git a/wasm/xcall.wasm b/wasm/xcall.wasm new file mode 100644 index 0000000..40bcf10 Binary files /dev/null and b/wasm/xcall.wasm differ