The fluxora_factory contract is an optional wrapper around FluxoraStream designed specifically to enforce treasury compliance policies during stream creation.
The base FluxoraStream contract is highly composable and intentionally un-opinionated about things like maximum stream sizes, minimum durations, and recipient identities. This makes it ideal as a protocol primitive. However, treasuries managing large token reserves often require strict operational policies.
The fluxora_factory acts as a proxy entrypoint to enforce these policies:
- Recipient Allowlist: Streams can only be created for recipients explicitly allowlisted by the admin.
- Deposit Caps: Enforces a
MaxDepositCapon the totaldeposit_amountof a single stream. - Minimum Duration: Enforces a
MinDuration(i.e.end_time - start_time >= min_duration), preventing overly short or instantaneous streams. - Time Relationship Checks: Rejects invalid schedules before calling
FluxoraStream.start_timemust be strictly less thanend_time, andcliff_timemust be within the inclusive[start_time, end_time]window.
The factory mirrors the underlying stream contract's creation-time schedule invariants and returns typed factory errors before making the cross-contract call:
| Condition | Error |
|---|---|
start_time >= end_time |
FactoryError::InvalidTimeRange |
cliff_time < start_time |
FactoryError::InvalidCliff |
cliff_time > end_time |
FactoryError::InvalidCliff |
These checks keep invalid treasury requests on the factory error surface instead of relying on downstream stream-contract panics.
The factory exposes read-only views so UIs, operators, and indexers can inspect policy before routing treasury activity through the wrapper.
| View | Returns | Notes |
|---|---|---|
get_factory_config() |
FactoryConfig { admin, stream_contract, max_deposit, min_duration } |
Reads all instance policy fields. Returns FactoryError::NotInitialized before init. |
is_allowlisted(recipient) |
bool |
Returns true only when the recipient currently has an allowlist entry. Missing entries return false. |
These views are permissionless and do not mutate factory state.
Warning
Because the underlying FluxoraStream contract does not natively enforce these policies, they are only enforced if the stream is created by routing through the factory contract.
If a user (e.g. the treasury multi-sig itself) directly calls create_stream on the FluxoraStream contract, these policies will be bypassed. To truly lock down treasury funds, the token vault or multi-sig must be configured to only approve transactions that invoke the fluxora_factory contract.
The factory contract follows the Checks-Effects-Interactions (CEI) pattern implicitly:
- Checks: Validates the recipient against the allowlist, validates the stream time relationship, and bounds the deposit and duration against the configured caps.
- Effects: No local persistent state changes occur during a successful stream creation.
- Interactions: Makes a cross-contract call to
FluxoraStream::create_stream.
Factory-routed creation has one client-facing entrypoint, but the sender authorization must cover both the wrapper call and the nested stream call:
flowchart TD
client[Client transaction]
factory["fluxora_factory.create_stream(sender, recipient, deposit, rate, start, cliff, end, dust_threshold)"]
stream["fluxora_stream.create_stream(sender, recipient, deposit, rate, start, cliff, end, dust_threshold, None, Linear)"]
token["token.transfer_from(sender -> fluxora_stream, deposit)"]
client --> factory
factory --> stream
stream --> token
The required authorization scopes are:
| Signer | Scope | Why it is required |
|---|---|---|
sender |
fluxora_factory.create_stream(...) with the exact wrapper arguments |
FluxoraFactory::create_stream calls sender.require_auth() after policy checks pass. |
sender |
Nested fluxora_stream.create_stream(...) with the exact stream arguments the factory forwards |
FluxoraStream::create_stream also calls sender.require_auth() before validating and pulling the deposit. |
This is not two independent user intents. A client should build the Soroban
authorization tree so the sender signs the factory invocation and its
fluxora_stream.create_stream sub-invocation in the same transaction. The nested
scope is intentionally narrow: it authorizes only the exact stream creation that
the factory forwards after enforcing recipient, cap, and duration policy.
The stream contract, not the factory, pulls deposit_amount from sender into
the stream contract during fluxora_stream.create_stream. The factory never
custodies the sender's tokens and has no standing privilege to spend sender
funds. If a later transaction tries to reuse the factory or a changed set of
arguments, the sender must authorize that new invocation tree again.
Assume a treasury UI wants to create this routed stream:
sender = G_SENDER
recipient = G_RECIPIENT
deposit_amount = 1_000
rate_per_second = 1
start_time = 1_800_000_000
cliff_time = 1_800_000_000
end_time = 1_800_001_000
withdraw_dust_threshold = 0
The client prepares a transaction whose root host function invokes
fluxora_factory.create_stream with those values. During simulation/preparation,
the authorization tree must contain G_SENDER for the root factory call and the
nested fluxora_stream.create_stream sub-invocation with:
memo = None
kind = Linear
G_SENDER signs that prepared authorization tree. The factory admin does not
sign stream creation unless the admin is also the sender. The recipient does
not sign creation. The recipient signs only later recipient-controlled actions
such as withdraw or withdraw_to.
For UI and wallet copy, describe the flow as "one sender signing session with two scopes" rather than "two unrelated signatures":
- The factory scope lets the sender opt into the treasury policy wrapper.
- The stream scope lets the stream contract create the stream and pull exactly the authorized deposit from the sender.
If the client omits either scope, the transaction fails at the corresponding
require_auth call. If the sub-invocation arguments differ from the signed
arguments, the nested authorization is not valid for that call.
The factory has an Admin key managed via set_admin. The admin can:
- Call
set_allowlistto grant or revoke recipient eligibility. - Call
set_capto update the max deposit limit. - Call
set_min_durationto update the minimum duration requirement. - Call
set_stream_contractto upgrade or switch the underlying stream primitive if a new version is deployed.
The factory admin can shape policy and the target stream contract, but cannot
spend sender funds by itself. A factory-routed stream still needs the sender
authorization described above, and the underlying stream contract still enforces
its own authorization table. See the docs/security.md admin powers
section for the protocol-wide admin boundary.
This document is aligned with the current implementation as follows:
FluxoraFactory::create_streamenforces allowlist, cap, and duration checks before callingsender.require_auth().- The factory forwards a linear
FluxoraStream::create_streamcall withmemo = NoneandStreamKind::Linear. FluxoraStream::create_streamcallssender.require_auth()before validating parameters and pullingdeposit_amountfromsender.contracts/stream/tests/factory_policy.rscovers the factory policy gates and admin-guarded policy updates that surround this authorization model.