diff --git a/docs/getting-started/storing-data.mdx b/docs/getting-started/storing-data.mdx index 6a38e936..81136323 100644 --- a/docs/getting-started/storing-data.mdx +++ b/docs/getting-started/storing-data.mdx @@ -6,18 +6,18 @@ title: 2. Storing Data The [increment example] demonstrates how to write a simple contract that stores data, with a single function that increments an internal counter and returns the value. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 -[increment example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/increment +[increment example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/increment ## Run the Example -First go through the [Setup] process to get your development environment configured, then clone the `v0.4.2` tag of `soroban-examples` repository: +First go through the [Setup] process to get your development environment configured, then clone the `v0.5.0` tag of `soroban-examples` repository: [Setup]: setup.mdx ``` -git clone -b v0.4.2 https://github.com/stellar/soroban-examples +git clone -b v0.5.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [Gitpod][oigp]. @@ -62,7 +62,7 @@ impl IncrementContract { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/v0.4.2/increment +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/increment ## How it Works diff --git a/docs/how-to-guides/alloc.mdx b/docs/how-to-guides/alloc.mdx index c7c6703b..5802e400 100644 --- a/docs/how-to-guides/alloc.mdx +++ b/docs/how-to-guides/alloc.mdx @@ -1,25 +1,25 @@ --- -sidebar_position: 15 +sidebar_position: 10 title: Allocator --- The [allocator example] demonstrates how to utilize the allocator feature when writing a contract. -[allocator example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/alloc +[allocator example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/alloc [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 The `soroban-sdk` crate provides a lightweight bump-pointer allocator which can be used to emulate heap memory allocation in a WASM smart contract. ## Run the Example -First go through the [Setup] process to get your development environment configured, then clone the `v0.4.2` tag of `soroban-examples` repository: +First go through the [Setup] process to get your development environment configured, then clone the `v0.5.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ``` -git clone -b v0.4.2 https://github.com/stellar/soroban-examples +git clone -b v0.5.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [Gitpod][oigp]. @@ -77,7 +77,7 @@ impl AllocContract { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/v0.4.2/alloc +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/alloc ## How it Works diff --git a/docs/how-to-guides/atomic-multi-swap.mdx b/docs/how-to-guides/atomic-multi-swap.mdx new file mode 100644 index 00000000..98a0ba83 --- /dev/null +++ b/docs/how-to-guides/atomic-multi-swap.mdx @@ -0,0 +1,20 @@ +--- +sidebar_position: 12 +title: Batched Atomic Swaps +--- + +The [atomic swap batching example] swaps a pair of tokens between the two groups +of users that authorized the `swap` operation from the [Atomic Swap] example. + +This contract basically batches the multiple swaps while following some simple +rules to match the swap participants. + +Follow the comments in the code for more information. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] + +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 + +[Atomic Swap]: atomic-swap.mdx + +[atomic swap batching example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/atomic_multiswap \ No newline at end of file diff --git a/docs/how-to-guides/atomic-swap.mdx b/docs/how-to-guides/atomic-swap.mdx new file mode 100644 index 00000000..fbbc6127 --- /dev/null +++ b/docs/how-to-guides/atomic-swap.mdx @@ -0,0 +1,233 @@ +--- +sidebar_position: 11 +title: Atomic Swap +--- + +The [atomic swap example] swaps two tokens between two authorized parties +atomically while following the limits they set. + +This is example demonstrates advanced usage of Soroban auth framework and +assumes the reader is familiar with the [auth example](auth.mdx) and with +Soroban token usage. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] + +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 + +[atomic swap example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/atomic_swap + +## Run the Example + +First go through the [Setup] process to get your development environment +configured, then clone the `v0.5.0` tag of `soroban-examples` repository: + +[Setup]: ../getting-started/setup.mdx + +``` +git clone -b v0.5.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example use `cargo test`. + +``` +cargo test -p soroban-atomic-swap +``` + +You should see the output: + +``` +running 1 test +test test::test_atomic_swap ... ok +``` + +## Code + +```rust title="atomic_swap/src/lib.rs" +mod token { + soroban_sdk::contractimport!(file = "../soroban_token_spec.wasm"); +} + +pub struct AtomicSwapContract; + +#[contractimpl] +impl AtomicSwapContract { + // Swap token A for token B atomically. Settle for the minimum requested price + // for each party (this is an arbitrary choice to demonstrate the usage of + // allowance; full amounts could be swapped as well). + pub fn swap( + env: Env, + a: Address, + b: Address, + token_a: BytesN<32>, + token_b: BytesN<32>, + amount_a: i128, + min_b_for_a: i128, + amount_b: i128, + min_a_for_b: i128, + ) { + // Verify preconditions on the minimum price for both parties. + if amount_b < min_b_for_a { + panic!("not enough token B for token A"); + } + if amount_a < min_a_for_b { + panic!("not enough token A for token B"); + } + // Require authorization for a subset of arguments specific to a party. + // Notice, that arguments are symmetric - there is no difference between + // `a` and `b` in the call and hence their signatures can be used + // either for `a` or for `b` role. + a.require_auth_for_args( + (token_a.clone(), token_b.clone(), amount_a, min_b_for_a).into_val(&env), + ); + b.require_auth_for_args( + (token_b.clone(), token_a.clone(), amount_b, min_a_for_b).into_val(&env), + ); + + // Perform the swap via two token transfers. + move_token(&env, token_a, &a, &b, amount_a, min_a_for_b); + move_token(&env, token_b, &b, &a, amount_b, min_b_for_a); + } +} + +fn move_token( + env: &Env, + token: BytesN<32>, + from: &Address, + to: &Address, + approve_amount: i128, + xfer_amount: i128, +) { + let token = token::Client::new(&env, &token); + let contract_address = env.current_contract_address(); + // This call needs to be authorized by `from` address. Since it increases + // the allowance on behalf of the contract, `from` doesn't need to know `to` + // at the signature time. + token.incr_allow(&from, &contract_address, &approve_amount); + token.xfer_from(&contract_address, &from, to, &xfer_amount); +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/atomic_swap + +## How it Works + +The example contract requires two `Address`-es to authorize their parts of the +swap operation: one `Address` wants to sell a given amount of token A for +token B at a given price and another `Address` wants to sell token B for token +A at a given price. The contract swaps the tokens atomically, but only if the +requested minimum price is respected for both parties. + +Open the `atomic_swap/src/lib.rs` file or see the code above to follow along. + +### Swap authorization + +```rust +... +a.require_auth_for_args( + (token_a.clone(), token_b.clone(), amount_a, min_b_for_a).into_val(&env), +); +b.require_auth_for_args( + (token_b.clone(), token_a.clone(), amount_b, min_a_for_b).into_val(&env), +); +... +``` + +Authorization of `swap` function leverages `require_auth_for_args` Soroban host +function. Both `a` and `b` need to authorize symmetric arguments: token they +sell, token they buy, amount of token they sell, minimum amount of token they +want to recieve. This means that `a` and `b` can be freely exchanged in the +invocation arguments (as long as the respective arguments are changed too). + +### Moving the tokens + +```rust +... +// Perform the swap via two token transfers. +move_token(&env, token_a, &a, &b, amount_a, min_a_for_b); +move_token(&env, token_b, &b, &a, amount_b, min_b_for_a); +... +fn move_token( + env: &Env, + token: BytesN<32>, + from: &Address, + to: &Address, + approve_amount: i128, + xfer_amount: i128, +) { + let token = token::Client::new(&env, &token); + let contract_address = env.current_contract_address(); + // This call needs to be authorized by `from` address. Since it increases + // the allowance on behalf of the contract, `from` doesn't need to know `to` + // at the signature time. + token.incr_allow(&from, &contract_address, &approve_amount); + token.xfer_from(&contract_address, &from, to, &xfer_amount); +} +``` + +The swap itself is implemented via two token moves: from `a` to `b` and from `b` +to `a`. The token move is implemented via allowance: the users don't need to +know each other in order to perform the swap and instead they authorize the swap +contract to spend the necessary amount of token on their behalf via +`incr_allow`. Soroban auth framework makes sure that the `incr_allow` signatures +would have the proper context and they won't be usable outside of the `swap` +contract invocation. + +### Tests + +Open the [`atomic_swap/src/test.rs`] file to follow along. + +[`atomic_swap/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v0.5.0/atomic_swap/src/test.rs + +Refer to another examples for the general information on the test setup. + +The interesting part for this example is verification of `swap` authorization: + +```rust +contract.swap( + &a, + &b, + &token_a.contract_id, + &token_b.contract_id, + &1000, + &4500, + &5000, + &950, +); + +assert_eq!( + env.recorded_top_authorizations(), + std::vec![ + ( + a.clone(), + contract.contract_id.clone(), + symbol!("swap"), + ( + token_a.contract_id.clone(), + token_b.contract_id.clone(), + 1000_i128, + 4500_i128 + ) + .into_val(&env), + ), + ( + b.clone(), + contract.contract_id.clone(), + symbol!("swap"), + ( + token_b.contract_id.clone(), + token_a.contract_id.clone(), + 5000_i128, + 950_i128 + ) + .into_val(&env), + ), + ] +); +``` + +`env.recorded_top_authorizations()` returns all the top-level authorizations. +Hence in the case of `swap` two authorizations are expected. Note, that the +element order here matches the order of `require_auth_for_args` calls in the +contract. diff --git a/docs/how-to-guides/auth-advanced.mdx b/docs/how-to-guides/auth-advanced.mdx deleted file mode 100644 index 457c6e9e..00000000 --- a/docs/how-to-guides/auth-advanced.mdx +++ /dev/null @@ -1,689 +0,0 @@ ---- -sidebar_position: 8 -title: Auth (Advanced) ---- - -The [advanced auth example] demonstrates how to write a contract that supports -multiple forms of authentication using the [soroban-auth] crate. - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 - -The example supports authentication by: - -- Being the invoker, as a transaction source account or a contract. -- Presigned invocations with account signers or ed25519 keys, similar to - [EIP-2612] and [EIP-712] in the Ethereum ecosystem. - -[EIP-2612]: https://eips.ethereum.org/EIPS/eip-2612 -[EIP-712]: https://eips.ethereum.org/EIPS/eip-712 - -:::info - -If you're just starting to explore Soroban, checkout the [auth example] that -uses a simpler form of invoker auth. - -::: - -Participants are identified with `Identifier`s that can represent an: - -- Account ID -- Contract ID -- Ed25519 key - -:::tip - -The `Identifier` type is a superset of the `Address` type used by the simpler -invoker-only auth. This means that contracts that store data using `Address` as -a key can often be updated to use `soroban-auth` without any impact to already -stored data. See the [auth example] more details. - -::: - -Participants specify how they are authenticating, and provide authentication by -including a `Signature` as an argument in the invocation. A `Signature` can be -an: - -- Invoker – An invoking address (account ID or contract ID). -- Account – An invocation presigned with account signers. -- Ed25519 – An invocation presigned with an ed25519 key. - -In this example, data is stored associated with an `Identifier` after -authorization has been verified. The contract supports auth via all methods -above. - -:::info - -This example describes a specific implementation of the general principles of -[authorization](../learn/authorization.mdx). - -::: - -:::caution - -The [soroban-auth] crate does not provide any functionality to prevent the -replay of valid signatures. This example implements a form of [replay -prevention](#verifying-the-nonce). Contracts must implement replay prevention that -is appropriate for themselves. - -::: - -[soroban-auth]: ../reference/sdks/rust-auth -[auth example]: auth.mdx -[advanced auth example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/auth_advanced - -## Run the Example - -First go through the [Setup] process to get your development environment -configured, then clone the `v0.4.2` tag of `soroban-examples` repository: - -[Setup]: ../getting-started/setup.mdx - -``` -git clone -b v0.4.2 https://github.com/stellar/soroban-examples -``` - -Or, skip the development environment setup and open this example in [Gitpod][oigp]. - -To run the tests for the example, navigate to the `auth_advanced` directory, and use `cargo test`. - -``` -cd auth_advanced -cargo test -``` - -You should see the output: - -``` -running 4 tests -test test::test_auth_with_invoker ... ok -test test::test_auth_with_ed25519 ... ok -thread 'test::test_auth_with_ed25519_wrong_signer' panicked at 'called `Result::unwrap()` on an `Err` value: HostError -Value: Status(UnknownError(0)) -... -test test::test_auth_with_ed25519_wrong_signer - should panic ... ok -thread 'test::test_auth_with_ed25519_wrong_nonce' panicked at 'called `Result::unwrap()` on an `Err` value: HostError -Value: Status(ContractError(2)) -... -test test::test_auth_with_ed25519_wrong_nonce - should panic ... ok -``` - -## Dependencies - -The example uses the Soroban auth SDK, and has the following dependencies in its -`Cargo.toml` file. - -```toml title="authorization/src/Cargo.toml -[dependencies] -soroban-sdk = "0.4.2" -soroban-auth = "0.4.2" - -[dev_dependencies] -soroban-sdk = { version = "0.4.2", features = ["testutils"] } -soroban-auth = { version = "0.4.2", features = ["testutils"] } -``` - -## Code - -```rust title="authorization/src/lib.rs" -#[contracttype] -pub enum DataKey { - Counter(Identifier), - Nonce(Identifier), -} - -#[contracterror] -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Error { - IncorrectNonceForInvoker = 1, - IncorrectNonce = 2, -} - -pub struct IncrementContract; - -#[contractimpl] -impl IncrementContract { - /// Increment increments a counter for the invoker, and returns the value. - pub fn increment(env: Env, sig: Signature, nonce: i128) -> u32 { - // Verify that the signature signs and authorizes this invocation. - let id = sig.identifier(&env); - verify(&env, &sig, symbol!("increment"), (&id, &nonce)); - - // Verify that the nonce has not been consumed to prevent replay of the - // same presigned invocation more than once. - verify_and_consume_nonce(&env, &sig, nonce); - - // Construct a key for the data being stored. Use an enum to set the - // contract up well for adding other types of data to be stored. - let key = DataKey::Counter(id); - - // Get the current count for the invoker. - let mut count: u32 = env - .storage() - .get(&key) - .unwrap_or(Ok(0)) // If no value set, assume 0. - .unwrap(); // Panic if the value of COUNTER is not u32. - - // Increment the count. - count += 1; - - // Save the count. - env.storage().set(&key, count); - - // Return the count to the caller. - count - } - - pub fn nonce(env: Env, id: Identifier) -> i128 { - get_nonce(&env, &id) - } -} - -fn verify_and_consume_nonce(env: &Env, sig: &Signature, nonce: i128) { - match sig { - Signature::Invoker => { - if nonce != 0 { - panic_with_error!(env, Error::IncorrectNonceForInvoker); - } - } - Signature::Ed25519(_) | Signature::Account(_) => { - let id = sig.identifier(env); - if nonce != get_nonce(env, &id) { - panic_with_error!(env, Error::IncorrectNonce); - } - set_nonce(env, &id, nonce + 1); - } - } -} - -fn get_nonce(env: &Env, id: &Identifier) -> i128 { - let key = DataKey::Nonce(id.clone()); - env.storage().get(key).unwrap_or(Ok(0)).unwrap() -} - -fn set_nonce(env: &Env, id: &Identifier, nonce: i128) { - let key = DataKey::Nonce(id.clone()); - env.storage().set(key, nonce); -} -``` - -Ref: https://github.com/stellar/soroban-examples/tree/v0.4.2/auth_advanced - -## How it Works - -This example contract tracks a counter for each `Identifier`. It uses the -`soroban-auth` crate to verify proof of identity through a `Signature`. How a -signature is formed is different for different types of identifier. - -### `soroban-auth` - -Follow along in the docs for [soroban-auth]. - -#### Identifiers - -The `soroban-auth` crate provides the `Identifier` type. Participants to be -authenticated are one of these. - -```rust -pub enum Identifier { - Account(AccountId), - Contract(BytesN<32>), - Ed25519(BytesN<32>), -} -``` - -The `Identifier` type is a superset of the `Address` type used by the simpler -invoker-only auth. This means that contracts that store data using `Address` as -a key can often be updated to use `soroban-auth` without any impact to already -stored data. See the [auth example] more details. - -```rust -pub enum Address { - Account(AccountId), - Contract(BytesN<32>), -} -``` - -#### Signature - -The `soroban-auth` crate provides the `Signature` type. Participants submit a -signature for the identifier they are proving. - -```rust -pub enum Signature { - Invoker, - Account(AccountSignatures), - Ed25519(Ed25519Signature), -} -``` - -`Signature` values map to `Identifier` values: - -- Invoker - - Account – Maps to `Identifier::Account`. - - Contract – Maps to `Identifier::Contract`. -- Account – Maps to `Identifier::Account`. -- Ed25519 – Maps to `Identifier::Ed25519`. - -##### Contract - -A contract identifier provides authentication of itself simply by being the -invoker of the contract. - -##### Ed25519 - -An ed25519 identifier provides a ed25519 signature using the private key -corresponding to the public key included in the signature. The signature payload -is the XDR serialized value of the `SignaturePayload` type. - -```rust -pub struct Ed25519Signature { - pub public_key: BytesN<32>, - pub signature: BytesN<64>, -} -``` - -##### Account - -An account identifier provides ed25519 signatures for each ed25519 signer of the -account. Verification works the same as verification of ed25519 signers on the -Stellar network. The total signing weight of the signers must exceed the medium -threshold of the account. The signature payload is the XDR serialized value of -the `SignaturePayload` type. - -```rust -pub struct AccountSignatures { - pub account_id: AccountId, - pub signatures: Vec, -} -``` - -#### Signature Payload - -The signature payload is the payload that ed25519 signers sign for both the -account and ed25519 signature types. - -The `network`, `contract`, and `name` fields of the payload act as a domain -separator to prevent the signature for a set of arguments for the invocation of -one function or contract being valid for another function or contract. - -The `name` field should be a value that sufficiently scopes where within a -contract the signature should be valid. In some cases it may be the name of the -function being invoked. It could be some other value depending on the needs of -the contract. - -```rust -pub enum SignaturePayload { - V0(SignaturePayloadV0), -} - -pub struct SignaturePayloadV0 { - pub network: Bytes, - pub contract: BytesN<32>, - pub name: Symbol, - pub args: Vec, -} -``` - -### Contract - -Open the `auth_advanced/src/lib.rs` file to follow along. - -#### Data - -The example stores two types of data, and uses an enum to distinguish keys -for the two types of data scoped by an `Identifier`. - -Each identifier will store a counter value, which is the value being incremented -by the invocation. - -Each identifier will store a nonce value, which is the next expected nonce and -will be used for replay prevention. - -```rust -#[contracttype] -pub enum DataKey { - Counter(Identifier), - Nonce(Identifier), -} -``` - -#### Verify - -The `increment` contract function increments a number for each `Identifier` -stored with key `DataKey::Counter`. - -The `soroban_auth::verify` method verifies the input `Signature`. - -```rust -pub fn increment(env: Env, sig: Signature, nonce: i128) -> u32 { - let id = sig.identifier(&env); - verify(&env, &sig, symbol!("increment"), (&id, &nonce)); - // ... -} -``` - -The function inputs are: - -- `nonce` – Included in the arguments of the function to prevent replay. -- `sig` – The `Signature` that proves the `Identifier` it contains. The contract - -The verification inputs are: - -- `&sig` – The `Signature` to verify. -- `symbol!("increment")` – The scope of where the signature should be valid - within this contract. In this example the scope is the name of this function. -- `&id` – The identifier the `Signature` signs approving the operation to - increment that identifiers number. -- `&nonce` – The nonce to ensure the replay prevention value is included in the - `SignaturePayload`. - -#### Verifying the Nonce - -The `increment` function verifies the nonce passed in and signed in the -`SignaturePayload` is the next expected nonce for the identifier. - -:::caution - -Whenever signatures are used to permit an operation, there is a risk of replay. -Replay occurs when a signature is used to permit an operation multiple times. -Such a situation can be catastrophic. For example, if you produce a signature -that permits $1 to be sent to an acquaintance and there is no replay prevention, -then a malicious acquaintance could use that signature to repeatedly transfer $1 -from you to them. In the end, your account would be empty. - -Contracts can provide replay prevention by numerous methods. One approach is by -including a sequential [nonce] as an argument in the `SignaturePayload` and -writing the contract such that it will not allow that nonce to be used more than -once. A sequential nonce is incremented on each use. The `SignaturePayload` -includes the nonce as a number. The contract stores the next expected nonce, -for each identifier. After verifying the signature, the contract loads the nonce -for the relevant identifier, checks that it matches the value in the signature, -and increments the value stored. This prevents reuse of the signature. - -This example uses a sequential nonce for replay prevention. - -[nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce - -::: - -```rust -pub fn increment(env: Env, sig: Signature, nonce: i128) -> u32 { - // ... - verify_and_consume_nonce(&env, &sig, &nonce); - // ... -} -``` - -When determining the next expected nonce the type of `Signature` is considered. - -Invokers need no replay prevention. Invocations directly from an account in a -Stellar transaction get replay prevention from the Stellar transaction they are -submitted in, because Stellar transactions contain a sequence number that -operates like a nonce for the account. Contract invokers control when an -invocation occurs so also do not provide a nonce. - -Presigned ed25519 signatures and account signatures get the stored nonce, verify -it matches the nonce included in this invocation, and error if not. The nonce is -incremented so that the value is consumed and cannot be reused. - -```rust -fn verify_and_consume_nonce(env: &Env, sig: &Signature, nonce: i128) { - match sig { - Signature::Invoker => { - if nonce != 0 { - panic_with_error!(env, Error::IncorrectNonceForInvoker); - } - } - Signature::Ed25519(_) | Signature::Account(_) => { - let id = sig.identifier(env); - if nonce != get_nonce(env, &id) { - panic_with_error!(env, Error::IncorrectNonce); - } - set_nonce(env, &id, nonce + 1); - } - } -} - -fn get_nonce(env: &Env, id: &Identifier) -> i128 { - let key = DataKey::Nonce(id.clone()); - env.storage().get(key).unwrap_or(Ok(0)).unwrap() -} - -fn set_nonce(env: &Env, id: &Identifier, nonce: i128) { - let key = DataKey::Nonce(id.clone()); - env.storage().set(key, nonce); -} -``` - -:::info - -If a contract function errors by returning an error or panicking with an error -or string, any operations the contract has performed such as emitting events or -storing data are rolled back. This also applies to incrementing of the nonce. -If a contract needs the nonce to be consumed on failed invocations, the contract -needs to be written so that an error is not returned and the invocation does not -fail. - -::: - -#### Storing Data Scoped by Identifier - -The `increment` function stores the counter for each identifier using the -`DataKey::Counter` enum variant. If the contract evolves to store other data new -enum variants can be added for any new data items. - -```rust -// Construct a key for the data being stored. Use an enum to set the -// contract up well for adding other types of data to be stored. -let key = DataKey::Counter(id); - -// Get the current count for the invoker. -let mut count: u32 = env - .storage() - .get(&key) - .unwrap_or(Ok(0)) // If no value set, assume 0. - .unwrap(); // Panic if the value of COUNTER is not u32. - -// Increment the count. -count += 1; - -// Save the count. -env.storage().set(&key, count); -``` - -#### Retrieving the Nonce - -Users of this contract will need to know which nonce to use. The contract could -expect users to keep track of the next nonce to use, but that may not be -trivial, so the contract exports a function that provides the information. - -```rust -pub fn nonce(env: Env, id: Identifier) -> i128 { - get_nonce(&env, &id) -} -``` - -### Tests - -Open the `auth_advanced/src/test.rs` file to follow along. - -#### Testing Auth by Invoker - -The contract supports authentication with an invoker. The way this works is very -similar to the simpler [auth example]. - -```rust title="auth/src/test.rs" -#[test] -fn test_auth_with_invoker() { - let env = Env::default(); - let contract_id = env.register_contract(None, IncrementContract); - let client = IncrementContractClient::new(&env, &contract_id); - - let user_1 = env.accounts().generate(); - let user_2 = env.accounts().generate(); - - assert_eq!( - client - .with_source_account(&user_1) - .increment(&Signature::Invoker, &0), - 1 - ); - // ... -} -``` - -The test generates account IDs for two users. - -The `generate` function creates a new account ID that the test can use to -represent an account interacting with the contract. - -```rust -let _ = env.accounts().generate(); -``` - -The `increment` function is invoked with the users configured as the source -account and therefore the invoker. The invocation still requires a `Signature` -and `nonce` to be provided, but for an invoker in this example these values are -very simple. - -```rust -client - .with_source_account(&user_1) - .increment(&Signature::Invoker, &0), -``` - -:::tip - -Functions invoked in tests can use `with_source_account(account_id)` to simulate -an invocation from an account, and the invoked function will see `env.invoker()` -as `Address::Account(account_id)`. - -::: - -#### Testing Auth by Ed25519 - -The contract supports authentication with an ed25519 signature. This form of -auth can be useful for situations where a participant has no existence on -network. - -```rust title="auth/src/test.rs" -#[test] -fn test_auth_with_ed25519() { - let env = Env::default(); - let contract_id = BytesN::from_array(&env, &[0; 32]); - env.register_contract(&contract_id, IncrementContract); - let client = IncrementContractClient::new(&env, contract_id.clone()); - - let (user_1_id, user_1_sign) = soroban_auth::testutils::ed25519::generate(&env); - let (user_2_id, user_2_sign) = soroban_auth::testutils::ed25519::generate(&env); - - let nonce = 0; - let sig = soroban_auth::testutils::ed25519::sign( - &env, - &user_1_sign, - &contract_id, - symbol!("increment"), - (&user_1_id, &nonce), - ); - assert_eq!(client.increment(&sig, &nonce), 1); - - // ... -} -``` - -The test generates ed25519 keys and identifiers for two users. - -The `generate` function creates a new random ed25519 key that the test can use -to produce signatures and verify to an identifier. The generate function -provides the identifier that the signer can sign for. - -```rust -let (user_1_id, user_1_sign) = soroban_auth::testutils::ed25519::generate(&env); -``` - -The `sign` function produces a valid signature for the inputs. These inputs -should match the inputs that the `verify` function within the contract will -use. - -```rust -let sig = soroban_auth::testutils::ed25519::sign( - &env, - &user_1_sign, - &contract_id, - symbol!("increment"), - (&user_1_id, &nonce), -); -``` - -The `increment` function is invoked with the signature and the nonce. - -```rust -client.increment(&sig, &nonce) -``` - -Open the `auth_advanced/src/test.rs` file to view other tests that demonstrate -how to test signature and nonce failure. - -## Build the Contract - -To build the contract into a `.wasm` file, use the `cargo build` command. - -```sh -cargo build --target wasm32-unknown-unknown --release -``` - -The `.wasm` file should be found in the `../target` directory after building: - -``` -target/wasm32-unknown-unknown/release/soroban_auth_advanced_contract.wasm -``` - -## Run the Contract - -If you have [`soroban-cli`] installed, you can invoke functions on the contract. - -:::caution - -The [`soroban-cli`] is in development and passing enum and struct values as -inputs requires typing their raw JSON representation. Improvements are planned. - -::: - -To invoke the contract as an account invoker the `Signature` to be passed must -be a single-element `Vec` containing the `Symbol` `"Invoker"`. - -```sh -soroban invoke \ - --wasm target/wasm32-unknown-unknown/release/soroban_auth_advanced_contract.wasm \ - --id 1 \ - --account GC24I42QMKKR4NE6IYNPCQHUO4PXWXDGNZ7QVMMSR5EWAYSGKBHPLGHH \ - --fn increment \ - --arg '{"object":{"vec":[{"symbol":[73,110,118,111,107,101,114]}]}}' \ - --arg 0 -``` - -```sh -soroban invoke \ - --wasm target/wasm32-unknown-unknown/release/soroban_auth_advanced_contract.wasm \ - --id 1 \ - --account GC24I42QMKKR4NE6IYNPCQHUO4PXWXDGNZ7QVMMSR5EWAYSGKBHPLGHH \ - --fn increment \ - --arg '{"object":{"vec":[{"symbol":[73,110,118,111,107,101,114]}]}}' \ - --arg 0 -``` - -Run these commands several times to increment the counters for each account. - -View the data that has been stored against each user with `soroban read`. - -```sh -soroban read --id 1 -``` - -``` -"[""Counter"",[""Account"",""GC24I42QMKKR4NE6IYNPCQHUO4PXWXDGNZ7QVMMSR5EWAYSGKBHPLGHH""]]",1 -"[""Counter"",[""Account"",""GDQHNBKFCO666SPX4RS62VTDY7H5W2QXHVVVQCDTADTOI3IYZGEOZL6V""]]",3 -``` - -[`soroban-cli`]: ../getting-started/setup#install-the-soroban-cli diff --git a/docs/how-to-guides/auth.mdx b/docs/how-to-guides/auth.mdx index 2dff6084..56c0c912 100644 --- a/docs/how-to-guides/auth.mdx +++ b/docs/how-to-guides/auth.mdx @@ -3,44 +3,27 @@ sidebar_position: 7 title: Auth --- -The [auth example] demonstrates how to tell who has invoked a contract, and -verify that a contract has been invoked by an account or contract. This example -is an extension of the [storing data example]. +The [auth example] demonstrates how to implement authentication and +authorization using the Soroban Host-managed auth framework. -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 - -[events example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/events -[storing data example]: getting-started/storing-data.mdx - -The participant who invoked a contract is an `Address` containing one-of a: - -- Account ID -- Contract ID - -In this example, data is stored associated with an `Address` after -authorization has been verified. +This example is an extension of the [storing data example]. -:::info - -For contracts that would benefit from supporting auth with presigned -invocations, or ed25519 keys independent of accounts and contracts, see the -[advanced auth example]. +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 -::: +[storing data example]: ../getting-started/storing-data.mdx -[auth example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/auth -[advanced auth example]: auth-advanced.mdx +[auth example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/auth ## Run the Example First go through the [Setup] process to get your development environment -configured, then clone the `v0.4.2` tag of `soroban-examples` repository: +configured, then clone the `v0.5.0` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone -b v0.4.2 https://github.com/stellar/soroban-examples +git clone -b v0.5.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [Gitpod][oigp]. @@ -72,12 +55,30 @@ pub struct IncrementContract; #[contractimpl] impl IncrementContract { - /// Increment increments a counter for the invoker, and returns the value. - pub fn increment(env: Env) -> u32 { + /// Increment increments a counter for the user, and returns the value. + pub fn increment(env: Env, user: Address, value: u32) -> u32 { + // Requires `user` to have authorized call of the `increment` of this + // contract with all the arguments passed to `increment`, i.e. `user` + // and `value`. This will panic if auth fails for any reason. + // When this is called, Soroban host performs the necessary + // authentication, manages replay prevention and enforces the user's + // authorization policies. + // The contracts normally shouldn't worry about these details and just + // write code in generic fashion using `Address` and `require_auth` (or + // `require_auth_for_args`). + user.require_auth(); + + // This call is equilvalent to the above: + // user.require_auth_for_args((&user, value).into_val(&env)); + + // The following has less arguments but is equivalent in authorization + // scope to the above calls (the user address doesn't have to be + // included in args as it's guaranteed to be authenticated). + // user.require_auth_for_args((value,).into_val(&env)); + // Construct a key for the data being stored. Use an enum to set the // contract up well for adding other types of data to be stored. - let invoker = env.invoker(); - let key = DataKey::Counter(invoker); + let key = DataKey::Counter(user.clone()); // Get the current count for the invoker. let mut count: u32 = env @@ -87,10 +88,10 @@ impl IncrementContract { .unwrap(); // Panic if the value of COUNTER is not u32. // Increment the count. - count += 1; + count += value; // Save the count. - env.storage().set(&key, count); + env.storage().set(&key, &count); // Return the count to the caller. count @@ -98,65 +99,82 @@ impl IncrementContract { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/v0.4.2/auth +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/auth ## How it Works -The example contract tracks a counter for each invoker. - -Open the `auth/src/lib.rs` file to follow along. +The example contract stores a per-`Address` counter that can only be incremented +by the owner of that `Address`. -### Invoker +Open the `auth/src/lib.rs` file or see the code above to follow along. -The invoker is an `Address` of the account or contract that directly -invoked the contract function. +### `Address` ```rust -let invoker = env.invoker(); +pub enum DataKey { + Counter(Address), +} ``` -The `env.invoker()` always returns the invoker of the currently executing -contract. It will return either: +`Address` is a universal Soroban identifier that may represent a Stellar +account, a contract or an 'account contract' (a contract that defines a custom +authentication scheme and authorization policies). Contracts don't need to +distinguish between these internal representations though. `Address` can be +used any time some network identity needs to be represented, like to +distinguish between counters for different users in this example. -- `Account` with an `AccountId` if the contract was invoked directly by an - account. -- `Contract` with a `BytesN<32>` contract ID if the contract was invoked by - another contract. +:::tip Enum keys like `DataKey` are useful for organizing contract storage. -Contracts should not need to unpack the `Address` for most use cases. Contracts -should treat the value as opaque wherever possible so that they are -interoperable with accounts and contracts. +Different enum values create different key 'namespaces'. -The `Address` value can be used as a key in storage to store data for a user. -The example contract only reads and writes data for the authenticated invoker, -and so the only thing that can access or change the data for an invoker, is the -invoker themselves. - -:::tip +In the example the counter for each address is stored against +`DataKey::Counter(Address)`. If the contract needs to start storing other types +of data, it can do so by adding additional variants to the enum. +::: -Enum keys like `DataKey` can be a helpful way to store a variety of different -types of data keyed against the same or similar things. In the example the -counter for each address is stored against `DataKey::Counter(Address)`. If the -contract needs to start storing other types of data for addresses, it can do so -by adding additional variants to the enum. +### `require_auth` ```rust -#[contracttype] -pub enum DataKey { - Counter(Address), -} +impl IncrementContract { + pub fn increment(env: Env, user: Address, value: u32) -> u32 { + user.require_auth(); ``` +`require_auth` method can be called for any `Address`. Semantically +`user.require_auth()` here means 'require `user` to have authorized calling +`increment` function of the current `IncrementContract` instance with the +current call arguments, i.e. the current `user` and `value` argument values'. +In simpler terms, this ensures that the `user` has allowed incrementing their +counter value and nobody else can increment it. + +When using `require_auth` the contract implementation doesn't need to worry +about the signatures, authentication, and replay prevention. All these features +are implemented by the Soroban host and happen automatically as long as +`Address` type is used. + +`Address` has another, more extensible version of this method called +`require_auth_for_args`. It works in the same fashion as `require_auth`, but +allows customizing the arguments that need to be authorized. Note though, this +should be used with care to ensure that there is a deterministic mapping +between the contract invocation arguments and the `require_auth_for_args` +arguments. + +The following two calls are functionally equivalent to `user.require_auth`: + ```rust -let invoker = env.invoker(); -let key = DataKey::Counter(invoker); +// Completely equivalent +user.require_auth_for_args((&user, value).into_val(&env)); +// The following has less arguments but is equivalent in authorization +// scope to the above call (the user address doesn't have to be +// included in args as it's guaranteed to be authenticated). +user.require_auth_for_args((value,).into_val(&env)); ``` -::: - ### Tests -Open the `auth/src/test.rs` file to follow along. +Open the [`auth/src/test.rs`] file to follow along. + +[`auth/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v0.5.0/auth/src/test.rs ```rust title="auth/src/test.rs" #[test] @@ -165,13 +183,33 @@ fn test() { let contract_id = env.register_contract(None, IncrementContract); let client = IncrementContractClient::new(&env, &contract_id); - let user_1 = env.accounts().generate(); - let user_2 = env.accounts().generate(); - - assert_eq!(client.with_source_account(&user_1).increment(), 1); - assert_eq!(client.with_source_account(&user_1).increment(), 2); - assert_eq!(client.with_source_account(&user_2).increment(), 1); - assert_eq!(client.with_source_account(&user_1).increment(), 3); + let user_1 = Address::random(&env); + let user_2 = Address::random(&env); + + assert_eq!(client.increment(&user_1, &5), 5); + // Verify that the user indeed had to authorize a call of `increment` with + // the expected arguments: + assert_eq!( + env.recorded_top_authorizations(), + std::vec![( + // Address for which auth is performed + user_1.clone(), + // Identifier of the called contract + contract_id.clone(), + // Name of the called function + symbol!("increment"), + // Arguments used to call `increment` (converted to the env-managed vector via `into_val`) + (user_1.clone(), 5_u32).into_val(&env) + )] + ); + + // Do more `increment` calls. It's not necessary to verify authorizations + // for every one of them as we don't expect the auth logic to change from + // call to call. + assert_eq!(client.increment(&user_1, &2), 7); + assert_eq!(client.increment(&user_2, &1), 1); + assert_eq!(client.increment(&user_1, &3), 10); + assert_eq!(client.increment(&user_2, &4), 5); } ``` @@ -199,40 +237,57 @@ type with `Client` appended. For example, in our contract the contract type is let client = IncrementContractClient::new(&env, &contract_id); ``` -The contract generates account IDs for two users. - -The `generate` function creates a new account ID that the test can use to -represent an account interacting with the contract. +Generate `Address`es for two users. Normally the exact value of the `Address` +shouldn't matter for testing, so they're simply generated randomly. ```rust -let _ = env.accounts().generate(); +let user_1 = Address::random(&env); +let user_2 = Address::random(&env); ``` -The `increment` function is invoked twice with `user_1` configured as the source -account and therefore the invoker. The results are asserted to be `1` and `2` -because it is the first and second increments for the user. +Invoke `increment` function for `user_1`. ```rust -assert_eq!(client.with_source_account(&user_1).increment(), 1); -assert_eq!(client.with_source_account(&user_1).increment(), 2); +assert_eq!(client.increment(&user_1, &5), 5); ``` -The `increment` function is invoked once with `user_2` configured as the source -account. The results are asserted to be `1` because it is the first increment -for the user. The first users data is separate to that of the second users. The -users have no access to each others data. +In tests, there is no need to do anything special to emulate the authorizations. +The Soroban environment will record all the authorizations that happened in the +most recent contract invocation. All authorizations are considered to have +succeeded. + +In order to verify that the `requre_auth` call(s) have indeed happened, use +`recorded_top_authorizations` function that returns a vector of tuples containing +the authorizations from the most recent contract invocation ('top' here means +that some deeper authorizations are skipped; that doesn't make any difference for +this example). ```rust -assert_eq!(client.with_source_account(&user_2).increment(), 1); +assert_eq!( + env.recorded_top_authorizations(), + std::vec![( + // Address for which auth is performed + user_1.clone(), + // Identifier of the called contract + contract_id.clone(), + // Name of the called function + symbol!("increment"), + // Arguments used to call `increment` (converted to the env-managed vector via `into_val`) + (user_1.clone(), 5_u32).into_val(&env) + )] +); ``` -:::tip -Functions invoked in tests can use `with_source_account(account_id)` to simulate -an invocation from an account, and the invoked function will see `env.invoker()` -as `Address::Account(account_id)`. +Invoke `increment` function several more times for both users. Notice, that the +values are tracked separately for each users. -::: +```rust +assert_eq!(client.increment(&user_1, &2), 7); +assert_eq!(client.increment(&user_2, &1), 1); +assert_eq!(client.increment(&user_1, &3), 10); +assert_eq!(client.increment(&user_2, &4), 5); +``` ## Build the Contract @@ -268,17 +323,33 @@ soroban invoke \ --fn increment ``` -Run these commands several times to increment the counters for each account. +Run these commands several times to increment the counters for each address. View the data that has been stored against each user with `soroban read`. ```sh soroban read --id 1 ``` - ``` "[""Counter"",[""Account"",""GC24I42QMKKR4NE6IYNPCQHUO4PXWXDGNZ7QVMMSR5EWAYSGKBHPLGHH""]]",1 "[""Counter"",[""Account"",""GDQHNBKFCO666SPX4RS62VTDY7H5W2QXHVVVQCDTADTOI3IYZGEOZL6V""]]",3 ``` [`soroban-cli`]: ../getting-started/setup#install-the-soroban-cli + + +## Further reading + +[Authorization documentation](../learn/authorization.mdx) provides more details +on how Soroban auth framework works. + +[Timelock](timelock.mdx) and [Single Offer](single-offer-sale.mdx) examples +demonstrate authorizing token operations on behalf of the user, which can be +extended to any nested contract invocations. + +[Atomic Swap](atomic-swap.mdx) example demonstrates multi-party authorizaton +where multiple users sign their parts of the contract invocation. + +[Custom Account](custom-account.mdx) example for demonstrates an account +contract that defines a custom authentication scheme and user-defined +authorization policies. \ No newline at end of file diff --git a/docs/how-to-guides/build-your-own-sdk.mdx b/docs/how-to-guides/build-your-own-sdk.mdx index e7105c59..b5d077af 100644 --- a/docs/how-to-guides/build-your-own-sdk.mdx +++ b/docs/how-to-guides/build-your-own-sdk.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 18 +sidebar_position: 20 title: Build Your Own SDK --- diff --git a/docs/how-to-guides/cross-contract-call.mdx b/docs/how-to-guides/cross-contract-call.mdx index 7b21322f..b4b558b4 100644 --- a/docs/how-to-guides/cross-contract-call.mdx +++ b/docs/how-to-guides/cross-contract-call.mdx @@ -7,7 +7,7 @@ The [cross contract call example] demonstrates how to call a contract from another contract. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 :::info @@ -19,17 +19,17 @@ appreciated [here](https://github.com/stellar/rs-soroban-sdk/issues/new/choose). ::: -[cross contract call example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/cross_contract_calls +[cross contract call example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/cross_contract_calls ## Run the Example First go through the [Setup] process to get your development environment -configured, then clone the `v0.4.2` tag of `soroban-examples` repository: +configured, then clone the `v0.5.0` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone -b v0.4.2 https://github.com/stellar/soroban-examples +git clone -b v0.5.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [Gitpod][oigp]. @@ -78,7 +78,7 @@ impl ContractB { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/v0.4.2/cross_contract +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/cross_contract ## How it Works diff --git a/docs/how-to-guides/custom-account.mdx b/docs/how-to-guides/custom-account.mdx new file mode 100644 index 00000000..a6f3934c --- /dev/null +++ b/docs/how-to-guides/custom-account.mdx @@ -0,0 +1,362 @@ +--- +sidebar_position: 18 +title: Custom Account +--- + +The [custom account example] demonstrates how to implement a simple account +contract that supports multisig and customizable authorization policies. This +account contract can be used with the Soroban auth framework, so that any time +an `Address` pointing at this contract instance is used, the custom logic here +is applied. + +The custom accounts are exclusive to Soroban and can't be used to perform the +regular Stellar operations. + +Implementing a proper custom account contract requires a very good +understanding of authentication and authorization and requires rigorous +testing and rewview. The example here is *not* a full-fledged account contract - +use it as an API reference only. + +:::caution + +Custom Accounts are still experimental and there is not much tooling built +around them yet beyond the basic SDK support. + +::: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 + +[custom account example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/account + +## Run the Example + +First go through the [Setup] process to get your development environment +configured, then clone the `v0.5.0` tag of `soroban-examples` repository: + +[Setup]: ../getting-started/setup.mdx + +``` +git clone -b v0.5.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example use `cargo test`. + +``` +cargo test -p cargo test -p soroban-account-contract +``` + +You should see the output: + +``` +running 1 test +test test::test_token_auth ... ok +``` + +## How it Works + +Open the `account/src/lib.rs` file to follow along. + +Any account contract has to implement a special function `check_auth` that takes +the signature payload, signatures and authorization context. + +The example contract here uses `ed25519` keys for signature verificaiton and +supports multiple unweighted signers. It also implements a policy that allows +setting per-token limits on transfers. The token can be spent beyond the limit +only if every signature is provided. + +For example, the user may initialize this contract with 2 keys and introduce 100 +USDC spend limit. This way they can use a single key to sign their contract +invocations and be sure that even if they sign a malicious transaction they +won't lose more than 100 USDC. + +### Initialization + +```rust +#[contracttype] +#[derive(Clone)] +enum DataKey { + SignerCnt, + Signer(BytesN<32>), + SpendLimit(BytesN<32>), +} +... +// Initialize the contract with a list of ed25519 public key ('signers'). +pub fn init(env: Env, signers: Vec>) { + // In reality this would need some additional validation on signers + // (deduplication etc.). + for signer in signers.iter() { + env.storage().set(&DataKey::Signer(signer.unwrap()), &()); + } + env.storage().set(&DataKey::SignerCnt, &signers.len()); +} +``` + +Account contracts are the few contracts that need to work with the public keys +explicitly and not the `Address` type. Here we initialize the contract with +ed25519 keys. + +### Policy modification + +```rust +// Adds a limit on any token transfers that aren't signed by every signer. +pub fn add_limit(env: Env, token: BytesN<32>, limit: i128) { + // The current contract address is the account contract address and has + // the same semantics for `require_auth` call as any other account + // contract address. + // Note, that if a contract *invokes* another contract, then it would + // authorize the call on its own behalf and that wouldn't require any + // user-side verification. + env.current_contract_address().require_auth(); + env.storage().set(&DataKey::SpendLimit(token), &limit); +} +``` + +This function allows to set and modify the per-token spen limit described above. +The neat trick here is that `require_auth` can be used for the +`current_contract_address()`, i.e. the account contract may be used to verify +authorization for its own administrative functions. This way there is no need +to write duplicate authorization and authentication logic. + +### `check_auth` + +```rust +pub fn check_auth( + env: Env, + signature_payload: BytesN<32>, + signatures: Vec, + auth_context: Vec, +) -> Result<(), AccError> { + // Perform authentication. + authenticate(&env, &signature_payload, &signatures)?; + + let tot_signers: u32 = env.storage().get(&DataKey::SignerCnt).unwrap().unwrap(); + let all_signed = tot_signers == signatures.len(); + + let curr_contract_id = env.current_contract_id(); + + // This is a map for tracking the token spend limits per token. This + // makes sure that if e.g. multiple `xfer` calls are being authorized + // for the same token we still respect the limit for the total + // transferred amount (and not the 'per-call' limits). + let mut spend_left_per_token = Map::, i128>::new(&env); + // Verify the authorization policy. + for context in auth_context.iter() { + verify_authorization_policy( + &env, + &context.unwrap(), + &curr_contract_id, + all_signed, + &mut spend_left_per_token, + )?; + } + Ok(()) +} +``` + +`check_auth` is a special function that turns a contract into an 'account +contract'. It will get called by the Soroban host every time +`require_auth/require_auth_for_args` is called for the address of this contract. + +:::caution + +`check_auth` has to be **read-only** now, as anyone can call it and it shouldn't +contain any data for authorizing itself. + +::: + +Here it is implemented in two steps. First, authentication is performed using +the signature payload and a vector of signatures. Second, authorization policy +is enforced using the `auth_context` vector. This vector contains all the +contract calls that are being authorized by the provided signatures. + +### Authentication + +```rust +fn authenticate( + env: &Env, + signature_payload: &BytesN<32>, + signatures: &Vec, +) -> Result<(), AccError> { + for i in 0..signatures.len() { + let signature = signatures.get_unchecked(i).unwrap(); + if i > 0 { + let prev_signature = signatures.get_unchecked(i - 1).unwrap(); + if prev_signature.public_key >= signature.public_key { + return Err(AccError::BadSignatureOrder); + } + } + if !env + .storage() + .has(&DataKey::Signer(signature.public_key.clone())) + { + return Err(AccError::UnknownSigner); + } + env.crypto().ed25519_verify( + &signature.public_key, + &signature_payload.clone().into(), + &signature.signature, + ); + } + Ok(()) +} +``` + +Authentication here simply checks that the provided signatures are valid given +the payload and also that they belong to the signers of this account contract. + +### Authorization policy + +```rust +fn verify_authorization_policy( + env: &Env, + context: &AuthorizationContext, + curr_contract_id: &BytesN<32>, + all_signed: bool, + spend_left_per_token: &mut Map, i128>, +) -> Result<(), AccError> { + // For the account control every signer must sign the invocation. + if &context.contract == curr_contract_id { + if !all_signed { + return Err(AccError::NotEnoughSigners); + } + } +``` + +We verify the policy per `AuthorizationContext` i.e. per one `require_auth` +call. +The policy for the account contract itself enforces every signer to have signed +the method call. + +```rust +// Otherwise, we're only interested in functions that spend tokens. +if context.fn_name != XFER_FN && context.fn_name != INCR_ALLOW_FN { + return Ok(()); +} + +let spend_left: Option = + if let Some(spend_left) = spend_left_per_token.get(context.contract.clone()) { + Some(spend_left.unwrap()) + } else if let Some(limit_left) = env + .storage() + .get(&DataKey::SpendLimit(context.contract.clone())) + { + Some(limit_left.unwrap()) + } else { + None + }; + +// 'None' means that the contract is outside of the policy. +if let Some(spend_left) = spend_left { + // 'amount' is the third argument in both `approve` and `xfer`. + // If the contract has a different signature, it's safer to panic + // here, as it's expected to have the standard interface. + let spent: i128 = context + .args + .get(2) + .unwrap() + .unwrap() + .try_into_val(env) + .unwrap(); + if spent < 0 { + return Err(AccError::NegativeAmount); + } + if !all_signed && spent > spend_left { + return Err(AccError::NotEnoughSigners); + } + spend_left_per_token.set(context.contract.clone(), spend_left - spent); +} +Ok(()) +``` + +Then we check for the standard token function names and verify that for these +function we don't exceed the spending limits. + +### Tests + +Open the [`account/src/test.rs`] file to follow along. + +[`account/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v0.5.0/account/src/test.rs + +Refer to another examples for the general information on the test setup. + +Here we only look at some points specific to the account contracts. + +```rust +fn sign(e: &Env, signer: &Keypair, payload: &BytesN<32>) -> Signature { + Signature { + public_key: signer_public_key(e, signer), + signature: signer + .sign(payload.to_array().as_slice()) + .to_bytes() + .into_val(e), + } +} +``` + +Unlike most of the contracts that may simply use `Address`, account contracts +deal with the signature verification and hence need to actually sign the +payloads. + +```rust +let payload = BytesN::random(&env); +let token = BytesN::random(&env); +account_contract + .try_check_auth( + &payload, + &vec![&env, sign(&env, &signers[0], &payload)], + &vec![ + &env, + token_auth_context(&env, &token, symbol!("xfer"), 1000), + ], + ) + .unwrap() + .unwrap(); +``` + +`check_auth` can be called in tests as any other contract function. Here without +any policies a single signer can authorize an arbitrary `xfer` call. + +```rust +let account_address = Address::from_contract_id(&env, &account_contract.contract_id); +// Add a spend limit of 1000 per 1 signer. +account_contract.add_limit(&token, &1000); +// Verify that this call needs to be authorized. +assert_eq!( + env.recorded_top_authorizations(), + std::vec![( + account_address.clone(), + account_contract.contract_id.clone(), + symbol!("add_limit"), + (token.clone(), 1000_i128).into_val(&env), + )] +); +``` +`env.recorded_top_authorizations()` can still be used for the functions that +aren't `check_auth` and use the regular Soroban auth. As usually in tests, +authorizations are just recorded and `check_auth` is not actually called. + +```rust +assert_eq!( + account_contract + .try_check_auth( + &payload, + &vec![&env, sign(&env, &signers[0], &payload)], + &vec![ + &env, + token_auth_context(&env, &token, symbol!("xfer"), 1001) + ], + ) + .err() + .unwrap() + .unwrap(), + AccError::NotEnoughSigners +); +``` +Using the `try_` version of the function allows us to verify the exact contract +error code and make sure that the verification has failed due to not having +enough signers and not for any other reason. It's a good idea for the account +contract to have detailed error codes and verify that they are returned when +they are expected. diff --git a/docs/how-to-guides/custom-types.mdx b/docs/how-to-guides/custom-types.mdx index e3a919c8..a7b355d6 100644 --- a/docs/how-to-guides/custom-types.mdx +++ b/docs/how-to-guides/custom-types.mdx @@ -6,19 +6,19 @@ title: Custom Types The [custom types example] demonstrates how to define your own data structures that can be stored on the ledger, or used as inputs and outputs to contract invocations. This example is an extension of the [storing data example]. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.3.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 -[custom types example]: https://github.com/stellar/soroban-examples/tree/v0.3.2/custom_types +[custom types example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/custom_types [storing data example]: getting-started/storing-data.mdx ## Run the Example -First go through the [Setup] process to get your development environment configured, then clone the `v0.3.2` tag of `soroban-examples` repository: +First go through the [Setup] process to get your development environment configured, then clone the `v0.5.0` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone -b v0.3.2 https://github.com/stellar/soroban-examples +git clone -b v0.5.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [Gitpod][oigp]. @@ -70,7 +70,7 @@ impl IncrementContract { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/v0.3.2/custom_types +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/custom_types ## How it Works diff --git a/docs/how-to-guides/deployer.mdx b/docs/how-to-guides/deployer.mdx index 04116e94..5e7ac923 100644 --- a/docs/how-to-guides/deployer.mdx +++ b/docs/how-to-guides/deployer.mdx @@ -6,7 +6,7 @@ title: Deployer The [deployer example] demonstrates how to deploy contracts using a contract. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 :::info @@ -15,17 +15,17 @@ tests deploy one with the other. ::: -[deployer example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/deployer +[deployer example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/deployer ## Run the Example First go through the [Setup] process to get your development environment -configured, then clone the `v0.4.2` tag of `soroban-examples` repository: +configured, then clone the `v0.5.0` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone -b v0.4.2 https://github.com/stellar/soroban-examples +git clone -b v0.5.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [Gitpod][oigp]. @@ -73,7 +73,7 @@ impl Deployer { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/v0.4.2/deployer +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/deployer ## How it Works diff --git a/docs/how-to-guides/errors.mdx b/docs/how-to-guides/errors.mdx index ddf3f171..29c4c504 100644 --- a/docs/how-to-guides/errors.mdx +++ b/docs/how-to-guides/errors.mdx @@ -8,20 +8,20 @@ contract that invokers of the contract can understand and handle. This example is an extension of the [storing data example]. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 -[errors example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/errors +[errors example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/errors [storing data example]: getting-started/storing-data.mdx ## Run the Example First go through the [Setup] process to get your development environment -configured, then clone the `v0.4.2` tag of `soroban-examples` repository: +configured, then clone the `v0.5.0` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone -b v0.4.2 https://github.com/stellar/soroban-examples +git clone -b v0.5.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [Gitpod][oigp]. @@ -97,7 +97,7 @@ impl IncrementContract { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/v0.4.2/errors +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/errors ## How it Works diff --git a/docs/how-to-guides/hello-world.mdx b/docs/how-to-guides/hello-world.mdx index 720d08d7..a452e3c2 100644 --- a/docs/how-to-guides/hello-world.mdx +++ b/docs/how-to-guides/hello-world.mdx @@ -6,18 +6,18 @@ title: Hello World The [hello world example] demonstrates how to write a simple contract, with a single function that takes one input and returns it as an output. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 -[hello world example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/hello_world +[hello world example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/hello_world ## Run the Example -First go through the [Setup] process to get your development environment configured, then clone the `v0.4.2` tag of `soroban-examples` repository: +First go through the [Setup] process to get your development environment configured, then clone the `v0.5.0` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone -b v0.4.2 https://github.com/stellar/soroban-examples +git clone -b v0.5.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [Gitpod][oigp]. @@ -52,7 +52,7 @@ impl HelloContract { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/v0.4.2/hello_world +Ref: https://github.com/stellar/soroban-examples/tree/v0.5.0/hello_world ## How it Works diff --git a/docs/how-to-guides/invoking-contracts-with-transactions.mdx b/docs/how-to-guides/invoking-contracts-with-transactions.mdx index 3840330a..7d56cee8 100644 --- a/docs/how-to-guides/invoking-contracts-with-transactions.mdx +++ b/docs/how-to-guides/invoking-contracts-with-transactions.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 17 +sidebar_position: 19 title: Invoking and Creating Contracts with Stellar Transactions --- diff --git a/docs/how-to-guides/liquidity-pool.mdx b/docs/how-to-guides/liquidity-pool.mdx index 9bbe1a79..785d8263 100644 --- a/docs/how-to-guides/liquidity-pool.mdx +++ b/docs/how-to-guides/liquidity-pool.mdx @@ -1,12 +1,12 @@ --- -sidebar_position: 14 +sidebar_position: 15 title: Liquidity Pool --- The [liquidity pool example] demonstrates how to write a constant product liquidity pool contract. The comments in the [source code] explain how the contract should be used. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 -[liquidity pool example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/liquidity_pool -[source code]: https://github.com/stellar/soroban-examples/blob/v0.4.2/liquidity_pool/src/lib.rs#L143 +[liquidity pool example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/liquidity_pool +[source code]: https://github.com/stellar/soroban-examples/blob/v0.5.0/liquidity_pool/src/lib.rs#L143 diff --git a/docs/how-to-guides/multisig-wallet.mdx b/docs/how-to-guides/multisig-wallet.mdx deleted file mode 100644 index 5777fb20..00000000 --- a/docs/how-to-guides/multisig-wallet.mdx +++ /dev/null @@ -1,20 +0,0 @@ ---- -sidebar_position: 12 -title: Multisig Wallet ---- - -The [multisig wallet example] demonstrates a complex auth scheme with multiple -signers that authorizes payments either immediately or in a delayed async fashion. - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 - -:::tip - -Stellar supports [multisig accounts] natively, and multisig accounts may be -simpler to manage than a contract driven multisig wallet. - -::: - -[multisig wallet example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/wallet -[multisig accounts]: https://developers.stellar.org/docs/encyclopedia/signatures-multisig diff --git a/docs/how-to-guides/single-offer-sale.mdx b/docs/how-to-guides/single-offer-sale.mdx index d912432b..b9dca850 100644 --- a/docs/how-to-guides/single-offer-sale.mdx +++ b/docs/how-to-guides/single-offer-sale.mdx @@ -1,12 +1,14 @@ --- -sidebar_position: 13 +sidebar_position: 14 title: Single Offer Sale --- -The [single offer sale example] demonstrates how to write a contract that allows a seller to set up an offer to sell token A for token B. The comments in the [source code] explain how the contract should be used. +The [single offer sale example] demonstrates how to write a contract that allows +a seller to set up an offer to sell token A for token B to multiple buyers. The +comments in the [source code] explain how the contract should be used. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 -[single offer sale example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/single_offer -[source code]: https://github.com/stellar/soroban-examples/blob/v0.4.2/single_offer/src/lib.rs +[single offer sale example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/single_offer +[source code]: https://github.com/stellar/soroban-examples/blob/v0.5.0/single_offer/src/lib.rs diff --git a/docs/how-to-guides/stellar-asset-contract.mdx b/docs/how-to-guides/stellar-asset-contract.mdx index 6770918f..25bc8414 100644 --- a/docs/how-to-guides/stellar-asset-contract.mdx +++ b/docs/how-to-guides/stellar-asset-contract.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 16 +sidebar_position: 17 title: Stellar Asset Contract --- @@ -19,7 +19,7 @@ The Stellar Asset Contract is in early development, has not been audited, and is Stellar has numerous assets on its classic network, and being able to use them in Soroban would give users much more flexibility with how they can use their assets. For this reason, we introduced the Stellar Asset Contract, or SAC for short, which will allow users to use their Stellar account and trustline balances in Soroban. -The SAC implements the [token interface](./tokens.mdx), which is similar to the widely used ERC-20 token standard. This should make it easier for existing smart contract developers to get started on Stellar. +The SAC implements the [token interface](tokens.mdx), which is similar to the widely used ERC-20 token standard. This should make it easier for existing smart contract developers to get started on Stellar. ## Deployment @@ -46,7 +46,7 @@ After the contract has been deployed, users can use their classic account or tru ## Authorization semantics -See the [advanced auth example](./auth-advanced.mdx) for an overview of authorization. +See the [auth example](./auth.mdx) for an overview of authorization. ### SAC operations @@ -88,39 +88,9 @@ token.with_source_account(&token_admin_id).mint( See a more complete example that uses invoker auth in the tests [here](https://github.com/stellar/soroban-examples/blob/51a95262caba3f8ac466fa8bbc07004ad297ae13/timelock/src/test.rs#L60). -### Example: Signing payloads - -The payload signature semantics is the same as for regular contracts using [advanced auth](./auth-advanced.mdx). The following snippet shows how should the signature payload look like: - -```rust -let nonce = token.nonce(&token_admin_id); -// This is the test call, but the contract call arguments and signature payload -// would be the same for the real contract call too. -let sig = soroban_auth::testutils::ed25519::sign( - &env, - // Signer has the private key of the admin. - &token_admin_signer, - // Identifier of the token contract. - &token_contract_id, - // Name of the contract function we call. - symbol!("mint"), - // Arguments of the contract function call. - // Notice that instead of the signature (first `mint` argument), public key - // is used as the first argument here. - (&token_admin_id, &nonce, &user_id, &1000), -); -// Call the contract with signature we computed above. -token.mint( - &sig, - &nonce, - &user_id, - &1000, -); -``` - ## Contract Interface -This interface can be found in the [SDK](https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-token-spec/src/lib.rs). It extends the common [token interface](./tokens.mdx). +This interface can be found in the [SDK](https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-token-spec/src/lib.rs). It extends the common [token interface](tokens.mdx). ## Interacting with the token contract in tests diff --git a/docs/how-to-guides/timelock.mdx b/docs/how-to-guides/timelock.mdx index 3912b191..0a539e97 100644 --- a/docs/how-to-guides/timelock.mdx +++ b/docs/how-to-guides/timelock.mdx @@ -1,14 +1,17 @@ --- -sidebar_position: 11 +sidebar_position: 13 title: Timelock --- -The [timelock example] demonstrates how to write a timelock and implements a greatly simplified claimable balance similar to the [claimable balance] feature available on Stellar. +The [timelock example] demonstrates how to write a timelock and implements a +greatly simplified claimable balance similar to the [claimable balance] feature +available on Stellar. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] -[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.4.2 +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v0.5.0 -The contract accepts deposits of an amount of a token, and allows other accounts to claim it before or after a time point. +The contract accepts deposits of an amount of a token, and allows other users +to claim it before or after a time point. -[timelock example]: https://github.com/stellar/soroban-examples/tree/v0.4.2/timelock +[timelock example]: https://github.com/stellar/soroban-examples/tree/v0.5.0/timelock [claimable balance]: https://developers.stellar.org/docs/glossary/claimable-balance diff --git a/docs/how-to-guides/tokens.mdx b/docs/how-to-guides/tokens.mdx index 9070ee39..9fa7e944 100644 --- a/docs/how-to-guides/tokens.mdx +++ b/docs/how-to-guides/tokens.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 15 +sidebar_position: 16 title: Tokens ---