This note documents the contract boundaries under stellar-lend/contracts/ and identifies which crates are canonical deployment targets.
contracts/lending is the canonical deployment target for the lending protocol.
contracts/amm is a separate optional deployment for AMM integration. It is not the source of truth for lending positions.
contracts/hello-world is legacy and not canonical for deployment. It remains in the active workspace at stellar-lend/Cargo.toml only so it compiles against the same shared Soroban SDK pin as the maintained contracts.
| Crate | Purpose | Canonical? |
|---|---|---|
lending |
Core lending state machine and protocol controls | Yes |
amm |
Auxiliary AMM router/integration surface | Optional only |
hello-world |
Older all-in-one prototype with overlapping responsibilities | No |
bridge |
Bridge-specific contract | Separate concern |
common |
Shared library code | Library only |
The active Rust workspace is defined in stellar-lend/Cargo.toml. Soroban SDK
and other shared Rust dependencies must be pinned once under
[workspace.dependencies] and inherited by member crates with
{ workspace = true }.
Current workspace members:
contracts/commoncontracts/hello-worldcontracts/lendingcontracts/multisig
Policy:
- do not pin
soroban-sdkindependently inside member crates - do not introduce a second Soroban SDK version through direct crate-level version specs
- keep shared test dependencies, such as
proptest, in the workspace dependency table when more than one member uses them - treat
contracts/hello-worldas a compatibility consumer of the shared pin, not as the canonical deployment artifact
When upgrading Soroban-related dependencies:
- Update the pinned versions in
stellar-lend/Cargo.tomlunder[workspace.dependencies]. - Keep member crates on
workspace = trueinstead of adding crate-local versions. - Refresh
Cargo.lockfrom the workspace root and verify only the intended Soroban versions remain selected. - Run
cargo build --workspaceandcargo test --workspacefromstellar-lend/before merging.
This keeps Env, Address, test utilities, and generated host types aligned
across all workspace members and makes upgrade review a single-file audit.
lending owns:
- collateral and debt state
- interest accrual and liquidation checks
- pause and emergency lifecycle
- admin and guardian controls
- token receiver flows for deposit and repay
- flash-loan entrypoints
- upgrade and data-store management helpers
This is the contract users, liquidators, and token contracts should treat as authoritative for lending state.
amm owns:
- AMM protocol registry
- swap and liquidity settings
- callback nonce tracking
- AMM operation history
- its own upgrade state
The amm implementation hardens pool initialization against "donation attacks" (where the first depositor inflates the share value to steal from subsequent depositors). On the first deposit (when total_supply == 0), the AMM mints sqrt(amount_0 * amount_1) shares, but subtracts and permanently locks a MINIMUM_LIQUIDITY amount (e.g., 1000 units) to a dead address. This enforces a minimum pool value and makes inflating the LP share price prohibitively expensive.
This invariant is enforced in the add_liquidity execution path, which issues the initial MINIMUM_LIQUIDITY shares to a dead address before returning the remaining sqrt(x*y) - MINIMUM_LIQUIDITY shares to the initial depositor. Subsequent deposits mint shares proportionally using min(amount_0 * total_supply / reserve_0, amount_1 * total_supply / reserve_1).
It does not own lending solvency state, debt balances, or collateral balances.
The AMM implementation enforces a constant-product invariant guard on all
mutating operations (swaps and liquidity changes). Each path computes the
pool product k = reserve_a * reserve_b before and after the mutation and
asserts the expected monotonic direction: swaps and liquidity additions must
not decrease k (fees are retained in the pool), while liquidity removals
must not increase k. This check is implemented in the helper
assert_k_monotonic in the contracts/amm crate to detect rounding or
implementation errors and revert on violation.
hello-world combines lending, AMM, bridge, governance, analytics, and monitoring concerns in one crate. Because it remains in the active workspace only for compatibility and duplicates functionality now split across maintained crates, it should be treated as historical/reference code rather than the deployment artifact.
lending trusts:
- the configured admin for protocol parameter changes
- the configured guardian only for emergency shutdown initiation
- the configured oracle for read-only price queries
- Soroban token contracts that invoke the receiver hook correctly
- flash-loan receivers to return principal plus fee by the end of the callback transaction
lending does not trust arbitrary users to bypass auth, pause, or recovery gates.
amm trusts:
- registered AMM protocol addresses
- callback identity for registered protocols
- admin-configured slippage and protocol settings
amm should not be treated as the canonical lending state machine.
The lending admin can initialize the contract, set pause flags, set the guardian, set the oracle, set liquidation parameters, set flash-loan fees, initialize operation settings, manage recovery, and operate the upgrade/data-store helpers.
The guardian can trigger emergency_shutdown and nothing else. The guardian cannot modify parameters, move user balances directly, or complete recovery.
The amm admin is intended to initialize AMM settings, register protocols, update settings, and manage upgrades.
Security caveat: the local amm helper require_admin checks equality against stored admin state but does not call require_auth(). That means amm should be treated as an auxiliary contract pending auth hardening.
lending::receive(token_asset, from, amount, payload) dispatches only two actions:
depositrepay
Unknown actions are rejected. Safety here relies on the Soroban token hook calling convention and trusted token contracts, because the hook accepts the token_asset argument as provided and does not independently verify that the invoker matches that token address.
The flash-loan flow is:
- read the lending contract token balance
- transfer tokens to the receiver
- invoke
on_flash_loanon the receiver - read the final token balance
- require repayment of principal plus fee
This is the main external call path in lending.
Current amm swap and liquidity execution paths are still modeled with mock protocol helpers. They track slippage, callback validation, and history, but they are not yet a fully hardened production router.
Reviewed external call surfaces:
- flash-loan token transfer
- flash-loan receiver callback
- oracle
pricequeries in views - token receiver hook entrypoint
Findings:
- user-sensitive entrypoints consistently require auth
- admin setters use explicit auth checks
- pause and recovery gates are checked on high-risk paths
- arithmetic mostly uses
checked_*or widened math viaI256 - flash loans have a dedicated reentrancy guard and repayment check
Caveat:
flash_loan::calculate_feeuses saturating arithmetic instead of returning an explicit overflow error
Reviewed external call surfaces:
- callback validation
- swap/liquidity execution helpers
- admin mutation paths
Findings:
- slippage and amount bounds are validated
- callback nonces provide replay resistance
- arithmetic generally uses checked operations
Caveats:
- admin authorization is incomplete because
require_auth()is missing inrequire_admin - swap/liquidity execution is still mock-style rather than full external protocol integration
lending has a dedicated flash-loan reentrancy guard. No general contract-wide guard exists, so the flash-loan callback remains the most important reviewed reentrancy surface.
amm has no explicit global reentrancy guard today. If it graduates from mocked integration to real external AMM calls, it should add one before production deployment.
Important existing bounds include:
lendingclose factor:1..=10000lendingliquidation incentive:0..=10000lendingflash-loan fee:0..=1000ammslippage bounded by configuredmax_slippage- positive-amount checks on swap and liquidity inputs
AmmContract::swap now requires two additional safety parameters that must
be supplied by every caller:
pub fn swap(
env: Env,
caller: Address,
asset_in: Address,
asset_out: Address,
amount_in: i128,
min_out: i128, // NEW – minimum acceptable output (>= 0)
deadline: u64, // NEW – latest valid ledger timestamp (seconds)
) -> Result<SwapResult, AmmError>
| # | Condition | Error |
|---|---|---|
| 1 | env.ledger().timestamp() > deadline |
DeadlineExpired |
| 2 | amount_out < min_out |
SlippageExceeded |
The deadline is checked before the slippage floor. This means a transaction
that is both stale and would produce sub-minimum output will surface
DeadlineExpired, not SlippageExceeded.
deadlinerejects transactions that were signed for a different market state but sat in the mempool long enough for prices to move against the caller. Pass the current ledger timestamp plus a small grace window (e.g., 30–60 seconds / ledger-time equivalent).min_outprovides a hard slippage floor that prevents sandwich attacks: a block-producer cannot insert trades that move the price against the caller beyond the declared tolerance.
Callers that previously invoked swap(caller, asset_in, asset_out, amount_in)
must now supply min_out and deadline.
Recommended derivation (off-chain, before building the transaction):
// 1. Simulate the swap to get expected_out.
// 2. Apply your slippage tolerance (e.g. 50 bps = 0.5 %).
let min_out = expected_out * (10_000 - slippage_bps) / 10_000;
// 3. Set deadline to now + grace_window (in ledger-time seconds).
let deadline = current_ledger_timestamp + 60; // 60-second window
To opt out of a guard (not recommended in production):
- Pass
min_out = 0to skip the slippage check. - Pass
deadline = u64::MAXto skip the deadline check.
scripts/init.sh hint: the --amm-min-out-bps flag (default 50) is
printed during AMM initialisation as a documentation hint for the suggested
slippage tolerance. It is not stored on-chain.
| Error | Meaning |
|---|---|
AmmError::InvalidAmount (4) |
amount_in <= 0 |
AmmError::InvalidMinOut (5) |
min_out < 0 |
AmmError::DeadlineExpired (6) |
ledger timestamp > deadline |
AmmError::SlippageExceeded (7) |
amount_out < min_out |
- deploy
contracts/lendingas the canonical lending contract - deploy
contracts/ammonly as a separately reviewed auxiliary contract - do not use
contracts/hello-worldas the canonical protocol deployment target