TESTNET MODE IS EXTREMELY DANGEROUS FOR PRODUCTION USE
Testnet mode relaxes validations that protect investor funds. Enabling testnet mode on production/mainnet contracts can lead to catastrophic fund loss.
- NEVER enable testnet mode on mainnet/production contracts
- Always verify
is_testnet_mode()returnsfalsebefore production deployment - Admin must ensure testnet mode is disabled before going live
- Integrators must check
is_testnet_mode()in their client code
When enabled, testnet mode bypasses these critical safety checks:
- Revenue Share Validation: Allows
revenue_share_bps > 10000(100%+ distributions) - Concentration Enforcement: Ignores holder concentration limits that prevent manipulation
These relaxations can allow malicious actors to drain all funds from the contract.
The testnet mode feature provides a configuration flag that enables simplified behavior for testnet and development deployments. When enabled, certain strict validations are relaxed to facilitate testing and experimentation without compromising production safety.
Testnet mode is designed for:
- Non-production deployments (testnet, devnet, local development)
- Testing edge cases and boundary conditions
- Rapid prototyping and experimentation
- Integration testing with flexible parameters
When testnet mode is enabled, the following behaviors are modified:
Normal Mode:
revenue_share_bpsmust be ≤ 10,000 (100%)- Values > 10,000 return
InvalidRevenueShareBpserror
Testnet Mode:
revenue_share_bpsvalidation is skipped- Any value is accepted, including > 10,000
- Useful for testing extreme scenarios
Normal Mode:
- If concentration limit is set with
enforce=true,report_revenuefails when reported concentration exceeds the limit - Returns
ConcentrationLimitExceedederror
Testnet Mode:
- Concentration enforcement is skipped
report_revenuesucceeds regardless of concentration- Concentration warnings are still emitted via events
- Set Admin (one-time operation):
contract.set_admin(&admin_address);- Enable Testnet Mode (admin only):
contract.set_testnet_mode(&true);- Verify Mode:
let is_testnet = contract.is_testnet_mode();- Disable Testnet Mode (when moving to production):
contract.set_testnet_mode(&false);// Enable testnet mode
contract.set_admin(&admin);
contract.set_testnet_mode(&true);
// Register offering with > 100% revenue share (for testing)
contract.register_offering(&issuer, &token, &15_000); // 150%
// This would fail in normal mode but succeeds in testnet mode// Enable testnet mode
contract.set_admin(&admin);
contract.set_testnet_mode(&true);
// Set up concentration limit with enforcement
contract.register_offering(&issuer, &token, &5_000);
contract.set_concentration_limit(&issuer, &token, &5000, &true);
// Report high concentration
contract.report_concentration(&issuer, &token, &8000); // 80% > 50% limit
// Report revenue - succeeds in testnet mode, would fail in normal mode
contract.report_revenue(&issuer, &token, &1_000_000, &1);Testnet mode bypasses validations designed to protect investor funds. Enabling it on production contracts can result in:
- Over-distribution:
revenue_share_bps > 10000allows distributions exceeding 100% - Concentration manipulation: Bypassed enforcement allows unlimited holder concentration
- Fund drainage: Malicious issuers could extract all contract funds
- Only the contract admin can toggle testnet mode
- Requires
set_admin()to be called first - Admin authorization is enforced via
require_auth()
- Testnet mode is disabled by default
- Must be explicitly enabled by admin
- Can be toggled on/off at any time
- Mode changes emit events for auditability
- CRITICAL: Always verify
is_testnet_mode() == falsebefore production use
All integrators and frontends MUST implement these checks:
// ALWAYS check testnet mode before using the contract
let is_testnet = contract.is_testnet_mode();
if is_testnet {
panic!("CRITICAL: Contract has testnet mode enabled - DO NOT USE FOR PRODUCTION");
}pub fn connect_to_revora_contract(contract_id: &str) -> Result<RevoraClient> {
let client = RevoraClient::new(contract_id);
// Mandatory safety check
if client.is_testnet_mode()? {
return Err(Error::TestnetModeEnabled);
}
Ok(client)
}Before deploying to production:
- Deploy contract
- Verify
is_testnet_mode()returnsfalse - Document the check in deployment scripts
- Include in security audits
The following operations work identically in both modes:
- Blacklist management
- Pagination
- Audit summaries
- Claim operations
- Rounding modes
- All read-only queries
Testnet mode changes emit the test_mode event:
Topic: (test_mode, admin_address)
Payload: enabled (bool)
This allows off-chain systems to track when testnet mode is toggled.
The feature includes comprehensive test coverage (95%+):
testnet_mode_disabled_by_default- Verifies default stateset_testnet_mode_requires_admin- Admin authorizationtestnet_mode_can_be_toggled- Enable/disable cyclesset_testnet_mode_emits_event- Event emission
testnet_mode_allows_bps_over_10000- BPS validation skiptestnet_mode_disabled_rejects_bps_over_10000- Normal mode enforcementtestnet_mode_skips_concentration_enforcement- Concentration skiptestnet_mode_disabled_enforces_concentration- Normal mode enforcement
testnet_mode_toggle_after_offerings_exist- Mode change with existing datatestnet_mode_affects_only_validation_not_storage- Storage integritytestnet_mode_multiple_offerings_with_varied_bps- Multiple offerings
testnet_mode_normal_operations_unaffected- Other operations worktestnet_mode_blacklist_operations_unaffected- Blacklist unchangedtestnet_mode_pagination_unaffected- Pagination unchanged
- Enable at deployment: Set admin and enable testnet mode immediately after contract deployment
- Document clearly: Mark testnet contracts in your documentation
- Monitor events: Track
test_modeevents to verify configuration - Test thoroughly: Use testnet mode to test edge cases before production
- Never enable: Keep testnet mode disabled for production contracts
- Verify state: Check
is_testnet_mode()returnsfalsebefore going live - Admin security: Protect admin keys to prevent unauthorized mode changes
- Audit trail: Review event logs to ensure mode was never enabled
- Deploy new contract instance (testnet mode disabled by default)
- Migrate data if needed
- Verify
is_testnet_mode()returnsfalse - Do not reuse testnet contracts for production
Testnet mode state is stored in persistent storage:
DataKey::TestnetMode -> bool- Storage key:
src/lib.rs-DataKey::TestnetMode - Event symbol:
src/lib.rs-EVENT_TESTNET_MODE - Functions:
src/lib.rs-set_testnet_mode(),is_testnet_mode() - Modified flows:
register_offering()(readsSelf::is_testnet_mode(env.clone())to gate BPS validation),report_revenue()(reads same flag to gate concentration enforcement) - Tests:
src/test.rs- Testnet mode section
-
Testnet mode does NOT affect:
- Token transfers
- Claim calculations
- Blacklist enforcement
- Freeze functionality
- Any other validation logic
-
Mode changes are immediate (no delay or grace period)
-
Existing offerings retain their parameters when mode is toggled
faucet_seed_holders(requester, issuer, namespace, token, count) allocates count deterministic
32-byte seeds for an offering's holder slots. It is strictly testnet-only — calling it
while testnet_mode == false returns RevoraError::TestnetOnly (wire value 51).
Integration test suites can call this function once and pin their holder addresses against the returned seeds without manually wiring up holders per test run.
Each seed is computed as:
seed[idx] = sha256(issuer_xdr || namespace_xdr || token_xdr || idx_xdr)
The XDR encoding is the standard Soroban to_xdr representation. Seeds are
offering-specific and index-specific, guaranteeing no collisions within or across offerings.
The 10 000 basis-point total is split floor-evenly across count slots:
floor_bps = 10_000 / count- The last slot absorbs the remainder:
last_bps = floor_bps + (10_000 % count)
The per-slot BPS is included in the emitted fct_seed event so test suites can assert
expected distribution without re-computing it.
One fct_seed event per slot:
Topics: (fct_seed, issuer, namespace, token)
Data: (idx: u32, seed: BytesN<32>, share_bps: u32)
Seeds are persisted in DataKey2::FaucetSeedEntry(offering_id, idx) so they can be
retrieved by index without re-calling the function.
- Guarded by
is_testnet_mode()— panics withTestnetOnlyon mainnet. - Requires the offering to be registered (
OfferingNotFoundotherwise). - Requests are throttled per
requesteraddress with a 1-hour cooldown. - Repeated requests inside the cooldown return
RevoraError::FaucetCooldownActiveand emit afct_cdrjevent. count == 0is a no-op (returns empty vec, emits no events).
// Prerequisites: testnet mode enabled, offering registered.
let seeds = client.faucet_seed_holders(&issuer, &ns, &token, &5);
// seeds[0] is the raw ed25519 public key for slot-0 test holder.
// Use it externally to derive the corresponding Stellar keypair.| Code | Variant | Condition |
|---|---|---|
| 62 | TestnetOnly |
faucet_seed_holders or faucet_reset called with testnet_mode == false |
| 63 | FaucetCooldownActive |
faucet_seed_holders called within the 1-hour cooldown window |
faucet_reset(caller, issuer, namespace, token, seed) deterministically resets the
faucet state for an offering. It clears all persisted FaucetSeedEntry records and
resets the seed count to zero, then emits a single fct_rst event carrying the
caller-supplied seed value.
This lets CI test suites restore a known clean state between runs without redeploying the contract.
- Strictly testnet-only. Returns
RevoraError::TestnetOnly(wire value 62) whentestnet_mode == false. Must never be callable on mainnet. - Admin-only.
callermust equal the stored admin address; any other address returnsRevoraError::NotAuthorized. - Offering must be registered; returns
RevoraError::OfferingNotFoundotherwise.
| Name | Type | Description |
|---|---|---|
caller |
Address |
Admin address — must match the stored admin key. |
issuer |
Address |
Offering issuer address. |
namespace |
Symbol |
Offering namespace. |
token |
Address |
Offering token address. |
seed |
BytesN<32> |
Arbitrary 32-byte value; echoed in the fct_rst event so test suites can anchor the exact reset. |
- Removes
FaucetSeedEntry(offering_id, idx)foridxin0..seed_count(whereseed_countis the running counter stored inFaucetSeedCount(offering_id)). - Resets
FaucetSeedCount(offering_id)to0. - Emits one
fct_rstevent.
- Per-requester
FaucetLastRequestcooldown timestamps are not cleared by this call. Cooldowns expire naturally afterDEFAULT_FAUCET_COOLDOWN_SECONDS(3 600 s). This is intentional: the reset targets seed state reproducibility, not cooldown bypass.
Topics: (fct_rst, issuer, namespace, token)
Data: (caller: Address, seed: BytesN<32>, cleared_count: u32)
cleared_count is the number of FaucetSeedEntry records that were removed.
// Prerequisites: testnet mode enabled, offering registered, caller == admin.
let seed: BytesN<32> = env.crypto().sha256(&Bytes::from_array(&env, b"test-run-42"));
client.faucet_reset(&admin, &issuer, &ns, &token, &seed);
// After this call, faucet_seed_holders behaves as if the offering is fresh.
// (Cooldown for each requester still applies independently.)- Admin key required. Unauthorised callers cannot reset faucet state, preventing cooldown circumvention by unprivileged actors.
- Testnet gate is unconditional. Even the admin cannot call this on mainnet;
testnet_modemust be explicitly enabled first. - The
seedparameter is informational only — it does not influence storage mutations. It provides an anchor for test-suite assertions and audit logs.
-
v0.1.0 - Initial implementation (Issue #24)
- Admin-only toggle
- BPS validation relaxation
- Concentration enforcement skip
- Comprehensive test coverage
-
v0.2.0 - Deterministic faucet (Issue #476)
faucet_seed_holdersfunctionRevoraError::TestnetOnly(wire value 62)RevoraError::FaucetCooldownActive(wire value 63)DataKey2::FaucetSeedEntrystorage keyEVENT_FAUCET_SEED(fct_seed) event symbolsrc/test_faucet_seed.rs— 95%+ test coverage
-
v0.3.0 - Faucet reset primitive (Issue #615)
faucet_reset(caller, issuer, namespace, token, seed)functionDataKey2::FaucetSeedCountstorage key (tracks highest slot index for safe iteration)EVENT_FAUCET_RESET(fct_rst) event symbolfaucet_seed_holdersupdated to maintainFaucetSeedCount- Comprehensive
faucet_resettests insrc/test_faucet_seed.rs
For questions or issues related to testnet mode:
- Check test cases in
src/test.rsfor usage examples - Review event logs for mode change history
- Verify admin configuration with
get_admin()