Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ mod state_machine_tests {
.max()
.unwrap_or(0);
assert_eq!(
era, 300,
era,
3 * crate::economic::native_reserve::ERA_FAUCET_PAYOUT,
"current_state must reflect the canonical head's balance"
);
assert_eq!(cs.hash, head.root(), "hash is the canonical SMT root");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ const ERA_POLICY_PROTO: [u8; 40] = [
0x03, b'E', b'R', b'A', //
// alias "ERA".
0x00, 0x03, b'E', b'R', b'A', //
// decimals: whole ERA.
0x00, //
// genesis supply: 80,000,000,000 (u128, big-endian; owner, 2026-09-26).
// decimals: two (SoFi Amendment S18, owner 2026-10-01).
0x02, //
// genesis supply: 80,000,000,000.00 ERA, which is 8,000,000,000,000 base
// units (u128, big-endian; owner 2026-09-26, in base units since S18).
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x12, 0xA0, 0x5F, 0x20, 0x00, //
0x00, 0x00, 0x07, 0x46, 0xA5, 0x28, 0x80, 0x00, //
// description: none; icon: none.
0x00, 0x00, 0x00, 0x00, //
// recipient allowlist: none (kind NONE, count 0).
Expand Down Expand Up @@ -84,7 +85,7 @@ mod tests {
);
assert_eq!(
crate::utils::text_id::encode_base32_crockford(&era_policy_commit()),
"JXPMPGJH45HDTE0ARWE2CTB9E9BWTQZ3T78CE5RFF1RXMR9VKK80"
"NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0"
);
}

Expand All @@ -97,16 +98,16 @@ mod tests {
assert_eq!(decoded.encode_to_vec(), era_policy_bytes());
}

/// Every field of ERA's policy, as SoFi Amendment S11 fixes it.
/// Every field of ERA's policy, as SoFi Amendments S11 and S18 fix it.
#[test]
fn eras_policy_states_what_the_specification_fixes() {
assert_eq!(
era_policy().expect("ERA's policy parses"),
&TokenPolicy {
ticker: "ERA".into(),
alias: "ERA".into(),
decimals: 0,
genesis_supply: 80_000_000_000,
decimals: 2,
genesis_supply: 8_000_000_000_000,
release: Release::Faucet,
description: None,
icon_url: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ use crate::types::error::DsmError;
/// conservation guard must be able to validate it. The guard is a pure function
/// over `(operation, deltas)`; a fee it cannot see is a fee it cannot enforce,
/// and a fee that a runtime map could change is not a protocol rule. The SDK's
/// schedule now READS this value, so there is exactly one authority.
pub const TOKEN_CREATION_FEE_ERA: u64 = 10;
/// schedule now READS this value, so there is exactly one authority. In base
/// units: 10.00 ERA at ERA's two decimals (SoFi Amendment S18).
pub const TOKEN_CREATION_FEE_ERA: u64 = 1_000;

/// Display-only ticker resolution for non-builtin (CPTA-anchored) tokens.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ use crate::types::proto as generated;

type D32 = [u8; 32];

/// The whole distributable ERA supply of one network at genesis. Nothing is
/// minted after it; every unit in circulation was released from it.
pub const ERA_RESERVE_GENESIS_SUPPLY: u64 = 80_000_000_000;
/// The whole distributable ERA supply of one network at genesis, in base
/// units: 80,000,000,000.00 ERA at ERA's two decimals (SoFi Amendment S18).
/// Nothing is minted after it; every unit in circulation was released from it.
pub const ERA_RESERVE_GENESIS_SUPPLY: u64 = 8_000_000_000_000;

/// What one beta faucet claim releases. ERA is whole-unit (`decimals = 0`),
/// so this is literally 100 ERA. The claim names no amount: the beta claim
/// policy fixes it, and the accepting transition refuses any other delta.
pub const ERA_FAUCET_PAYOUT: u64 = 100;
/// What one beta faucet claim releases, in base units: 100.00 ERA (SoFi
/// Amendment S18). The claim names no amount: the beta claim policy fixes it,
/// and the accepting transition refuses any other delta.
pub const ERA_FAUCET_PAYOUT: u64 = 10_000;

/// Matches the proto's `dsm_max_len`; prost does not enforce it, so this
/// module does.
Expand Down Expand Up @@ -849,7 +850,7 @@ mod tests {

#[test]
fn the_envelope_round_trips_and_is_strict() {
let release = signed(&genesis(), 100);
let release = signed(&genesis(), ERA_FAUCET_PAYOUT);
let bytes = release.envelope_bytes.clone();
// Decodable-but-non-canonical: unknown field, silently skipped by
// prost, caught only by the re-encode comparison.
Expand Down Expand Up @@ -1041,7 +1042,7 @@ mod tests {
release_constructible(&r0, &zero),
Err(ReleaseRefusal::ZeroRelease)
);
let (release, pk) = signed_with_key(&r0, 100);
let (release, pk) = signed_with_key(&r0, ERA_FAUCET_PAYOUT);
let r1 = release_constructible(&r0, &release).expect("constructible");
// Every unit that left the reserve is accounted to the recipient the
// body names, and that recipient is the signer.
Expand All @@ -1057,29 +1058,29 @@ mod tests {
fn a_release_must_succeed_exactly_its_parent() {
let r0 = genesis();
let (pk, sk) = keypair();
let mut wrong_root = body(&r0, 100, &pk);
let mut wrong_root = body(&r0, ERA_FAUCET_PAYOUT, &pk);
wrong_root.parent_root = [0xEE; 32];
let wrong_root =
decode_and_verify_release(&sign_release(&wrong_root, &sk).unwrap()).unwrap();
assert_eq!(
release_constructible(&r0, &wrong_root),
Err(ReleaseRefusal::ParentRootMismatch)
);
let mut wrong_gen = body(&r0, 100, &pk);
let mut wrong_gen = body(&r0, ERA_FAUCET_PAYOUT, &pk);
wrong_gen.generation = 2;
let wrong_gen = decode_and_verify_release(&sign_release(&wrong_gen, &sk).unwrap()).unwrap();
assert_eq!(
release_constructible(&r0, &wrong_gen),
Err(ReleaseRefusal::GenerationIsNotSuccessor)
);
let mut other = body(&r0, 100, &pk);
let mut other = body(&r0, ERA_FAUCET_PAYOUT, &pk);
other.reserve_id = era_reserve_id(b"othernet");
let other = decode_and_verify_release(&sign_release(&other, &sk).unwrap()).unwrap();
assert_eq!(
release_constructible(&r0, &other),
Err(ReleaseRefusal::NamesAnotherReserve)
);
let mut foreign = body(&r0, 100, &pk);
let mut foreign = body(&r0, ERA_FAUCET_PAYOUT, &pk);
foreign.storage_set_id = [0x77; 32];
let foreign = decode_and_verify_release(&sign_release(&foreign, &sk).unwrap()).unwrap();
assert_eq!(
Expand All @@ -1097,7 +1098,7 @@ mod tests {
#[test]
fn finality_without_the_deterministic_leader_is_impossible() {
let r0 = genesis();
let release = signed(&r0, 100);
let release = signed(&r0, ERA_FAUCET_PAYOUT);
let x = release.envelope_bytes.clone();
let (at, mut skipped_leader) = successor_cell(&r0);
skipped_leader.write(&x, ROUTE_LEN - 1, &[0]);
Expand Down Expand Up @@ -1130,7 +1131,7 @@ mod tests {
#[test]
fn unrecognized_bytes_never_occupy_the_cell() {
let r0 = genesis();
let release = signed(&r0, 100);
let release = signed(&r0, ERA_FAUCET_PAYOUT);
// Signed, canonical, succeeding R_0 — and releasing more than exists.
let too_much = signed(&r0, ERA_RESERVE_GENESIS_SUPPLY + 1);
let (at, mut cell) = successor_cell(&r0);
Expand All @@ -1153,8 +1154,8 @@ mod tests {
#[test]
fn additional_replicas_do_not_alter_the_winner() {
let r0 = genesis();
let a = signed(&r0, 100);
let b = signed(&r0, 100);
let a = signed(&r0, ERA_FAUCET_PAYOUT);
let b = signed(&r0, ERA_FAUCET_PAYOUT);
let child_a = release_constructible(&r0, &a).unwrap();
let (at, mut cell) = successor_cell(&r0);
cell.write(&a.envelope_bytes, 0, &[]);
Expand Down Expand Up @@ -1183,7 +1184,7 @@ mod tests {
#[test]
fn a_final_release_has_a_completion_proof_that_checks() {
let r0 = genesis();
let release = signed(&r0, 100);
let release = signed(&r0, ERA_FAUCET_PAYOUT);
let (at, mut cell) = successor_cell(&r0);
cell.write(&release.envelope_bytes, 1, &[]);
assert_eq!(successor_completion(&at, &cell.evidence()), Ok(None));
Expand All @@ -1206,7 +1207,7 @@ mod tests {
#[test]
fn the_walk_advances_through_final_releases_and_stops_at_the_head() {
let r0 = genesis();
let rel1 = signed(&r0, 100);
let rel1 = signed(&r0, ERA_FAUCET_PAYOUT);
let r1 = release_constructible(&r0, &rel1).unwrap();
let rel2 = signed(&r1, ERA_FAUCET_PAYOUT);
let r2 = release_constructible(&r1, &rel2).unwrap();
Expand Down Expand Up @@ -1267,7 +1268,7 @@ mod tests {
})
);
assert_eq!(visited, 0, "nothing final was read");
let rel1 = signed(&r0, 100);
let rel1 = signed(&r0, ERA_FAUCET_PAYOUT);
let r1 = release_constructible(&r0, &rel1).unwrap();
let stop = walk_lineage(
r0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@ async fn bearer_pair() -> (
"wallet.loadOffline",
&crate::generated::OfflineCashRequest {
token_id: "ERA".to_string(),
amount: "20".to_string(),
amount: "20.00".to_string(),
},
)
.await;
Expand Down Expand Up @@ -789,7 +789,7 @@ async fn a_bearer_step_proves_its_whole_write_set_and_commits_on_both() {
assert!(kinds.contains(&ReceiptLeaf::AnchorState), "{kinds:?}");
assert!(
kinds.contains(&ReceiptLeaf::OfflineAllocation {
pre_amount: 20,
pre_amount: crate::economic_fixtures::whole_era(20),
pre_sequence: 1,
}),
"the allocation's pre-state is the load: {kinds:?}"
Expand Down Expand Up @@ -1868,7 +1868,11 @@ async fn an_online_send_waits_for_the_offline_step_in_flight() {
!refused.success,
"A sent online with its offline step in flight"
);
assert_eq!(pair.a.era_balance(), 1_000, "a refused send debited");
assert_eq!(
pair.a.era_balance(),
crate::economic_fixtures::whole_era(1_000),
"a refused send debited"
);

b.device.enter();
b.handler
Expand All @@ -1880,7 +1884,11 @@ async fn an_online_send_waits_for_the_offline_step_in_flight() {
!refused.success,
"B sent online while it held A's offline proposal"
);
assert_eq!(pair.b.era_balance(), 1_000, "a refused send debited");
assert_eq!(
pair.b.era_balance(),
crate::economic_fixtures::whole_era(1_000),
"a refused send debited"
);

a.device.enter();
let cancellation = a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ pub fn use_test_storage_dir() {
/// else can never produce an admitted position.
pub const NETWORK: &[u8] = b"dsm-testnet";

/// `whole` ERA in base units, at ERA's committed decimals (SoFi Amendment
/// S18): a test that speaks in ERA states its amounts through this, so it
/// keeps its meaning whatever ERA's decimals are.
pub fn whole_era(whole: u64) -> u64 {
whole
* 10u64.pow(
dsm::core::token::era_policy::era_policy()
.expect("ERA's policy")
.decimals,
)
}

/// The device's environment config, pointed at one node set. Dropping it
/// removes the config, so a later test that forgets to point the SDK at its
/// own nodes fails to load a config rather than reaching this set's.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,18 @@ async fn decimals_survive_a_restart_for_a_held_token() {
assert_eq!(t.display_amount, "500.00");
}

/// Builtins keep their exact values: ERA whole units, dBTC in satoshis, no
/// policy icon, and each its protocol-defined anchor.
/// Builtins keep their exact values: ERA at two decimals (SoFi Amendment S18),
/// dBTC in satoshis, no policy icon, and each its protocol-defined anchor.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn builtin_tokens_keep_their_metadata() {
let d = Device::funded(0x83).await;
let rows = wire_rows(&d.router).await;
let era = row(&rows, "ERA");
assert_eq!(era.decimals, 0);
assert_eq!(era.decimals, 2);
assert_eq!(era.symbol, "ERA");
assert_eq!(era.available, 100);
assert_eq!(era.display_amount, "100");
assert_eq!(era.available, crate::economic_fixtures::whole_era(100));
assert_eq!(era.display_amount, "100.00");
let dbtc = row(&rows, "dBTC");
assert_eq!(dbtc.decimals, 8);
assert_eq!(dbtc.symbol, "dBTC");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! release, R7 byte-identical checkpoint replay, R11 one deleter for the gate.
//! R5/R6 live beside the code they pin (`storage_routes` / `core_sdk`).

use crate::economic_fixtures::whole_era;
use crate::storage::client_db as cdb;
use crate::test_support::two_device::{assert_incomplete, Pair, TestDevice};
use dsm::types::proto as generated;
Expand Down Expand Up @@ -340,7 +341,7 @@ async fn r1_role_reversal_applies_once_on_a_and_finalizes_on_b() {
let rel = p.a.rel_key_with(&p.b);
generation(&p.a, &p.b, 10).await;
generation(&p.a, &p.b, 10).await;
assert_eq!(p.b.era_balance(), 120);
assert_eq!(p.b.era_balance(), whole_era(100) + 20);

// A pins B's head at exactly the child B journaled on its second apply —
// learned from B's delta, authenticated by sig_b — and that is the parent
Expand Down Expand Up @@ -372,8 +373,12 @@ async fn r1_role_reversal_applies_once_on_a_and_finalizes_on_b() {

generation(&p.b, &p.a, 5).await;

assert_eq!(p.a.era_balance(), 985, "A credited exactly once");
assert_eq!(p.b.era_balance(), 115);
assert_eq!(
p.a.era_balance(),
whole_era(1_000) - 20 + 5,
"A credited exactly once"
);
assert_eq!(p.b.era_balance(), whole_era(100) + 20 - 5);
p.a.enter();
assert_eq!(rows_for_relationship("canonical_apply_identity", &rel), 1);
assert_eq!(
Expand Down Expand Up @@ -466,7 +471,7 @@ async fn r2b_the_certificate_releases_the_recipient() {
"B released by the certificate: {status:?}"
);
generation(&p.b, &p.a, 5).await;
assert_eq!(p.a.era_balance(), 995);
assert_eq!(p.a.era_balance(), whole_era(1_000) - 10 + 5);
}

// =====================================================================
Expand Down Expand Up @@ -507,7 +512,7 @@ async fn r3_sender_stays_gated_until_the_checkpoint_reaches_quorum() {
assert_eq!(status.send_block_reason, pending_catchup());
let refused = p.a.send(&p.b, 1).await;
assert!(!refused.success, "second A->B must be refused");
assert_eq!(p.a.era_balance(), 990, "no second debit");
assert_eq!(p.a.era_balance(), whole_era(1_000) - 10, "no second debit");

// Fleet back: the sweep replays the exact certificate, quorum, release.
p.nodes.restore_spools(&down).await;
Expand Down Expand Up @@ -558,7 +563,7 @@ async fn a_send_before_the_previous_step_finalizes_is_gated_never_marked_for_res
!cdb::cert_resync_blocks_send(&rel).expect("resync state"),
"the relationship is not marked for resync"
);
assert_eq!(p.a.era_balance(), 990, "no second debit");
assert_eq!(p.a.era_balance(), whole_era(1_000) - 10, "no second debit");

// The step finalizes; the relationship sends again.
let b_sync = p.b.sync().await;
Expand Down Expand Up @@ -825,7 +830,7 @@ async fn r7_a_frozen_checkpoint_is_replayed_byte_identically_after_the_fleet_ret
vec![cdb::OUTBOX_GC_PENDING.to_string()]
);
assert_eq!(proposal_statuses(&rel).len(), 1, "no second proposal");
assert_eq!(p.a.era_balance(), 990, "no second debit");
assert_eq!(p.a.era_balance(), whole_era(1_000) - 10, "no second debit");
// The replay re-posts to members that already held the first delivery;
// a node deduplicates nothing (storage spec §8), so those hold it twice.
// What matters is that every copy anywhere is the frozen envelope under
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ mod pending_list_tests {
);
assert_eq!(prepared.recipient_id, peer.to_vec());
assert_eq!((prepared.amount, prepared.token_id.as_str()), (3, "ERA"));
assert_eq!(prepared.display_amount.as_deref(), Some("3"));
assert_eq!(prepared.display_amount.as_deref(), Some("0.03"));
assert!(prepared.cancellable, "an unconfirmed proposal is offered");

let confirmed = step([0xA2; 32]);
Expand Down
Loading
Loading