
The Factory Pattern
When a user callsAccountManager.create_account(), the Account Manager first checks whether the user has any previously closed accounts sitting in the inactive pool:
(trader_address, account_manager_address, account_index). The address is therefore predictable before deployment. The new account is registered in the Registry.
If an inactive account exists, it is reused directly - no new deployment. The Registry entry is updated to point to it. This amortizes deployment cost across the account’s lifetime.
When an account is closed via close_account(), it is swept, deactivated, and pushed back into the user’s inactive pool for reuse next time.
Storage Layout
Every SmartAccount contract stores the following state in persistent storage:| Storage Key | Type | Description |
|---|---|---|
CollateralTokensList | Vec<Symbol> | All token symbols currently held as collateral |
CollateralBalanceWAD(Symbol) | U256 | Balance of each collateral token in WAD precision |
BorrowedTokensList | Vec<Symbol> | All token symbols with outstanding debt |
HasDebt | bool | True if any borrowed token entry exists |
IsAccountActive | bool | Whether the account is currently active |
OwnerAddress | Address | The wallet that owns and controls this account |
AccountManager | Address | The AccountManager contract authorized to call this account |
RegistryContract | Address | The Registry used to resolve peer contract addresses |
CollateralTokensList grows when a new asset is deposited and shrinks when a balance is fully withdrawn or swept. BorrowedTokensList grows when a borrow is issued and shrinks when the debt for a token is fully repaid. HasDebt is set to false automatically when BorrowedTokensList becomes empty.
Account States
Each SmartAccount contract moves through the following states: Deployed (inactive) - The constructor setsIsAccountActive = false. A freshly deployed contract starts inactive and cannot execute operations.
Active - AccountManager calls activate_account(), which sets IsAccountActive = true. The account is now ready for deposits, borrows, and external calls. This happens immediately after creation or reuse.
Inactive - AccountManager calls deactivate_account() when the user closes the account. The contract remains on-chain but cannot execute new operations. It enters the user’s inactive pool and will be reactivated if the user opens a new account.
How Operations Flow Through the Account
The SmartAccount contract itself cannot be called directly by users. Every operation goes through the Account Manager, which authenticates the caller, checks with the Risk Engine, and only then calls the SmartAccount to mutate state.External Protocol Calls - execute()
When a user deploys borrowed capital into an external protocol, the Account Manager callsSmartAccount.execute() with an action type drawn from the SmartAccExternalAction enum:
| Action | Protocol | Description |
|---|---|---|
Deposit | Blend | Supply tokens to a Blend lending pool, receive b-tokens |
Withdraw | Blend | Withdraw tokens from a Blend lending pool, burn b-tokens |
Swap | Aquarius | Swap tokens in an Aquarius AMM pool |
AddLiquidity | Aquarius | Provide liquidity to an Aquarius pool, receive LP tokens |
RemoveLiquidity | Aquarius | Withdraw liquidity from an Aquarius pool, burn LP tokens |
(bool, i128) where the i128 is the token delta - positive for tokens received, negative for tokens sent.
The Account Manager reads this delta and mints or burns the corresponding Tracking Token on behalf of the SmartAccount:
BLEND_XLM, BLEND_USDC, and AQ_XLM_USDC, converts them to their underlying USD value, and includes them in the collateral sum. See Health Factor for the collateral valuation detail.
sweep_to()
sweep_to(destination) is called in two places: by close_account() (sweeps to the owner) and by liquidate() (sweeps to the trader after debt is cleared).
It iterates every token in CollateralTokensList and calls remove_collateral_token_bal_internal() for each:
- XLM, USDC - the actual token balance is transferred to
destinationvia the token contract. TheCollateralBalanceWADentry is zeroed and the token is removed fromCollateralTokensList. - BLEND_XLM, BLEND_USDC, AQ_XLM_USDC - the
CollateralBalanceWADentry is zeroed and the token is removed fromCollateralTokensList. No token transfer occurs - the underlying b-tokens held in Blend’s pool and LP positions in Aquarius remain in the external protocol and are not unwound.
Why Isolated Contracts
The alternative is a shared mapping: one contract holds all user positions in amapping(address => Position). It is cheaper to deploy but means a single logic bug can affect every user simultaneously.
With isolated contracts, a bug that corrupts one SmartAccount’s storage cannot propagate to another. The Risk Engine calls each account directly - it cannot accidentally read or write the wrong account’s state. The blast radius of any single-account failure is bounded to that account.
Constraints
Every function in SmartAccount begins withaccount_manager.require_auth(). Direct user calls are rejected. The only authorized caller is the AccountManager.
- Users cannot move collateral to another address except through
withdraw_collateral_balance()(which runs a risk check) orsweep_to()(which requires Account Manager authorization). - External calls are limited to protocols the AccountManager supports: currently Blend and Aquarius. Calls to arbitrary contracts are not possible through this interface.
- The owner address is set at deployment and cannot be changed.
Related
- Health Factor - how the Risk Engine computes solvency using SmartAccount state
- Liquidation - what happens to the SmartAccount when health falls below 1.1x
- Lending Pools - where borrow shares are stored
- Registry - how peer contract addresses are resolved at runtime
- Oracle System - how asset prices enter the collateral calculation

