Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions content/stellar-contracts/access/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ This allows for creating complex organizational structures with chains of comman
Here's how to establish and use role hierarchies in practice:

```rust
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol};
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol, Vec};
use stellar_access::access_control::{self as access_control, AccessControl};

const MANAGER_ROLE: Symbol = symbol_short!("manager");
Expand Down Expand Up @@ -71,6 +71,9 @@ impl MyContract {
access_control::revoke_role_no_auth(e, &guardian1, &GUARDIAN_ROLE, &manager);
}
}

#[contractimpl(contracttrait)]
impl AccessControl for MyContract {}
```

In this example:
Expand All @@ -96,7 +99,7 @@ The module includes several procedural macros to simplify authorization checks i

These macros automatically call `require_auth()` on the specified account before executing the function:

#### @only_admin
#### `#[only_admin]`

Restricts access to the contract admin only:

Expand All @@ -108,7 +111,7 @@ pub fn admin_function(e: &Env) {
}
```

#### @only_role
#### `#[only_role]`

Restricts access to accounts with a specific role:

Expand All @@ -120,7 +123,7 @@ pub fn mint(e: &Env, to: Address, token_id: u32, caller: Address) {
}
```

#### @only_any_role
#### `#[only_any_role]`

Restricts access to accounts with any of the specified roles:

