diff --git a/Cargo.lock b/Cargo.lock index 632c8c62..cfb08e33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer 0.10.3", "crypto-common", @@ -286,7 +286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" dependencies = [ "bitmaps", - "rand_core 0.6.3", + "rand_core 0.6.4", "rand_xoshiro", "sized-chunks", "typenum", @@ -311,9 +311,9 @@ checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" [[package]] name = "itertools" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] @@ -326,9 +326,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "libm" @@ -487,9 +487,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rand_hc" @@ -506,7 +506,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -567,20 +567,20 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] name = "signature" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90531723b08e4d6d71b791108faf51f03e1b4a7784f96b2b87f852ebc247228" +checksum = "deb766570a2825fa972bceff0d195727876a9cdf2460ab2e52d455dc2de47fd9" [[package]] name = "sized-chunks" @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "soroban-env-common" version = "0.0.5" -source = "git+https://github.com/stellar/rs-soroban-env?rev=a51888cf#a51888cf2dd659d71d3dd876b63e3bcdb7c970f4" +source = "git+https://github.com/stellar/rs-soroban-env?rev=5d57d88#5d57d8814c3f7e2f6c10954435b8057221313106" dependencies = [ "soroban-env-macros", "soroban-wasmi", @@ -659,7 +659,7 @@ dependencies = [ [[package]] name = "soroban-env-guest" version = "0.0.5" -source = "git+https://github.com/stellar/rs-soroban-env?rev=a51888cf#a51888cf2dd659d71d3dd876b63e3bcdb7c970f4" +source = "git+https://github.com/stellar/rs-soroban-env?rev=5d57d88#5d57d8814c3f7e2f6c10954435b8057221313106" dependencies = [ "soroban-env-common", "static_assertions", @@ -668,7 +668,7 @@ dependencies = [ [[package]] name = "soroban-env-host" version = "0.0.5" -source = "git+https://github.com/stellar/rs-soroban-env?rev=a51888cf#a51888cf2dd659d71d3dd876b63e3bcdb7c970f4" +source = "git+https://github.com/stellar/rs-soroban-env?rev=5d57d88#5d57d8814c3f7e2f6c10954435b8057221313106" dependencies = [ "backtrace", "dyn-fmt", @@ -680,7 +680,7 @@ dependencies = [ "num-integer", "num-traits", "parity-wasm", - "sha2 0.10.5", + "sha2 0.10.6", "soroban-env-common", "soroban-native-sdk-macros", "soroban-wasmi", @@ -691,7 +691,7 @@ dependencies = [ [[package]] name = "soroban-env-macros" version = "0.0.5" -source = "git+https://github.com/stellar/rs-soroban-env?rev=a51888cf#a51888cf2dd659d71d3dd876b63e3bcdb7c970f4" +source = "git+https://github.com/stellar/rs-soroban-env?rev=5d57d88#5d57d8814c3f7e2f6c10954435b8057221313106" dependencies = [ "itertools", "proc-macro2", @@ -727,7 +727,7 @@ version = "0.0.0" dependencies = [ "ed25519-dalek", "rand", - "sha2 0.10.5", + "sha2 0.10.6", "soroban-auth", "soroban-sdk", "soroban-token-contract", @@ -740,7 +740,7 @@ version = "0.0.0" dependencies = [ "ed25519-dalek", "rand", - "sha2 0.10.5", + "sha2 0.10.6", "soroban-auth", "soroban-liquidity-pool-contract", "soroban-sdk", @@ -748,10 +748,18 @@ dependencies = [ "stellar-xdr", ] +[[package]] +name = "soroban-liquidity-pool-router-with-payload" +version = "0.0.0" +dependencies = [ + "soroban-auth", + "soroban-sdk", +] + [[package]] name = "soroban-native-sdk-macros" version = "0.0.5" -source = "git+https://github.com/stellar/rs-soroban-env?rev=a51888cf#a51888cf2dd659d71d3dd876b63e3bcdb7c970f4" +source = "git+https://github.com/stellar/rs-soroban-env?rev=5d57d88#5d57d8814c3f7e2f6c10954435b8057221313106" dependencies = [ "itertools", "proc-macro2", @@ -780,7 +788,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "sha2 0.10.5", + "sha2 0.10.6", "soroban-env-common", "soroban-spec", "stellar-xdr", @@ -815,7 +823,7 @@ version = "0.0.0" dependencies = [ "ed25519-dalek", "rand", - "sha2 0.10.5", + "sha2 0.10.6", "soroban-auth", "soroban-sdk", "soroban-single-offer-contract", @@ -836,7 +844,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2 0.10.5", + "sha2 0.10.6", "soroban-env-host", "stellar-xdr", "syn", @@ -866,6 +874,17 @@ dependencies = [ "soroban-sdk", ] +[[package]] +name = "soroban-token-with-payload" +version = "0.0.4" +dependencies = [ + "ed25519-dalek", + "num-bigint", + "rand", + "soroban-auth", + "soroban-sdk", +] + [[package]] name = "soroban-wasmi" version = "0.16.0" @@ -923,9 +942,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" dependencies = [ "proc-macro2", "quote", @@ -993,9 +1012,9 @@ checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index e4703c5c..acdf77ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,9 @@ members = [ "single_offer_router", "events", "timelock", - "token" + "token", + "token_with_payload", + "liquidity_pool_router_with_payload" ] [profile.release] @@ -35,10 +37,10 @@ soroban-sdk = { git = "https://github.com/stellar/rs-soroban-sdk", rev = "3b2994 soroban-spec = { git = "https://github.com/stellar/rs-soroban-sdk", rev = "3b299468" } soroban-auth = { git = "https://github.com/stellar/rs-soroban-sdk", rev = "3b299468" } soroban-sdk-macros = { git = "https://github.com/stellar/rs-soroban-sdk", rev = "3b299468" } -soroban-env-common = { git = "https://github.com/stellar/rs-soroban-env", rev = "a51888cf" } -soroban-env-guest = { git = "https://github.com/stellar/rs-soroban-env", rev = "a51888cf" } -soroban-env-host = { git = "https://github.com/stellar/rs-soroban-env", rev = "a51888cf" } -soroban-env-macros = { git = "https://github.com/stellar/rs-soroban-env", rev = "a51888cf" } -soroban-native-sdk-macros = { git = "https://github.com/stellar/rs-soroban-env", rev = "a51888cf" } +soroban-env-common = { git = "https://github.com/stellar/rs-soroban-env", rev = "5d57d88" } +soroban-env-guest = { git = "https://github.com/stellar/rs-soroban-env", rev = "5d57d88" } +soroban-env-host = { git = "https://github.com/stellar/rs-soroban-env", rev = "5d57d88" } +soroban-env-macros = { git = "https://github.com/stellar/rs-soroban-env", rev = "5d57d88" } +soroban-native-sdk-macros = { git = "https://github.com/stellar/rs-soroban-env", rev = "5d57d88" } stellar-xdr = { git = "https://github.com/stellar/rs-stellar-xdr", rev = "469efc9" } wasmi = { package = "soroban-wasmi", git = "https://github.com/stellar/wasmi", rev = "a61b6df" } diff --git a/liquidity_pool_router_with_payload/Cargo.toml b/liquidity_pool_router_with_payload/Cargo.toml new file mode 100644 index 00000000..7d79abf8 --- /dev/null +++ b/liquidity_pool_router_with_payload/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "soroban-liquidity-pool-router-with-payload" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["export"] +export = [] +testutils = ["soroban-sdk/testutils", "soroban-auth/testutils"] + +[dependencies] +soroban-sdk = "0.0.4" +soroban-auth = "0.0.4" + +[dev_dependencies] +soroban-sdk = { version = "0.0.4", features = ["testutils"] } +soroban-auth = { version = "0.0.4", features = ["testutils"] } diff --git a/liquidity_pool_router_with_payload/src/auth.rs b/liquidity_pool_router_with_payload/src/auth.rs new file mode 100644 index 00000000..ffae1d80 --- /dev/null +++ b/liquidity_pool_router_with_payload/src/auth.rs @@ -0,0 +1,137 @@ +use soroban_auth::{AccountSignatures, Ed25519Signature, Signature}; +use soroban_sdk::serde::Serialize; +use soroban_sdk::{contracttype, Account, Bytes, BytesN, Env, IntoVal, RawVal, Symbol, Vec}; + +#[derive(Clone)] +#[contracttype] +pub struct SaltedSignaturePayloadV0 { + pub function: Symbol, + pub contract: BytesN<32>, + pub network: Bytes, + pub args: Vec, + pub salt: RawVal, +} + +#[derive(Clone)] +#[contracttype] +pub enum SaltedSignaturePayload { + V0(SaltedSignaturePayloadV0), +} + +fn verify_ed25519_signature( + env: &Env, + auth: Ed25519Signature, + function: Symbol, + args: Vec, + salt: RawVal, +) { + let msg = SaltedSignaturePayloadV0 { + function, + contract: env.get_current_contract(), + network: env.ledger().network_passphrase(), + args, + salt, + }; + let msg_bin = SaltedSignaturePayload::V0(msg).serialize(env); + + env.verify_sig_ed25519(auth.public_key, msg_bin, auth.signature); +} + +fn verify_account_signatures( + env: &Env, + auth: AccountSignatures, + function: Symbol, + args: Vec, + salt: RawVal, +) { + let acc = Account::from_public_key(&auth.account_id).unwrap(); + + let msg = SaltedSignaturePayloadV0 { + function, + contract: env.get_current_contract(), + network: env.ledger().network_passphrase(), + args, + salt, + }; + let msg_bytes = SaltedSignaturePayload::V0(msg).serialize(env); + + let threshold = acc.medium_threshold(); + let mut weight = 0u32; + + let sigs = &auth.signatures; + let mut prev_pk: Option> = None; + for sig in sigs.iter().map(Result::unwrap) { + // Cannot take multiple signatures from the same key + if let Some(prev) = prev_pk { + if prev == sig.public_key { + panic!("signature duplicate") + } + if prev > sig.public_key { + panic!("signature out of order") + } + } + + env.verify_sig_ed25519( + sig.public_key.clone(), + msg_bytes.clone(), + sig.signature.clone(), + ); + + weight = weight + .checked_add(acc.signer_weight(&sig.public_key)) + .expect("weight overflow"); + + prev_pk = Some(sig.public_key); + } + + if weight < threshold { + panic!("insufficient signing weight") + } +} + +/// Verify that a [`Signature`] is a valid signature of a [`SignaturePayload`] +/// containing the provided arguments by the [`Identifier`] contained within the +/// [`Signature`]. +/// +/// Verify that the given signature is a signature of the [`SignaturePayload`] +/// that contain `function`, and `args`. +/// +/// Three types of signature are accepted: +/// +/// - Contract Signature +/// +/// An invoking contract can sign the message by simply making the invocation. +/// No actual signature of [`SignaturePayload`] is required. +/// +/// - Ed25519 Signature +/// +/// An ed25519 key can sign [`SignaturePayload`] and include that signature in +/// the `sig` field. +/// +/// - Account Signatures +/// +/// An account's signers can sign [`SignaturePayload`] and include those +/// signatures in the `sig` field. +/// +/// **This function provides no replay protection. Contracts must provide their +/// own mechanism suitable for replay prevention that prevents contract +/// invocations to be replayable if it is important they are not.** +pub fn verify( + env: &Env, + sig: Signature, + function: Symbol, + args: impl IntoVal>, + salt: RawVal, +) { + match sig { + Signature::Contract => { + env.get_invoking_contract(); + } + Signature::Ed25519(e) => { + verify_ed25519_signature(env, e, function, args.into_val(env), salt) + } + Signature::Account(a) => { + verify_account_signatures(env, a, function, args.into_val(env), salt) + } + } +} diff --git a/liquidity_pool_router_with_payload/src/lib.rs b/liquidity_pool_router_with_payload/src/lib.rs new file mode 100644 index 00000000..8598c562 --- /dev/null +++ b/liquidity_pool_router_with_payload/src/lib.rs @@ -0,0 +1,242 @@ +#![no_std] + +mod auth; +mod pool_contract; +mod token_contract; + +pub use crate::token_contract::{SaltedSignaturePayload, SaltedSignaturePayloadV0}; +use soroban_auth::{Identifier, Signature}; +use soroban_sdk::{ + contractimpl, contracttype, symbol, BigInt, Bytes, BytesN, Env, IntoVal, Map, RawVal, Symbol, + TryIntoVal, Vec, +}; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Pool(BytesN<32>), +} + +#[derive(Clone)] +#[contracttype] +pub struct Deposit { + pub token: BytesN<32>, + pub nonce: BigInt, + pub desired: BigInt, + pub min: BigInt, +} + +fn pool_salt(e: &Env, token_a: &BytesN<32>, token_b: &BytesN<32>) -> BytesN<32> { + if token_a >= token_b { + panic!("token_a must be less t&han token_b"); + } + + let mut salt_bin = Bytes::new(&e); + salt_bin.append(&token_a.clone().into()); + salt_bin.append(&token_b.clone().into()); + e.compute_hash_sha256(salt_bin) +} + +fn get_pool_id(e: &Env, salt: &BytesN<32>) -> BytesN<32> { + e.contract_data() + .get_unchecked(DataKey::Pool(salt.clone())) + .unwrap() +} + +fn put_pool(e: &Env, salt: &BytesN<32>, pool: &BytesN<32>) { + e.contract_data() + .set(DataKey::Pool(salt.clone()), pool.clone()) +} + +fn has_pool(e: &Env, salt: &BytesN<32>) -> bool { + e.contract_data().has(DataKey::Pool(salt.clone())) +} + +fn get_deposit_amounts( + desired_a: BigInt, + min_a: BigInt, + desired_b: BigInt, + min_b: BigInt, + reserves: (BigInt, BigInt), +) -> (BigInt, BigInt) { + if reserves.0 == 0 && reserves.1 == 0 { + return (desired_a, desired_b); + } + + let amount_b = desired_a.clone() * reserves.1.clone() / reserves.0.clone(); + if amount_b <= desired_b { + if amount_b < min_b { + panic!("amount_b less than min") + } + return (desired_a, amount_b); + } else { + let amount_a = desired_b.clone() * reserves.0 / reserves.1; + if amount_a > desired_a || desired_a < min_a { + panic!("amount_a invalid") + } + return (amount_a, desired_b); + } +} + +pub trait LiquidityPoolRouterTrait { + fn deposit( + e: Env, + to: Identifier, + deposit_a: Deposit, + deposit_b: Deposit, + sigs: Map, + ); +} + +pub struct LiquidityPoolRouter; + +impl LiquidityPoolRouterTrait for LiquidityPoolRouter { + fn deposit( + e: Env, + to: Identifier, + deposit_a: Deposit, + deposit_b: Deposit, + sigs: Map, + ) { + crate::auth::verify( + &e, + sigs.get_unchecked(symbol!("deposit")).unwrap(), + symbol!("deposit"), + (&to, &deposit_a, &deposit_b), + e.get_current_call_stack().into(), + ); + + let salt = pool_salt(&e, &deposit_a.token, &deposit_b.token); + if !has_pool(&e, &salt) { + let pool_id = pool_contract::create_contract(&e, &salt); + put_pool(&e, &salt, &pool_id); + pool_contract::Client::new(&e, &pool_id).initialize(&deposit_a.token, &deposit_b.token); + } + let pool_id = get_pool_id(&e, &salt); + let pool_client = pool_contract::Client::new(&e, &pool_id); + + let reserves = pool_client.get_rsrvs(); + let amounts = get_deposit_amounts( + deposit_a.desired, + deposit_a.min, + deposit_b.desired, + deposit_b.min, + reserves, + ); + + let mut sigs_a = Map::new(&e); + sigs_a.set( + symbol!("sig"), + sigs.get_unchecked(symbol!("xfer_a")).unwrap(), + ); + let mut sigs_b = Map::new(&e); + sigs_b.set( + symbol!("sig"), + sigs.get_unchecked(symbol!("xfer_b")).unwrap(), + ); + + token_contract::Client::new(&e, deposit_a.token).xfer( + &to, + &deposit_a.nonce, + &Identifier::Contract(e.get_current_contract()), + &amounts.0, + &sigs_a, + ); + token_contract::Client::new(&e, deposit_b.token).xfer( + &to, + &deposit_b.nonce, + &Identifier::Contract(e.get_current_contract()), + &amounts.1, + &sigs_b, + ); + + pool_client.deposit(&to); + } +} + +pub trait PayloadTrait { + fn has_sig(e: Env, function: Symbol) -> bool; + + fn payload( + e: Env, + function: Symbol, + args: Vec, + callstack: Vec<(BytesN<32>, Symbol)>, + ) -> Map; +} + +pub struct LiquidityPoolRouterPayload; + +#[contractimpl] +impl PayloadTrait for LiquidityPoolRouterPayload { + fn has_sig(_e: Env, function: Symbol) -> bool { + const DEPOSIT_RAW: u64 = symbol!("deposit").to_raw().get_payload(); + match function.to_raw().get_payload() { + DEPOSIT_RAW => true, + _ => false, + } + } + + fn payload( + e: Env, + function: Symbol, + args: Vec, + callstack: Vec<(BytesN<32>, Symbol)>, + ) -> Map { + const DEPOSIT_RAW: u64 = symbol!("deposit").to_raw().get_payload(); + match function.to_raw().get_payload() { + DEPOSIT_RAW => { + let to: Identifier = args.get_unchecked(0).unwrap().try_into_val(&e).unwrap(); + let deposit_a: Deposit = args.get_unchecked(1).unwrap().try_into_val(&e).unwrap(); + let deposit_b: Deposit = args.get_unchecked(2).unwrap().try_into_val(&e).unwrap(); + + let pool_id = get_pool_id(&e, &pool_salt(&e, &deposit_a.token, &deposit_b.token)); + let reserves = pool_contract::Client::new(&e, &pool_id).get_rsrvs(); + let amounts = get_deposit_amounts( + deposit_a.desired, + deposit_a.min, + deposit_b.desired, + deposit_b.min, + reserves, + ); + + let mut callstack_a = callstack.clone(); + callstack_a.push_back((deposit_a.token.clone(), symbol!("xfer"))); + let to_forward_a = token_contract::Client::new(&e, &deposit_a.token) + .payload( + &symbol!("xfer"), + &(&to, deposit_a.nonce, e.get_current_contract(), amounts.0).into_val(&e), + &callstack_a, + ) + .get_unchecked(symbol!("sig")) + .unwrap(); + + let mut callstack_b = callstack.clone(); + callstack_b.push_back((deposit_b.token.clone(), symbol!("xfer"))); + let to_forward_b = token_contract::Client::new(&e, &deposit_b.token) + .payload( + &symbol!("xfer"), + &(&to, deposit_b.nonce, e.get_current_contract(), amounts.1).into_val(&e), + &callstack_b, + ) + .get_unchecked(symbol!("sig")) + .unwrap(); + + let to_verify = SaltedSignaturePayload::V0(SaltedSignaturePayloadV0 { + function, + contract: e.get_current_contract(), + network: e.ledger().network_passphrase(), + args, + salt: callstack.into(), + }); + + let mut res = Map::new(&e); + res.set(symbol!("xfer_a"), to_forward_a); + res.set(symbol!("xfer_b"), to_forward_b); + res.set(symbol!("deposit"), (to, to_verify)); + } + _ => panic!(), + } + todo!() + } +} diff --git a/liquidity_pool_router_with_payload/src/pool_contract.rs b/liquidity_pool_router_with_payload/src/pool_contract.rs new file mode 100644 index 00000000..06467382 --- /dev/null +++ b/liquidity_pool_router_with_payload/src/pool_contract.rs @@ -0,0 +1,12 @@ +use soroban_sdk::{BytesN, Env}; + +soroban_sdk::contractimport!( + file = "../target/wasm32-unknown-unknown/release/soroban_liquidity_pool_contract.wasm" +); +pub type Client = ContractClient; + +pub fn create_contract(e: &Env, salt: &BytesN<32>) -> BytesN<32> { + use soroban_sdk::Bytes; + let bin = Bytes::from_slice(e, WASM); + e.deployer().from_current_contract(salt).deploy(bin) +} diff --git a/liquidity_pool_router_with_payload/src/token_contract.rs b/liquidity_pool_router_with_payload/src/token_contract.rs new file mode 100644 index 00000000..9eb9ef3d --- /dev/null +++ b/liquidity_pool_router_with_payload/src/token_contract.rs @@ -0,0 +1,4 @@ +soroban_sdk::contractimport!( + file = "../target/wasm32-unknown-unknown/release/soroban_token_with_payload.wasm" +); +pub type Client = ContractClient; diff --git a/token_with_payload/Cargo.toml b/token_with_payload/Cargo.toml new file mode 100644 index 00000000..3990f331 --- /dev/null +++ b/token_with_payload/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "soroban-token-with-payload" +description = "Soroban token with payloads" +version = "0.0.4" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["export"] +export = [] +testutils = ["soroban-sdk/testutils", "soroban-auth/testutils", "dep:ed25519-dalek"] + +[dependencies] +ed25519-dalek = { version = "1.0.1", optional = true } +num-bigint = { version = "0.4", optional = true } +soroban-sdk = { version = "0.0.4" } +soroban-auth = { version = "0.0.4" } + +[dev-dependencies] +soroban-sdk = { version = "0.0.4", features = ["testutils"] } +soroban-auth = { version = "0.0.4", features = ["testutils"] } +ed25519-dalek = { version = "1.0.1" } +rand = { version = "0.7.3" } diff --git a/token_with_payload/src/admin.rs b/token_with_payload/src/admin.rs new file mode 100644 index 00000000..493c4bb1 --- /dev/null +++ b/token_with_payload/src/admin.rs @@ -0,0 +1,25 @@ +use crate::storage_types::DataKey; +use soroban_auth::{Identifier, Signature}; +use soroban_sdk::Env; + +pub fn has_administrator(e: &Env) -> bool { + let key = DataKey::Admin; + e.contract_data().has(key) +} + +fn read_administrator(e: &Env) -> Identifier { + let key = DataKey::Admin; + e.contract_data().get_unchecked(key).unwrap() +} + +pub fn write_administrator(e: &Env, id: Identifier) { + let key = DataKey::Admin; + e.contract_data().set(key, id); +} + +pub fn check_admin(e: &Env, auth: &Signature) { + let auth_id = auth.get_identifier(&e); + if auth_id != read_administrator(&e) { + panic!("not authorized by admin") + } +} diff --git a/token_with_payload/src/allowance.rs b/token_with_payload/src/allowance.rs new file mode 100644 index 00000000..ba916534 --- /dev/null +++ b/token_with_payload/src/allowance.rs @@ -0,0 +1,25 @@ +use crate::storage_types::{AllowanceDataKey, DataKey}; +use soroban_auth::Identifier; +use soroban_sdk::{BigInt, Env}; + +pub fn read_allowance(e: &Env, from: Identifier, spender: Identifier) -> BigInt { + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + if let Some(allowance) = e.contract_data().get(key) { + allowance.unwrap() + } else { + BigInt::zero(e) + } +} + +pub fn write_allowance(e: &Env, from: Identifier, spender: Identifier, amount: BigInt) { + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + e.contract_data().set(key, amount); +} + +pub fn spend_allowance(e: &Env, from: Identifier, spender: Identifier, amount: BigInt) { + let allowance = read_allowance(e, from.clone(), spender.clone()); + if allowance < amount { + panic!("insufficient allowance"); + } + write_allowance(e, from, spender, allowance - amount); +} diff --git a/token_with_payload/src/auth.rs b/token_with_payload/src/auth.rs new file mode 100644 index 00000000..ffae1d80 --- /dev/null +++ b/token_with_payload/src/auth.rs @@ -0,0 +1,137 @@ +use soroban_auth::{AccountSignatures, Ed25519Signature, Signature}; +use soroban_sdk::serde::Serialize; +use soroban_sdk::{contracttype, Account, Bytes, BytesN, Env, IntoVal, RawVal, Symbol, Vec}; + +#[derive(Clone)] +#[contracttype] +pub struct SaltedSignaturePayloadV0 { + pub function: Symbol, + pub contract: BytesN<32>, + pub network: Bytes, + pub args: Vec, + pub salt: RawVal, +} + +#[derive(Clone)] +#[contracttype] +pub enum SaltedSignaturePayload { + V0(SaltedSignaturePayloadV0), +} + +fn verify_ed25519_signature( + env: &Env, + auth: Ed25519Signature, + function: Symbol, + args: Vec, + salt: RawVal, +) { + let msg = SaltedSignaturePayloadV0 { + function, + contract: env.get_current_contract(), + network: env.ledger().network_passphrase(), + args, + salt, + }; + let msg_bin = SaltedSignaturePayload::V0(msg).serialize(env); + + env.verify_sig_ed25519(auth.public_key, msg_bin, auth.signature); +} + +fn verify_account_signatures( + env: &Env, + auth: AccountSignatures, + function: Symbol, + args: Vec, + salt: RawVal, +) { + let acc = Account::from_public_key(&auth.account_id).unwrap(); + + let msg = SaltedSignaturePayloadV0 { + function, + contract: env.get_current_contract(), + network: env.ledger().network_passphrase(), + args, + salt, + }; + let msg_bytes = SaltedSignaturePayload::V0(msg).serialize(env); + + let threshold = acc.medium_threshold(); + let mut weight = 0u32; + + let sigs = &auth.signatures; + let mut prev_pk: Option> = None; + for sig in sigs.iter().map(Result::unwrap) { + // Cannot take multiple signatures from the same key + if let Some(prev) = prev_pk { + if prev == sig.public_key { + panic!("signature duplicate") + } + if prev > sig.public_key { + panic!("signature out of order") + } + } + + env.verify_sig_ed25519( + sig.public_key.clone(), + msg_bytes.clone(), + sig.signature.clone(), + ); + + weight = weight + .checked_add(acc.signer_weight(&sig.public_key)) + .expect("weight overflow"); + + prev_pk = Some(sig.public_key); + } + + if weight < threshold { + panic!("insufficient signing weight") + } +} + +/// Verify that a [`Signature`] is a valid signature of a [`SignaturePayload`] +/// containing the provided arguments by the [`Identifier`] contained within the +/// [`Signature`]. +/// +/// Verify that the given signature is a signature of the [`SignaturePayload`] +/// that contain `function`, and `args`. +/// +/// Three types of signature are accepted: +/// +/// - Contract Signature +/// +/// An invoking contract can sign the message by simply making the invocation. +/// No actual signature of [`SignaturePayload`] is required. +/// +/// - Ed25519 Signature +/// +/// An ed25519 key can sign [`SignaturePayload`] and include that signature in +/// the `sig` field. +/// +/// - Account Signatures +/// +/// An account's signers can sign [`SignaturePayload`] and include those +/// signatures in the `sig` field. +/// +/// **This function provides no replay protection. Contracts must provide their +/// own mechanism suitable for replay prevention that prevents contract +/// invocations to be replayable if it is important they are not.** +pub fn verify( + env: &Env, + sig: Signature, + function: Symbol, + args: impl IntoVal>, + salt: RawVal, +) { + match sig { + Signature::Contract => { + env.get_invoking_contract(); + } + Signature::Ed25519(e) => { + verify_ed25519_signature(env, e, function, args.into_val(env), salt) + } + Signature::Account(a) => { + verify_account_signatures(env, a, function, args.into_val(env), salt) + } + } +} diff --git a/token_with_payload/src/balance.rs b/token_with_payload/src/balance.rs new file mode 100644 index 00000000..4e4e9abc --- /dev/null +++ b/token_with_payload/src/balance.rs @@ -0,0 +1,52 @@ +use crate::storage_types::DataKey; +use soroban_auth::Identifier; +use soroban_sdk::{BigInt, Env}; + +pub fn read_balance(e: &Env, id: Identifier) -> BigInt { + let key = DataKey::Balance(id); + if let Some(balance) = e.contract_data().get(key) { + balance.unwrap() + } else { + BigInt::zero(e) + } +} + +fn write_balance(e: &Env, id: Identifier, amount: BigInt) { + let key = DataKey::Balance(id); + e.contract_data().set(key, amount); +} + +pub fn receive_balance(e: &Env, id: Identifier, amount: BigInt) { + let balance = read_balance(e, id.clone()); + let is_frozen = read_state(e, id.clone()); + if is_frozen { + panic!("can't receive when frozen"); + } + write_balance(e, id, balance + amount); +} + +pub fn spend_balance(e: &Env, id: Identifier, amount: BigInt) { + let balance = read_balance(e, id.clone()); + let is_frozen = read_state(e, id.clone()); + if is_frozen { + panic!("can't spend when frozen"); + } + if balance < amount { + panic!("insufficient balance"); + } + write_balance(e, id, balance - amount); +} + +pub fn read_state(e: &Env, id: Identifier) -> bool { + let key = DataKey::State(id); + if let Some(state) = e.contract_data().get(key) { + state.unwrap() + } else { + false + } +} + +pub fn write_state(e: &Env, id: Identifier, is_frozen: bool) { + let key = DataKey::State(id); + e.contract_data().set(key, is_frozen); +} diff --git a/token_with_payload/src/contract.rs b/token_with_payload/src/contract.rs new file mode 100644 index 00000000..23168bc9 --- /dev/null +++ b/token_with_payload/src/contract.rs @@ -0,0 +1,341 @@ +use crate::admin::{check_admin, has_administrator, write_administrator}; +use crate::allowance::{read_allowance, spend_allowance, write_allowance}; +use crate::auth::{SaltedSignaturePayload, SaltedSignaturePayloadV0}; +use crate::balance::{read_balance, receive_balance, spend_balance}; +use crate::balance::{read_state, write_state}; +use crate::metadata::{ + read_decimal, read_name, read_symbol, write_decimal, write_name, write_symbol, +}; +use crate::storage_types::DataKey; +use soroban_auth::check_auth; +use soroban_auth::{Identifier, Signature}; +use soroban_sdk::{contractimpl, symbol, BigInt, Bytes, Env, IntoVal}; + +use soroban_sdk::{BytesN, Map, RawVal, Symbol, TryIntoVal, Vec}; + +pub trait TokenTrait { + fn initialize(e: Env, admin: Identifier, decimal: u32, name: Bytes, symbol: Bytes); + + fn nonce(e: Env, id: Identifier) -> BigInt; + + fn allowance(e: Env, from: Identifier, spender: Identifier) -> BigInt; + + fn approve( + e: Env, + from: Identifier, + nonce: BigInt, + spender: Identifier, + amount: BigInt, + sigs: Map, + ); + + fn balance(e: Env, id: Identifier) -> BigInt; + + fn is_frozen(e: Env, id: Identifier) -> bool; + + fn xfer( + e: Env, + from: Identifier, + nonce: BigInt, + to: Identifier, + amount: BigInt, + sigs: Map, + ); + + fn xfer_from( + e: Env, + spender: Signature, + nonce: BigInt, + from: Identifier, + to: Identifier, + amount: BigInt, + ); + + fn burn(e: Env, admin: Signature, nonce: BigInt, from: Identifier, amount: BigInt); + + fn freeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier); + + fn mint(e: Env, admin: Signature, nonce: BigInt, to: Identifier, amount: BigInt); + + fn set_admin(e: Env, admin: Signature, nonce: BigInt, new_admin: Identifier); + + fn unfreeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier); + + fn decimals(e: Env) -> u32; + + fn name(e: Env) -> Bytes; + + fn symbol(e: Env) -> Bytes; +} + +fn read_nonce(e: &Env, id: &Identifier) -> BigInt { + let key = DataKey::Nonce(id.clone()); + e.contract_data() + .get(key) + .unwrap_or_else(|| Ok(BigInt::zero(e))) + .unwrap() +} + +fn verify_and_consume_nonce(e: &Env, id: &Identifier, expected_nonce: &BigInt) { + match id { + Identifier::Contract(_) => { + if BigInt::zero(&e) != expected_nonce { + panic!("nonce should be zero for Contract") + } + return; + } + _ => {} + } + + let key = DataKey::Nonce(id.clone()); + let nonce = read_nonce(e, id); + + if nonce != expected_nonce { + panic!("incorrect nonce") + } + e.contract_data().set(key, &nonce + 1); +} + +pub struct Token; + +#[cfg_attr(feature = "export", contractimpl)] +#[cfg_attr(not(feature = "export"), contractimpl(export = false))] +impl TokenTrait for Token { + fn initialize(e: Env, admin: Identifier, decimal: u32, name: Bytes, symbol: Bytes) { + if has_administrator(&e) { + panic!("already initialized") + } + write_administrator(&e, admin); + + write_decimal(&e, u8::try_from(decimal).expect("Decimal must fit in a u8")); + write_name(&e, name); + write_symbol(&e, symbol); + } + + fn nonce(e: Env, id: Identifier) -> BigInt { + read_nonce(&e, &id) + } + + fn allowance(e: Env, from: Identifier, spender: Identifier) -> BigInt { + read_allowance(&e, from, spender) + } + + fn approve( + e: Env, + from: Identifier, + nonce: BigInt, + spender: Identifier, + amount: BigInt, + sigs: Map, + ) { + let from_sig = sigs.get_unchecked(symbol!("from")).unwrap(); + if from != from_sig.get_identifier(&e) { + panic!(); + } + + verify_and_consume_nonce(&e, &from, &nonce); + + crate::auth::verify( + &e, + from_sig, + symbol!("approve"), + (&from, nonce, &spender, &amount), + e.get_current_call_stack().into_val(&e), + ); + write_allowance(&e, from, spender, amount); + } + + fn balance(e: Env, id: Identifier) -> BigInt { + read_balance(&e, id) + } + + fn is_frozen(e: Env, id: Identifier) -> bool { + read_state(&e, id) + } + + fn xfer( + e: Env, + from: Identifier, + nonce: BigInt, + to: Identifier, + amount: BigInt, + sigs: Map, + ) { + let from_sig = sigs.get_unchecked(symbol!("from")).unwrap(); + if from != from_sig.get_identifier(&e) { + panic!(); + } + + verify_and_consume_nonce(&e, &from, &nonce); + + check_auth( + &e, + &from_sig, + symbol!("xfer"), + (&from, nonce, &to, &amount).into_val(&e), + ); + spend_balance(&e, from, amount.clone()); + receive_balance(&e, to, amount); + } + + fn xfer_from( + e: Env, + spender: Signature, + nonce: BigInt, + from: Identifier, + to: Identifier, + amount: BigInt, + ) { + let spender_id = spender.get_identifier(&e); + + verify_and_consume_nonce(&e, &spender_id, &nonce); + + check_auth( + &e, + &spender, + symbol!("xfer_from"), + (&spender_id, nonce, &from, &to, &amount).into_val(&e), + ); + spend_allowance(&e, from.clone(), spender_id, amount.clone()); + spend_balance(&e, from, amount.clone()); + receive_balance(&e, to, amount); + } + + fn burn(e: Env, admin: Signature, nonce: BigInt, from: Identifier, amount: BigInt) { + check_admin(&e, &admin); + let admin_id = admin.get_identifier(&e); + + verify_and_consume_nonce(&e, &admin_id, &nonce); + + check_auth( + &e, + &admin, + symbol!("burn"), + (admin_id, nonce, &from, &amount).into_val(&e), + ); + spend_balance(&e, from, amount); + } + + fn freeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier) { + check_admin(&e, &admin); + let admin_id = admin.get_identifier(&e); + + verify_and_consume_nonce(&e, &admin_id, &nonce); + + check_auth( + &e, + &admin, + symbol!("freeze"), + (admin_id, nonce, &id).into_val(&e), + ); + write_state(&e, id, true); + } + + fn mint(e: Env, admin: Signature, nonce: BigInt, to: Identifier, amount: BigInt) { + check_admin(&e, &admin); + let admin_id = admin.get_identifier(&e); + + verify_and_consume_nonce(&e, &admin_id, &nonce); + + check_auth( + &e, + &admin, + symbol!("mint"), + (admin_id, nonce, &to, &amount).into_val(&e), + ); + receive_balance(&e, to, amount); + } + + fn set_admin(e: Env, admin: Signature, nonce: BigInt, new_admin: Identifier) { + check_admin(&e, &admin); + let admin_id = admin.get_identifier(&e); + + verify_and_consume_nonce(&e, &admin_id, &nonce); + + check_auth( + &e, + &admin, + symbol!("set_admin"), + (admin_id, nonce, &new_admin).into_val(&e), + ); + write_administrator(&e, new_admin); + } + + fn unfreeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier) { + check_admin(&e, &admin); + let admin_id = admin.get_identifier(&e); + + verify_and_consume_nonce(&e, &admin_id, &nonce); + + check_auth( + &e, + &admin, + symbol!("unfreeze"), + (admin_id, nonce, &id).into_val(&e), + ); + write_state(&e, id, false); + } + + fn decimals(e: Env) -> u32 { + read_decimal(&e) + } + + fn name(e: Env) -> Bytes { + read_name(&e) + } + + fn symbol(e: Env) -> Bytes { + read_symbol(&e) + } +} + +pub trait PayloadTrait { + fn has_sig(e: Env, function: Symbol) -> bool; + + fn payload( + e: Env, + function: Symbol, + args: Vec, + callstack: Vec<(BytesN<32>, Symbol)>, + ) -> Map; +} + +pub struct TokenPayload; + +#[cfg_attr(feature = "export", contractimpl)] +#[cfg_attr(not(feature = "export"), contractimpl(export = false))] +impl PayloadTrait for TokenPayload { + fn has_sig(_e: Env, function: Symbol) -> bool { + const APPROVE_RAW: u64 = symbol!("approve").to_raw().get_payload(); + const XFER_RAW: u64 = symbol!("xfer").to_raw().get_payload(); + match function.to_raw().get_payload() { + APPROVE_RAW | XFER_RAW => true, + _ => false, + } + } + + fn payload( + e: Env, + function: Symbol, + args: Vec, + callstack: Vec<(BytesN<32>, Symbol)>, + ) -> Map { + let to_verify = SaltedSignaturePayload::V0(SaltedSignaturePayloadV0 { + function, + contract: e.get_current_contract(), + network: e.ledger().network_passphrase(), + args: args.clone(), + salt: callstack.into(), + }); + + let mut res = Map::new(&e); + res.set( + symbol!("sig"), + ( + args.get_unchecked(0).unwrap().try_into_val(&e).unwrap(), + to_verify, + ), + ); + res + } +} diff --git a/token_with_payload/src/lib.rs b/token_with_payload/src/lib.rs new file mode 100644 index 00000000..0b75ab78 --- /dev/null +++ b/token_with_payload/src/lib.rs @@ -0,0 +1,11 @@ +#![no_std] + +mod admin; +mod allowance; +mod auth; +mod balance; +mod contract; +mod metadata; +mod storage_types; + +pub use crate::contract::TokenClient; diff --git a/token_with_payload/src/metadata.rs b/token_with_payload/src/metadata.rs new file mode 100644 index 00000000..0a90edbb --- /dev/null +++ b/token_with_payload/src/metadata.rs @@ -0,0 +1,32 @@ +use crate::storage_types::DataKey; +use soroban_sdk::{Bytes, Env}; + +pub fn read_decimal(e: &Env) -> u32 { + let key = DataKey::Decimals; + e.contract_data().get_unchecked(key.clone()).unwrap() +} + +pub fn write_decimal(e: &Env, d: u8) { + let key = DataKey::Decimals; + e.contract_data().set(key, u32::from(d)) +} + +pub fn read_name(e: &Env) -> Bytes { + let key = DataKey::Name; + e.contract_data().get_unchecked(key.clone()).unwrap() +} + +pub fn write_name(e: &Env, d: Bytes) { + let key = DataKey::Name; + e.contract_data().set(key, d) +} + +pub fn read_symbol(e: &Env) -> Bytes { + let key = DataKey::Symbol; + e.contract_data().get_unchecked(key.clone()).unwrap() +} + +pub fn write_symbol(e: &Env, d: Bytes) { + let key = DataKey::Symbol; + e.contract_data().set(key, d) +} diff --git a/token_with_payload/src/storage_types.rs b/token_with_payload/src/storage_types.rs new file mode 100644 index 00000000..24750fec --- /dev/null +++ b/token_with_payload/src/storage_types.rs @@ -0,0 +1,22 @@ +use soroban_auth::Identifier; +use soroban_sdk::contracttype; + +#[derive(Clone)] +#[contracttype] +pub struct AllowanceDataKey { + pub from: Identifier, + pub spender: Identifier, +} + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Allowance(AllowanceDataKey), + Balance(Identifier), + Nonce(Identifier), + State(Identifier), + Admin, + Decimals, + Name, + Symbol, +}