Contract source:
contracts/src/lib.rscontracts/src/types.rscontracts/src/portfolio.rscontracts/src/templates.rscontracts/src/reflector.rs
For common invocation examples and debugging commands, see the Soroban Cookbook. For main domain terms used in this contract, see docs/GLOSSARY.md.
- Purpose: One-time contract initialization with admin and Reflector oracle contract addresses.
- Parameters:
env: Soroban execution environment.admin: Admin address stored for privileged actions (for example emergency stop).- This may be a standard account or a contract-managed/governance address.
- Future multisig or governed deployments can provide an address that authorizes via Soroban auth rules.
reflector_address: Reflector oracle contract address used for price lookups.
- Returns:
Ok(())on success,Err(Error::AlreadyInitialized)if already initialized. - Preconditions:
- Contract must not already be initialized.
- Purpose: Reads the configured admin address from contract instance storage.
- Parameters:
env: Soroban execution environment.
- Returns: Stored admin
Address. - Notes:
- External clients can use this to confirm the configured governance/admin address before invoking privileged actions.
create_portfolio(env: Env, user: Address, target_allocations: Map<Address, u32>, asset_decimals: Map<Address, u32>, rebalance_threshold: u32, slippage_tolerance: u32, slippage_policy_version: u32) -> Result<u64, Error>
- Purpose: Creates a new user portfolio (with default
StrategyType::Thresholdstrategy) and emits a("portfolio","created")event. - Parameters:
user: Portfolio owner; must authorize this call.target_allocations: Target allocations per asset (Address -> percentage).asset_decimals: Decimal precision per asset (Address -> decimals).rebalance_threshold: Drift threshold percent (1..=50).slippage_tolerance: Slippage tolerance in basis points (10..=500).slippage_policy_version: Policy version matchingCURRENT_SLIPPAGE_POLICY_VERSION.
- Returns:
Ok(portfolio_id)or one of:Err(Error::InvalidAllocation)Err(Error::InvalidAssetDecimals)Err(Error::TooManyAssets)Err(Error::InvalidThreshold)Err(Error::InvalidSlippageTolerance)Err(Error::PortfolioStorageFootprintTooLarge)Err(Error::UnsupportedSlippagePolicyVersion)
- Preconditions:
user.require_auth()succeeds.- Allocation map passes
portfolio::validate_allocations. - Asset count is
<= MAX_PORTFOLIO_ASSETS(10).
create_portfolio_with_strategy(env: Env, user: Address, target_allocations: Map<Address, u32>, asset_decimals: Map<Address, u32>, rebalance_threshold: u32, slippage_tolerance: u32, slippage_policy_version: u32, strategy: StrategyType, strategy_config: StrategyConfig) -> Result<u64, Error>
- Purpose: Creates a new user portfolio with an explicit rebalancing strategy and emits a
("portfolio","created")event. - Parameters:
- Same as
create_portfolioplus: strategy:StrategyTypeenum —Threshold (0),Periodic (1),Volatility (2), orCustom (3).strategy_config:StrategyConfigstruct with fieldsinterval_seconds,volatility_threshold_bps,min_interval_seconds.
- Same as
- Returns: Same as
create_portfolio.
- Strategy: Portfolio IDs are allocated from a monotonically increasing
counter stored in persistent contract storage under
DataKey::NextPortfolioId. The counter starts at1and increments by one for each created portfolio. - Behavioral guarantee: Given the same contract persistent state, the
assigned portfolio id for a
create_portfolioinvocation is deterministic. Off-chain systems may rely on this stable mapping to correlate portfolios across sync operations. - Notes: The contract exposes
get_portfolioto read portfolio contents by id. Consumers should store the returned id along with the portfolio metadata to maintain a canonical reference.
- Purpose: Admin-only: stores a new named on-chain allocation template. Templates are persistent contract storage, not hardcoded contract logic, so new named presets (e.g. "Conservative", "Balanced", "Aggressive") can be added or changed without a contract upgrade.
- Parameters:
name: Template name; must not already be in use.allocations: Target allocations per asset (Address -> percentage), same shape and validation ascreate_portfolio'starget_allocations.
- Returns:
Ok(())or one of:Err(Error::InvalidAllocation)— allocations do not sum to 100% or contain a zero percentage.Err(Error::TooManyAssets)— the allocation map has more thanMAX_PORTFOLIO_ASSETS(10) entries, same limitcreate_portfolioenforces.Err(Error::PortfolioStorageFootprintTooLarge)— a portfolio instantiated from this template would exceedMAX_PORTFOLIO_STORAGE_BYTES, estimated using default asset decimals and no balances yet (a lower bound on the real footprint).Err(Error::TemplateAlreadyExists)— a template with this name already exists; useupdate_templateinstead.Err(Error::TooManyTemplates)— the template registry already holdsMAX_TEMPLATES(50) entries.
- Preconditions:
admin.require_auth()succeeds. - Notes: These limits exist so a template accepted here can, under normal use, be turned into a portfolio later via
create_portfolio_from_templatewithout hittingcreate_portfolio's own asset-count and storage-footprint checks. This is not an absolute guarantee: the footprint estimate here assumesasset_decimalsat portfolio-creation time has exactly one entry per allocated asset (ascreate_portfolio_from_template's ownasset_decimalsparameter normally would). Since decimal values are fixed-width and don't affect the serialized size, only the number ofasset_decimalsentries matters — a caller who supplies extra, unrelated entries inasset_decimalsatcreate_portfolio_from_templatetime can still exceedMAX_PORTFOLIO_STORAGE_BYTESdespite the template having been accepted here.
- Purpose: Admin-only: replaces the allocations of an existing named template.
- Parameters: Same as
create_template. - Returns:
Ok(())or one of:Err(Error::InvalidAllocation)Err(Error::TooManyAssets)Err(Error::PortfolioStorageFootprintTooLarge)Err(Error::TemplateNotFound)— no template with this name exists; usecreate_templateinstead.
- Preconditions:
admin.require_auth()succeeds.
- Purpose: Public read-only view of a template's stored allocations.
- Returns:
Some(allocations)if the template exists,Noneotherwise. - Preconditions: None; callable without signing.
- Purpose: Public read-only view of all known template names, in creation order.
- Preconditions: None; callable without signing.
create_portfolio_from_template(env: Env, user: Address, template_name: String, asset_decimals: Map<Address, u32>, rebalance_threshold: u32, slippage_tolerance: u32, slippage_policy_version: u32) -> Result<u64, Error>
- Purpose: Creates a new user portfolio using the allocations stored under a named
template instead of passing
target_allocationsdirectly. Shares the same validation, storage-footprint checks, and("portfolio","created")event emission ascreate_portfolio. - Parameters: Same as
create_portfolio, excepttarget_allocationsis replaced bytemplate_name. - Returns:
Ok(portfolio_id)or any errorcreate_portfoliocan return, plus:Err(Error::TemplateNotFound)— no template with this name exists.
- Preconditions:
user.require_auth()succeeds.- A template named
template_nameexists.
- Purpose: Reads a stored portfolio by ID.
- Parameters:
portfolio_idunique integer ID. - Returns:
Portfoliovalue from persistent storage. - Preconditions:
- Portfolio must exist; otherwise contract panics on
.unwrap().
- Portfolio must exist; otherwise contract panics on
- Purpose: Checks internal consistency and structural invariants of a stored portfolio.
- Parameters:
portfolio_idunique integer ID. - Returns:
Ok(())if valid,Err(Error::InvariantViolation)orErr(Error::PortfolioNotFound).
deposit(env: Env, portfolio_id: u64, asset: Address, amount: i128, memo: String) -> Result<(), Error>
- Purpose: Deposits an amount into
current_balancesfor a portfolio and emits("portfolio","deposit"). - Parameters:
portfolio_id: Target portfolio.asset: Asset address key used incurrent_balances.amount: Amount to add.memo: Caller-supplied deposit memo included in the emitted event.
- Returns:
Ok(())on success, or one of:Err(Error::InvalidWithdrawAmount)— amount is zero or negative.Err(Error::EmergencyStop)— contract is in emergency stop.Err(Error::PortfolioPaused)— portfolio is inactive/paused.
- Event payload:
(portfolio_id: u64, asset: Address, amount: i128, memo: String) - Preconditions / failure behavior:
- Portfolio must exist (otherwise returns
Error::PortfolioNotFound). - Steward or portfolio owner authorization required (
steward.require_auth()).
- Portfolio must exist (otherwise returns
- Purpose: Withdraws an amount from
current_balancesfor a portfolio and emits("portfolio","withdraw"). - Parameters:
portfolio_id: Target portfolio.asset: Asset address key used incurrent_balances.amount: Amount to withdraw.
- Returns:
Ok(())on success, or one of:Err(Error::InvalidWithdrawAmount)— amount is zero or negative.Err(Error::EmergencyStop)— contract is in emergency stop.Err(Error::InsufficientBalance)— asset balance is less than requested amount.
- Preconditions / failure behavior:
- Portfolio owner authorization required (
portfolio.user.require_auth()).
- Portfolio owner authorization required (
- Purpose: Computes current drift versus target allocations using Reflector prices.
- Parameters:
portfolio_id. - Returns:
truewhen any tracked asset drift exceedsrebalance_threshold, elsefalse. - Preconditions / failure behavior:
- Portfolio and
ReflectorAddressmust exist in storage (panics on missing values). - Reflector timeout/unavailability semantics: if any held asset has missing or stale price data, the function returns
falsebecause a deterministic rebalance decision cannot be made.
- Portfolio and
execute_rebalance(env: Env, portfolio_id: u64, actual_balances: Map<Address, i128>) -> Result<(), Error>
- Purpose: Validates post-trade balances against slippage tolerance (per
slippage_policy_versionon the portfolio), updateslast_rebalance, and emits("portfolio","rebalanced"). - Parameters:
portfolio_id: Portfolio to rebalance.actual_balances: Actual balances used for slippage checks.
- Preconditions / failure behavior:
- Portfolio must exist and steward/owner must authorize call.
admin_force_rebalance(env: Env, portfolio_id: u64, actual_balances: Map<Address, i128>) -> Result<(), Error>
- Purpose: Allows the admin to force execute a rebalance on a portfolio, bypassing the standard cooldown check. Emits
("portfolio","rebalanced")and cooldown override events. - Parameters:
portfolio_id: Portfolio to rebalance.actual_balances: Actual balances used for slippage checks.
- Preconditions:
- Admin address stored in
DataKey::Adminmust authorize the call.
- Admin address stored in
- Purpose: Toggles emergency stop flag in instance storage and records
ContractPauseReason. - Parameters:
stopboolean. - Returns: No return value.
- Preconditions:
- Admin address stored in
DataKey::Adminmust authorize the call. - The configured admin may be a multisig/governance contract address, as long as it authorizes via Soroban auth.
- Admin address stored in
- Purpose: Transfers the stewardship of a portfolio to a new steward address. Emits
("portfolio","steward_transferred"). - Parameters:
portfolio_id: Target portfolio ID.new_steward: New steward address.
- Preconditions:
- Current steward (or portfolio owner if no steward set) must authorize the call.
- Purpose: Returns the configured steward address for a portfolio, defaulting to the portfolio owner if not explicitly set.
- Purpose: Read-only capability surface returning the overall deployed contract logic version (
CONTRACT_VERSION). - Returns:
1. - Usage: Used by frontend and backend clients during startup and compatibility checks to verify contract compatibility cheaply without attempting writes.
- Purpose: Read-only capability surface returning the expected contract event schema version (
CONTRACT_EVENT_SCHEMA_VERSION). - Returns:
1. - Usage: Used by frontend and backend clients during startup and compatibility checks to ensure event parsers and indexers match the deployed event formats.
- Purpose: Read-only capability surface returning a bitmask representing active on-chain contract capabilities (
CapabilityFlag). - Returns: Bitmask containing flags for
PerPortfolioSteward,DifferentiatedPricing, andEmergencyStop. - Usage: Allows external callers to detect deployed contract capabilities cheaply before attempting writes.
- Purpose: Unified read-only capability surface that aggregates version, schema version, capability flags, and key configuration parameters into a single cheap read call.
- Returns:
ContractCapabilitySummarystruct containingversion,schema_version,capability_flags,min_rebalance_threshold,max_rebalance_threshold,min_slippage_tolerance_bps,max_slippage_tolerance_bps, andmax_portfolio_assets. - Usage: Designed specifically for frontend and backend callers during startup and compatibility checks to confirm all supported contract limits and features in one query before attempting writes.
- Purpose: Sets fee configuration for the contract. Disabled by default (
enabled: false). - Parameters:
config:FeeConfigstruct withfee_bps: u32,fee_recipient: Address,enabled: bool.
- Returns: No return value.
- Preconditions:
- Admin address must authorize the call.
- Purpose: Returns the current fee configuration.
- Returns:
FeeConfigwithenabled: falsedefaults when not yet set.
- Purpose: Upgrades the contract WASM to a new version. Emits
("portfolio","upgraded")event. - Parameters:
new_wasm_hash: 32-byte WASM hash of the new contract code.
- Returns: No return value.
- Event payload:
UpgradeEvent { from_hash: Bytes, to_hash: Bytes, timestamp: u64 } - Preconditions:
- Admin address must authorize the call.
- Purpose: Returns the minimum allowed rebalance threshold percentage.
- Returns:
MIN_REBALANCE_THRESHOLD(currently1).
- Purpose: Returns the maximum allowed rebalance threshold percentage.
- Returns:
MAX_REBALANCE_THRESHOLD(currently50).
- Purpose: Returns the minimum allowed slippage tolerance in basis points.
- Returns:
MIN_SLIPPAGE_TOLERANCE_BPS(currently10).
- Purpose: Returns the maximum allowed slippage tolerance in basis points.
- Returns:
MAX_SLIPPAGE_TOLERANCE_BPS(currently500).
- Purpose: Returns the maximum number of assets allowed in a portfolio.
- Returns:
MAX_PORTFOLIO_ASSETS(currently10).
- Purpose: Non-mutating simulation path for backend dry-run APIs. Returns a
RebalancePreviewstruct detailing candidate trades, skipped assets, skip reasons, threshold decisions, and whether a rebalance is needed. - Parameters:
portfolio_id: Portfolio to preview rebalance for.
- Preconditions:
- Does not require portfolio owner authorization and does not mutate persistent storage.
- Purpose: Pauses a specific portfolio and records the pause reason.
- Parameters:
portfolio_id: Target portfolio ID.reason:PauseReasonenum value.
- Purpose: Returns the current contract-level pause reason.
- Returns:
PauseReason(defaults toPauseReason::Noneif active).
Error is declared with #[repr(u32)], so values are stable numeric codes:
| Code | Variant | Description | Recovery Action |
|---|---|---|---|
1 |
InvalidAllocation |
Target allocation percentages do not sum to 100% or individual allocations are zero. | Verify allocations in your create_portfolio call sum to exactly 100. Each asset must have a positive percentage. |
2 |
RebalanceNotNeeded |
No asset drift exceeds the portfolio's configured rebalance threshold. | This is informational — no action needed. Increase the threshold sensitivity if you want more frequent rebalancing. |
3 |
EmergencyStop |
Contract is in emergency stop mode; all state-mutating operations are blocked. | Wait for the admin to disable the emergency stop. Check the set_emergency_stop event logs for the reason code. |
4 |
CooldownActive |
A rebalance was executed too recently; the cooldown period has not elapsed. | Wait for the cooldown period to pass. |
5 |
StaleData |
Reflector oracle price data is stale or missing. | Retry after oracle data refreshes. Verify the Reflector contract address is correct and the oracle is operational. |
6 |
ExcessiveDrift |
Computed portfolio drift exceeds the allowed maximum. | Review your target allocations. Consider rebalancing in smaller steps or adjusting the rebalance threshold to a higher value. |
7 |
AlreadyInitialized |
The initialize function was called on an already-initialized contract. |
No action needed — the contract is already set up. |
8 |
InvalidThreshold |
Rebalance threshold is outside the allowed range (1–50%). | Provide a rebalance_threshold between MIN_REBALANCE_THRESHOLD (1) and MAX_REBALANCE_THRESHOLD (50). |
9 |
InvalidSlippageTolerance |
Slippage tolerance is outside the allowed range (10–500 bps). | Provide a slippage_tolerance between MIN_SLIPPAGE_TOLERANCE_BPS (10) and MAX_SLIPPAGE_TOLERANCE_BPS (50). |
10 |
SlippageExceeded |
Post-trade balances deviated beyond the portfolio's configured slippage tolerance. | Increase slippage_tolerance on the portfolio or split the rebalance into smaller trades. Check market liquidity for the affected assets. |
11 |
TooManyAssets |
A portfolio's target allocation map exceeds MAX_PORTFOLIO_ASSETS (10). |
Reduce the number of assets in the target_allocations map to 10 or fewer. |
12 |
StaleOraclePrice |
Reflector oracle price data is stale. | Retry after oracle data refreshes. |
13 |
InvalidAssetThreshold |
Asset threshold configuration is invalid. | Provide valid threshold configuration. |
14 |
InvariantViolation |
An internal contract invariant was violated — this indicates a bug. | Report this error with the full transaction envelope to the maintainers. Include the portfolio ID, contract version, and triggering operation. |
15 |
InvalidAssetDecimals |
An asset's decimal count exceeds MAX_ASSET_DECIMALS (18) or is otherwise invalid. |
Verify the asset's decimal configuration. Stellar assets typically use 7 decimals; other assets may use up to 18. |
16 |
UnsupportedSlippagePolicyVersion |
The portfolio's slippage_policy_version is not recognized by the current contract version. |
Upgrade the contract to a version that supports the portfolio's policy version, or recreate the portfolio with the current CURRENT_SLIPPAGE_POLICY_VERSION. |
17 |
InvalidWithdrawAmount |
The withdrawal or deposit amount is zero, negative, or invalid. | Provide a positive amount. |
18 |
PortfolioPaused |
The portfolio is in a paused state (user-paused, admin emergency, or circuit breaker). | Check the portfolio's pause_reason field to determine the cause. Admin can toggle emergency stop; user may need to unpause. |
19 |
InsufficientBalance |
The portfolio's current balance is insufficient for the requested operation. | Deposit additional funds into the portfolio before retrying the operation. Verify current_balances via get_portfolio. |
20 |
MissingPrice |
A required asset price could not be retrieved from the Reflector oracle. | Ensure the Reflector oracle contract is deployed and reachable. Verify the asset key matches the reflector's supported asset list. |
21 |
PortfolioNotFound |
The requested portfolio ID does not exist in persistent contract storage. | Verify the portfolio ID is correct. |
22 |
PortfolioStorageFootprintTooLarge |
The serialized portfolio struct exceeds MAX_PORTFOLIO_STORAGE_BYTES (3072 bytes). |
Reduce the number of assets in the portfolio. Each asset adds to the storage footprint of the target_allocations, current_balances, and asset_decimals maps. |
23 |
PreviewUnavailable |
The simulation path cannot generate a rebalance preview due to missing data. | Ensure the Reflector oracle is returning price data for all portfolio assets. Retry the simulation when oracle data is available. |
24 |
InvalidCooldown |
The cooldown duration is invalid. | Provide a valid cooldown setting. |
25 |
AssetNotSupported |
An asset in the portfolio has no price data available from the Reflector oracle. | Verify the asset is listed in the Reflector oracle. Check the asset's contract address or Stellar issuer is correctly specified. |
26 |
InvalidAmount |
A deposit or trade amount is zero, negative, or below the minimum trade size. | Provide a positive amount greater than the minimum trade size. |
27 |
WithdrawFailed |
A withdrawal operation could not be completed. | Check that the portfolio has sufficient balance and is not paused. Verify the withdrawal amount does not exceed available balances. |
28 |
InvalidAllocationSum |
A portfolio's target allocations no longer sum to exactly 100% at rebalance time. | Update the portfolio's target allocations so they sum to exactly 100% before retrying. |
29 |
BatchTooLarge |
batch_rebalance was called with more than MAX_BATCH_REBALANCE_PORTFOLIOS (10) portfolio IDs. |
Split the batch into groups of 10 or fewer portfolio IDs per call. |
30 |
InvalidOracleAddress |
The address passed to initialize as reflector_address does not behave like a Reflector oracle (its base() call failed or returned unexpectedly). |
Verify the Reflector contract address is correct and deployed on the target network before calling initialize. |
31 |
TemplateNotFound |
update_template or create_portfolio_from_template referenced a template name that does not exist. |
Call list_templates to see available names, or create_template first. |
32 |
TemplateAlreadyExists |
create_template was called with a name that is already in use. |
Use update_template to change an existing template, or choose a different name. |
33 |
TooManyTemplates |
The template registry already holds MAX_TEMPLATES (50) entries. |
There is no delete entrypoint. Repurpose an existing template's allocations via update_template instead of creating a new one. |
For common invocation examples and debugging commands, see the Soroban Cookbook.
The contract uses Soroban contract types (#[contracttype]) which are encoded as Soroban ScVal/XDR values over RPC.
Address(soroban_sdk::Address)- Used for users, assets, and external contract references.
Map<Address, u32>- Used for
target_allocations,asset_decimals, and percentage or decimal metadata.
- Used for
Map<Address, i128>- Used for
current_balances,actual_balances, andcandidate_trades.
- Used for
Portfolio(contracts/src/types.rs)- Composite struct:
user: Addresstarget_allocations: Map<Address, u32>current_balances: Map<Address, i128>asset_decimals: Map<Address, u32>rebalance_threshold: u32slippage_tolerance: u32slippage_policy_version: u32last_rebalance: u64total_value: i128is_active: boolpause_reason: PauseReasonstrategy: StrategyType— rebalancing strategy (Threshold,Periodic,Volatility,Custom)strategy_config: StrategyConfig— per-strategy parameters
StrategyType(contracts/src/types.rs)- Enum:
Threshold = 0,Periodic = 1,Volatility = 2,Custom = 3 - Mirrors backend
RebalanceStrategyType(excludingdca, which is handled separately)
- Enum:
StrategyConfig(contracts/src/types.rs)- Struct:
interval_seconds: u64,volatility_threshold_bps: u32,min_interval_seconds: u64 - Default: 7-day interval, 10% volatility threshold, 1-day minimum interval
- Struct:
LegacyPortfolio(contracts/src/types.rs)- Pre-strategy schema Portfolio struct used for on-read migration of existing stored portfolios
ContractCapabilitySummary(contracts/src/types.rs)- Struct with
version: u32,schema_version: u32,capability_flags: u32,min_rebalance_threshold: u32,max_rebalance_threshold: u32,min_slippage_tolerance_bps: u32,max_slippage_tolerance_bps: u32,max_portfolio_assets: u32.
- Struct with
Asset(contracts/src/reflector.rs)- Enum:
Stellar(Address)orOther(Symbol).
- Enum:
PriceData(contracts/src/reflector.rs)- Struct with
price: i128andtimestamp: u64.
- Struct with
For call builders and generated client bindings, use Soroban CLI/SDK tooling against the compiled WASM artifact.
The contract includes a comprehensive property-based test suite using the
proptest crate. These tests verify invariant
properties across 10 000 random input combinations each.
| # | Property | Description | Cases |
|---|---|---|---|
| 1 | Allocation sum invariance | Target allocations always sum to exactly ALLOCATION_DENOMINATOR (10 000 bps). Creating a portfolio with valid allocations always succeeds. |
10 000 |
| 2 | Drift range | Current allocation percentage and drift are always in [0, 10000] range. Portfolio valuation fields are within valid bounds. |
10 000 |
| 3 | Rebalance idempotency | When no rebalance is needed (drift within threshold), the portfolio state is stable — executing a rebalance does not change state. If rebalance IS needed, one execution brings portfolio into a stable state requiring no further rebalance. | 10 000 |
| 4 | Deposit-withdraw roundtrip | Depositing an amount and immediately withdrawing the same amount restores the original user balance. Internal portfolio balance returns to zero. | 10 000 |
| 5 | Random allocation validity | Any randomly generated allocation that sums to 10 000 bps and has 2–10 assets is accepted. Stored allocations match input exactly, and portfolio invariants hold. | 10 000 |
# Run all property tests (50 000 total cases, ~3–5 minutes)
cd contracts
make test-property
# Or directly:
cargo test --features testutils property_ -- --nocaptureThe property test suite runs in CI as part of the contract test pipeline. Test reports are uploaded as CI artifacts for regression analysis.
Each proptest run logs the random seed used. To reproduce a specific failure:
PROPTEST_SEED=<hex-seed> cargo test --features testutils property_The contract includes property-based tests using proptest with 10,000 random inputs per property.
| # | Property | Cases |
|---|---|---|
| 1 | Valid allocations (sum=10000) always accepted | 10,000 |
| 2 | Invalid allocations (sum!=10000) rejected | 10,000 |
| 3 | Deposit+withdraw roundtrip preserves balance | 10,000 |
| 4 | Drift and current_pct in [0,10000] range | 10,000 |
| 5 | Rebalance idempotent when no drift | 10,000 |
Run: cargo test --features testutils property_