Expand All @@ -136,7 +139,7 @@ pub fn multi_role_action(e: &Env, caller: Address) {

These macros check role membership but do **not** enforce authorization. You must manually call `require_auth()` if needed:

#### @has_role
#### `#[has_role]`

Checks if an account has a specific role:

Expand All @@ -148,7 +151,7 @@ pub fn conditional_mint(e: &Env, to: Address, token_id: u32, caller: Address) {
}
```

#### @has_any_role
#### `#[has_any_role]`

Checks if an account has any of the specified roles:

Expand All @@ -162,12 +165,13 @@ pub fn multi_role_check(e: &Env, caller: Address) {

## Usage Example

Here’s a simple example of using the Access Control module:
Here’s a simple example of using the Access Control module in a non-fungible token contract:

```rust
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env};
use soroban_sdk::{contract, contractimpl, symbol_short, vec, Address, Env, String, Symbol, Vec};
use stellar_access::access_control::{self as access_control, AccessControl};
use stellar_macros::{has_role, only_admin};
use stellar_macros::{has_any_role, only_admin, only_any_role, only_role};
use stellar_tokens::non_fungible::{Base, Compose, NonFungibleToken};

#[contract]
pub struct MyContract;
Expand All @@ -190,11 +194,10 @@ impl MyContract {


// we want `require_auth()` provided by the macro, since there is no
// `require_auth()` in `Base::mint`.
// `require_auth()` in the contract type's `mint_with_id`.
#[only_role(caller, "minter")]
pub fn mint(e: &Env, to: Address, token_id: u32, caller: Address) {
Base::mint(e, &to, token_id)

<Self as NonFungibleToken>::ContractType::mint_with_id(e, &to, token_id)
}


Expand All @@ -213,6 +216,14 @@ impl MyContract {
String::from_str(e, "multi_role_auth_action_success")
}
}

#[contractimpl(contracttrait)]
impl NonFungibleToken for MyContract {
type ContractType = Compose<(Base,)>;
}

#[contractimpl(contracttrait)]
impl AccessControl for MyContract {}
```

## Benefits and Trade-offs
Expand Down
27 changes: 22 additions & 5 deletions content/stellar-contracts/access/ownable.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ This feature is useful for contracts that need to become fully decentralized aft
A common use case is renouncing ownership after initial setup to ensure the contract becomes immutable and cannot be upgraded:

```rust
use soroban_sdk::{contract, contractimpl, Address, Env};
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env};
use stellar_access::ownable::{self as ownable, Ownable};
use stellar_contract_utils::upgradeable;
use stellar_macros::only_owner;

#[contract]
Expand All @@ -45,7 +46,8 @@ impl MyContract {
ownable::set_owner(e, &initial_owner);
}

#[only_owner]
// No `#[only_owner]` here: `renounce_ownership` already requires the owner's
// authorization, and requiring it twice in the same call would panic.
pub fn finalize_and_lock(e: &Env) {
// Perform any final configuration here
// ...
Expand All @@ -60,9 +62,12 @@ impl MyContract {
#[only_owner]
pub fn emergency_upgrade(e: &Env, new_wasm_hash: BytesN<32>) {
// This function becomes permanently inaccessible after renounce_ownership
e.deployer().update_current_contract_wasm(new_wasm_hash);
upgradeable::upgrade(e, &new_wasm_hash);
}
}

#[contractimpl(contracttrait)]
impl Ownable for MyContract {}
```

**Important Notes:**
Expand All @@ -75,7 +80,7 @@ impl MyContract {

The module includes a procedural macro to simplify owner authorization checks:

#### @only_owner
#### `#[only_owner]`

Ensures the caller is the owner before executing the function:

Expand All @@ -86,7 +91,12 @@ pub fn restricted_function(e: &Env, other_param: u32) {
}
```

This expands to code that retrieves the owner from storage and requires authorization before executing the function body.
This expands to a call to `stellar_access::ownable::enforce_owner_auth(e)` at the start of the function body, which
retrieves the owner from storage and requires its authorization. It panics with `OwnableError::OwnerNotSet` (`2100`)
if no owner is set, e.g. after ownership has been renounced.

Don't add `#[only_owner]` to a function that calls `ownable::transfer_ownership()` or `ownable::renounce_ownership()`:
these already enforce the owner's authorization, and requiring it twice in the same call panics.

## Usage Example

Expand Down Expand Up @@ -119,8 +129,15 @@ impl MyContract {
42
}
}

// Exposes `get_owner`, `transfer_ownership`, `accept_ownership` and `renounce_ownership`
#[contractimpl(contracttrait)]
impl Ownable for MyContract {}
```

For a complete contract with tests, see the
[ownable example](https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/ownable).

## Benefits and Trade-offs

### Benefits
Expand Down
60 changes: 45 additions & 15 deletions content/stellar-contracts/fee-abstraction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ sequenceDiagram
Relayer->>FeeForwarder: 7. Execute forward():<br/>submit a tx and pay XLM fees

FeeForwarder->>FeeForwarder: 8. Validate authorizations
FeeForwarder->>Token: 9. Approve max fee amount (optional)
FeeForwarder->>Token: 10. Transfer fee amount to fee recipient
alt Eager approval
FeeForwarder->>Token: 9. Approve max_fee_amount
FeeForwarder->>Token: 10. Pull max_fee_amount from user,<br/>pay fee_amount to fee recipient,<br/>refund the remainder to user
else Lazy approval
FeeForwarder->>Token: 9. Approve max_fee_amount<br/>(only if allowance is insufficient)
FeeForwarder->>Token: 10. Transfer fee_amount from user to fee recipient
end
FeeForwarder->>Target Contract: 11. Invoke target_fn(target_args)
Target Contract-->>User: Result
```
Expand All @@ -57,7 +62,7 @@ FeeForwarder.forward()
└─ Target.target_fn() ← nested sub-invocation (if needed)
```

Equally important for the fee abstraction model is enabling the relayer to collect the compensation fee. This is done through the common 2-step fungible token flow of `approve` + `transfer_from`. Soroban authorization framework allows embedding the approval step in the same transaction, so the user might also need to sign `Token.approve()`.
Equally important for the fee abstraction model is enabling the relayer to collect the compensation fee. This relies on the fungible token allowance mechanism: the user approves the FeeForwarder, which then pulls the tokens with `transfer_from`. Soroban authorization framework allows embedding the approval step in the same transaction, so the user might also need to sign `Token.approve()`.

To guarantee the atomicity of both, the target invocation and the fee collection, the [fee-abstraction](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/fee-abstraction) package exposes the function `collect_fee_and_invoke()`, which, in most of the cases, will require signing an authorization tree that contains one entry with two sub-invocations:

Expand All @@ -71,15 +76,35 @@ The approval sub-invocation might be optional depending on the chosen approval s

1. **Lazy**

The user pre-approves a lump sum to the FeeForwarder contract in a separate transaction. Each forwarded call then draws from this existing allowance. This strategy is more resource efficient per call, but requires an initial approval transaction and trust in the FeeForwarder contract.
The FeeForwarder approves `max_fee_amount` only when the user's current allowance is below it, then transfers
`fee_amount` from the user directly to the fee recipient with `transfer_from`. The `Token.approve()` sub-invocation
is thus needed only when the allowance is insufficient, and calls covered by an existing allowance (left over from
a previous call or approved separately) don't need it. The unspent remainder stays approved for later calls, which
makes this strategy cheaper per call, but requires trust in the FeeForwarder contract.

2. **Eager**

The approval is embedded directly in the authorization entry as a sub-invocation for every call, no preliminary transactions needed. This strategy is self-contained with no trust assumptions, but more expensive per call (~25%) due to the nested approval.
The FeeForwarder always approves `max_fee_amount` (overwriting any existing allowance), pulls the full
`max_fee_amount` from the user with `transfer_from`, pays `fee_amount` to the fee recipient (skipped when the
recipient is the FeeForwarder itself) and refunds the remainder to the user. The approval is embedded as a
sub-invocation in every call, but no allowance is left behind, so there are no trust assumptions. This strategy is
more expensive per call due to the approval and the extra transfers, and the user needs a spendable balance of at
least `max_fee_amount`, not just `fee_amount`.

<Callout type="warning">
Eager requires a balance-conserving fee token, i.e. one whose transfers credit the receiver exactly the amount
debited from the sender. The FeeForwarder pays out the fee and the remainder regardless of the amount it received, so
with fee-on-transfer or other non-conserving tokens the shortfall is drawn from the FeeForwarder's own balance, or the
call reverts when that balance is insufficient. Use Lazy for such tokens, as it transfers directly from the user to
the fee recipient. In both strategies, the `fee_collected` event reports the requested `fee_amount`, which can exceed
what the recipient actually receives from a non-conserving token.
</Callout>

## Fee Token Allowlist

The package includes optional allowlist functionality to restrict which tokens can be used for fee payment:
The package includes optional allowlist functionality to restrict which tokens can be used for fee payment. The
allowlist is enabled implicitly once at least one token is added: while it is empty (including after the last token
is removed), every token is allowed. `is_fee_token_allowlist_enabled` reports whether it is currently enabled.

```rust
// Enable a token for fee payments
Expand All @@ -92,7 +117,12 @@ set_allowed_fee_token(e, &deprecated_token, false);
let is_allowed = is_allowed_fee_token(e, &token);
```

The allowlist uses a swap-and-pop algorithm for efficient O(1) removal and automatically extends TTL for frequently-used tokens.
Allowing a token that is already allowed panics with `FeeTokenAlreadyAllowed`, and disallowing a token that is not
allowed panics with `FeeTokenNotAllowed`.

The allowlist uses a swap-and-pop algorithm for efficient O(1) removal and automatically extends TTL for frequently-used
tokens. When a removal moves the last token into the freed slot, the TTL of the moved token's storage entries is
extended as well.

## Token Sweeping

Expand Down Expand Up @@ -126,7 +156,7 @@ A role-based implementation where only authorized executors can relay transactio
- Uses `#[only_role]` macro for executor authorization
- Collects fees to the contract (not directly to relayer)
- Managers can sweep accumulated fees to designated recipients
- Uses lazy approval strategy (requires pre-approval transaction)
- Uses lazy approval strategy (`Token.approve()` sub-invocation needed only when the allowance is insufficient)
- Suitable for trusted relayer networks

```rust
Expand All @@ -143,7 +173,7 @@ pub fn forward(
user: Address,
relayer: Address,
) -> Val {
collect_fee_then_invoke(
collect_fee_and_invoke(
e,
&fee_token,
fee_amount,
Expand All @@ -166,7 +196,7 @@ An open implementation where anyone can act as a relayer. Fees go directly to th
**Characteristics:**
- No role restrictions
- Fees transfer directly to the relayer
- Uses eager approval strategy (approval embedded in each call)
- Uses eager approval strategy (approval embedded in each call, no allowance left behind)
- Suitable for open relayer markets

```rust
Expand Down Expand Up @@ -202,7 +232,7 @@ pub fn forward(

## Integration with OpenZeppelin Relayer

The fee abstraction package is designed to work seamlessly with [OpenZeppelin Relayer](/relayer/1.3.x). The Relayer can:
The fee abstraction package is designed to work seamlessly with [OpenZeppelin Relayer](/relayer/1.5.x). The Relayer can:

1. **Submit sponsored transactions**: Pay XLM fees on behalf of users
2. **Calculate optimal fees**: Determine the actual fee based on network conditions
Expand All @@ -223,16 +253,16 @@ sequenceDiagram
User->>User: Sign authorization
User->>OZ Relayer: Submit signed authorization
OZ Relayer->>Forwarder: Execute forward (pays XLM)
Forwarder->>Target: Invoke user's action
Forwarder->>OZ Relayer: Transfer token fee
Forwarder->>Target: Invoke user's action
OZ Relayer-->>User: Transaction complete
```

For detailed integration instructions, see the [Stellar Sponsored Transactions Guide](/relayer/1.3.x/guides/stellar-sponsored-transactions-guide).
For detailed integration instructions, see the [Stellar Sponsored Transactions Guide](/relayer/1.5.x/guides/stellar-sponsored-transactions-guide).

## See Also

- [Smart Accounts](/stellar-contracts/accounts/smart-account)
- [Access Control](/stellar-contracts/access/access-control)
- [OpenZeppelin Relayer](/relayer/1.3.x)
- [Stellar Sponsored Transactions Guide](/relayer/1.3.x/guides/stellar-sponsored-transactions-guide)
- [OpenZeppelin Relayer](/relayer/1.5.x)
- [Stellar Sponsored Transactions Guide](/relayer/1.5.x/guides/stellar-sponsored-transactions-guide)
Loading
Loading