diff --git a/app/onchain/contracts/aid_escrow/src/lib.rs b/app/onchain/contracts/aid_escrow/src/lib.rs
index 981e1fb7..796a914e 100644
--- a/app/onchain/contracts/aid_escrow/src/lib.rs
+++ b/app/onchain/contracts/aid_escrow/src/lib.rs
@@ -44,6 +44,22 @@ const KEY_PENDING_ADMIN: Symbol = symbol_short!("pendadm");
const KEY_ADMIN_DEADLINE: Symbol = symbol_short!("admdln");
const DEFAULT_ADMIN_DEADLINE: u64 = 7 * 24 * 60 * 60; // 7 days in seconds
+/// Maximum number of decimals supported by Stellar assets / SAC tokens.
+///
+/// The Stellar / SAC token standard caps `decimals()` at 38. 39+ is
+/// rejected because no on-chain token can declare more, and `10^39`
+/// already overflows `i128` (so even if it existed we could never
+/// represent a whole unit). `validate_token` rejects anything above
+/// this with `Error::InvalidTokenDecimals`. See issue #235.
+pub const MAX_TOKEN_DECIMALS: u32 = 38;
+
+/// Initial value of `Config.min_decimals` written by `init()` when no
+/// admin has called `set_config()` yet. 0 disables the floor check, so
+/// tokens with 0 decimals (e.g. NFTs, indivisible units) are accepted by
+/// default. This is the *init-default* only — admins are free to raise
+/// it later via `set_config`.
+pub const INIT_MIN_TOKEN_DECIMALS: u32 = 0;
+
// --- Data Types ---
#[contracttype]
@@ -77,6 +93,11 @@ pub struct Config {
pub min_amount: i128,
pub max_expires_in: u64,
pub allowed_tokens: Vec
,
+ /// Minimum decimals accepted by `validate_token` (admin-configurable).
+ /// Tokens with `decimals < min_decimals` are rejected with
+ /// `Error::InvalidTokenDecimals`. The upper bound is fixed by
+ /// [`MAX_TOKEN_DECIMALS`] and produces the same error.
+ pub min_decimals: u32,
}
#[contracttype]
@@ -114,6 +135,10 @@ pub enum Error {
ProofTooLarge = 20,
NoPendingAdmin = 21,
AdminRotationExpired = 22,
+ /// Token decimals are out of the accepted `[min_decimals, 38]` range.
+ /// Distinguished from `InvalidToken` (meaning the token contract is
+ /// malformed / does not respond to `decimals()`).
+ InvalidTokenDecimals = 23,
}
// --- Contract Events (indexer-friendly; stable topics & payloads) ---
@@ -247,6 +272,7 @@ impl AidEscrow {
min_amount: 1,
max_expires_in: 0,
allowed_tokens: Vec::new(&env),
+ min_decimals: INIT_MIN_TOKEN_DECIMALS,
};
env.storage().instance().set(&KEY_CONFIG, &config);
Ok(())
@@ -345,6 +371,16 @@ impl AidEscrow {
///
/// # Errors
/// Returns `Error::NotAuthorized` if caller is not the admin.
+ ///
+ /// # Migration notes
+ /// - `1 -> 2`: the `Config` struct gained a `min_decimals: u32` field.
+ /// Configurations persisted by v1 cannot be deserialised into the
+ /// v2 layout, so this arm resets the stored config to a safe
+ /// default (`min_amount=1`, `max_expires_in=0`, no allowed tokens,
+ /// `min_decimals=0`). Admins must call `set_config(...)` again
+ /// after the upgrade to reinstate their policy. Without `migrate`
+ /// they would silently observe `get_config()` returning defaults
+ /// while believing their v1 settings were still active.
pub fn migrate(env: Env, new_version: u32) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
admin.require_auth();
@@ -354,7 +390,17 @@ impl AidEscrow {
// Perform version-specific migrations
match (current_version, new_version) {
(1, 2) => {
- // Future: Add migration logic for v1 -> v2
+ // v1 -> v2: the stored `Config` shape is no longer
+ // compatible (it lacks `min_decimals`). Reset to a
+ // permissive default; the admin will re-supply their
+ // policy via `set_config` after the upgrade.
+ let config = Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens: Vec::new(&env),
+ min_decimals: INIT_MIN_TOKEN_DECIMALS,
+ };
+ env.storage().instance().set(&KEY_CONFIG, &config);
}
_ => {
// No-op for now, but structured for future use
@@ -411,10 +457,17 @@ impl AidEscrow {
/// Admin-only. Updates the global contract configuration.
///
/// # Arguments
- /// * `config` — New config values (`min_amount`, `max_expires_in`, `allowed_tokens`).
+ /// * `config` — New config values (`min_amount`, `max_expires_in`,
+ /// `allowed_tokens`, `min_decimals`).
///
/// # Errors
/// Returns `Error::InvalidAmount` if `config.min_amount` is zero or negative.
+ /// Returns `Error::InvalidTokenDecimals` if `config.min_decimals` is
+ /// greater than `MAX_TOKEN_DECIMALS`.
+ /// Returns `Error::InvalidTokenDecimals` if any token in `allowed_tokens`
+ /// has decimals outside `[min_decimals, MAX_TOKEN_DECIMALS]`.
+ /// Returns `Error::InvalidToken` if a token contract does not respond
+ /// to `decimals()`.
/// Returns `Error::NotAuthorized` if caller is not the admin.
pub fn set_config(env: Env, config: Config) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
@@ -424,9 +477,13 @@ impl AidEscrow {
return Err(Error::InvalidAmount);
}
+ if config.min_decimals > MAX_TOKEN_DECIMALS {
+ return Err(Error::InvalidTokenDecimals);
+ }
+
for i in 0..config.allowed_tokens.len() {
let token = config.allowed_tokens.get(i).ok_or(Error::InvalidToken)?;
- Self::validate_token(&env, &token)?;
+ Self::validate_token(&env, &token, config.min_decimals)?;
}
env.storage().instance().set(&KEY_CONFIG, &config);
@@ -506,13 +563,14 @@ impl AidEscrow {
}
/// Returns the current contract configuration.
- /// Falls back to defaults (`min_amount: 1`, `max_expires_in: 0`, empty token list)
- /// if no config has been explicitly set.
+ /// Falls back to defaults (`min_amount: 1`, `max_expires_in: 0`, empty
+ /// token list, `min_decimals: 0`) if no config has been explicitly set.
pub fn get_config(env: Env) -> Config {
env.storage().instance().get(&KEY_CONFIG).unwrap_or(Config {
min_amount: 1,
max_expires_in: 0,
allowed_tokens: Vec::new(&env),
+ min_decimals: INIT_MIN_TOKEN_DECIMALS,
})
}
@@ -527,8 +585,12 @@ impl AidEscrow {
return Err(Error::InvalidAmount);
}
- // 2. Validate token interface and fetch decimals dynamically.
- let decimals = Self::validate_token(&env, &token)?;
+ // 2. Validate token interface, decimal bounds, and fetch decimals dynamically.
+ // The min/max-decimals policy is read from the active config so that
+ // `Error::InvalidTokenDecimals` is returned for tokens outside
+ // `[config.min_decimals, MAX_TOKEN_DECIMALS]`.
+ let config = Self::get_config(env.clone());
+ let decimals = Self::validate_token(&env, &token, config.min_decimals)?;
// 3. Dynamic Precision Check
// Instead of checking 6 AND 8, we check ONLY the decimals this token uses.
@@ -597,7 +659,7 @@ impl AidEscrow {
// --- DYNAMIC PRECISION CHECK ---
// Fetch the actual decimals from a validated token contract.
- let decimals = Self::validate_token(&env, &token)?;
+ let decimals = Self::validate_token(&env, &token, config.min_decimals)?;
let unit = 10i128.pow(decimals);
// Enforce that only whole units can be used (if that is your business requirement).
@@ -726,7 +788,7 @@ impl AidEscrow {
return Err(Error::InvalidState);
}
- let decimals = Self::validate_token(&env, &token)?;
+ let decimals = Self::validate_token(&env, &token, config.min_decimals)?;
let unit = 10i128.pow(decimals);
let contract_balance = Self::token_balance(&env, &token, &env.current_contract_address())?;
@@ -1211,8 +1273,12 @@ impl AidEscrow {
return Err(Error::InvalidAmount);
}
- // 3. Get contract's current balance for the token
- Self::validate_token(&env, &token)?;
+ // 3. Get contract's current balance for the token. The min decimals
+ // policy is sourced from the active config so staking withdrawals on
+ // low-decimal tokens can be restricted just like funding and
+ // package creation.
+ let config = Self::get_config(env.clone());
+ Self::validate_token(&env, &token, config.min_decimals)?;
let contract_balance = Self::token_balance(&env, &token, &env.current_contract_address())?;
// 4. Get total locked amount for the token
@@ -1291,11 +1357,30 @@ impl AidEscrow {
env.storage().instance().set(&KEY_TOTAL_LOCKED, &locked_map);
}
- fn validate_token(env: &Env, token: &Address) -> Result {
+ /// Validates a token contract against the configured decimals policy.
+ ///
+ /// # Behaviour
+ /// - Calls the token's `decimals()` via `try_invoke_contract`.
+ /// - If the call fails or returns a malformed value (RPC / decoder
+ /// failure) → `Error::InvalidToken`.
+ /// - If `decimals < min_decimals` or `decimals > MAX_TOKEN_DECIMALS` →
+ /// `Error::InvalidTokenDecimals`.
+ /// - Otherwise returns the decoded decimals value.
+ ///
+ /// `min_decimals` is passed in (rather than read from storage inside this
+ /// function) so callers like `set_config` validate against the *new*
+ /// config being submitted instead of stale on-chain state.
+ fn validate_token(env: &Env, token: &Address, min_decimals: u32) -> Result {
let args: Vec = Vec::new(env);
match env.try_invoke_contract::(token, &symbol_short!("decimals"), args) {
- Ok(Ok(decimals)) if decimals <= 38 => Ok(decimals),
+ Ok(Ok(decimals)) => {
+ if decimals < min_decimals || decimals > MAX_TOKEN_DECIMALS {
+ Err(Error::InvalidTokenDecimals)
+ } else {
+ Ok(decimals)
+ }
+ }
_ => Err(Error::InvalidToken),
}
}
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/create_package_with_thirty_nine_decimal_token_returns_invalid_token_decimals.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/create_package_with_thirty_nine_decimal_token_returns_invalid_token_decimals.1.json
new file mode 100644
index 00000000..e2a1369e
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/create_package_with_thirty_nine_decimal_token_returns_invalid_token_decimals.1.json
@@ -0,0 +1,264 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 39
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_seven_decimal_token_passes_validate_token.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_seven_decimal_token_passes_validate_token.1.json
new file mode 100644
index 00000000..31a624cf
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_seven_decimal_token_passes_validate_token.1.json
@@ -0,0 +1,375 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "fund",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ },
+ {
+ "i128": "10000000"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ]
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 7
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": [
+ {
+ "event": {
+ "ext": "v0",
+ "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "type_": "contract",
+ "body": {
+ "v0": {
+ "topics": [
+ {
+ "symbol": "escrow_funded"
+ }
+ ],
+ "data": {
+ "map": [
+ {
+ "key": {
+ "symbol": "amount"
+ },
+ "val": {
+ "i128": "10000000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "from"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "token"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "failed_call": false
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_thirty_eight_decimal_token_passes_validate_token.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_thirty_eight_decimal_token_passes_validate_token.1.json
new file mode 100644
index 00000000..d11f924a
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_thirty_eight_decimal_token_passes_validate_token.1.json
@@ -0,0 +1,264 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 38
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_thirty_nine_decimal_token_returns_invalid_token_decimals.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_thirty_nine_decimal_token_returns_invalid_token_decimals.1.json
new file mode 100644
index 00000000..9b27c09e
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_thirty_nine_decimal_token_returns_invalid_token_decimals.1.json
@@ -0,0 +1,264 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 39
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_zero_decimal_token_passes_validate_token.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_zero_decimal_token_passes_validate_token.1.json
new file mode 100644
index 00000000..3b2e851f
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/fund_with_zero_decimal_token_passes_validate_token.1.json
@@ -0,0 +1,375 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "fund",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ },
+ {
+ "i128": "100"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ]
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": [
+ {
+ "event": {
+ "ext": "v0",
+ "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "type_": "contract",
+ "body": {
+ "v0": {
+ "topics": [
+ {
+ "symbol": "escrow_funded"
+ }
+ ],
+ "data": {
+ "map": [
+ {
+ "key": {
+ "symbol": "amount"
+ },
+ "val": {
+ "i128": "100"
+ }
+ },
+ {
+ "key": {
+ "symbol": "from"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "token"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "failed_call": false
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/set_config_with_thirty_nine_decimal_token_in_allowlist_returns_invalid_token_decimals.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/set_config_with_thirty_nine_decimal_token_in_allowlist_returns_invalid_token_decimals.1.json
new file mode 100644
index 00000000..bd893051
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/boundary_decimals/set_config_with_thirty_nine_decimal_token_in_allowlist_returns_invalid_token_decimals.1.json
@@ -0,0 +1,179 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 39
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/config_migration/migrate_v1_to_v2_allows_admin_to_re_set_config.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/config_migration/migrate_v1_to_v2_allows_admin_to_re_set_config.1.json
new file mode 100644
index 00000000..a86760b9
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/config_migration/migrate_v1_to_v2_allows_admin_to_re_set_config.1.json
@@ -0,0 +1,324 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "migrate",
+ "args": [
+ {
+ "u32": 2
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 2
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 7
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/config_migration/migrate_v1_to_v2_resets_stored_config_to_default_v2_layout.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/config_migration/migrate_v1_to_v2_resets_stored_config_to_default_v2_layout.1.json
new file mode 100644
index 00000000..40c5c738
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/config_migration/migrate_v1_to_v2_resets_stored_config_to_default_v2_layout.1.json
@@ -0,0 +1,190 @@
+{
+ "generators": {
+ "address": 2,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "migrate",
+ "args": [
+ {
+ "u32": 2
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 2
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/invalid_token_preservation/create_package_with_non_token_contract_returns_invalid_token.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/invalid_token_preservation/create_package_with_non_token_contract_returns_invalid_token.1.json
new file mode 100644
index 00000000..a302b328
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/invalid_token_preservation/create_package_with_non_token_contract_returns_invalid_token.1.json
@@ -0,0 +1,254 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": null
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/invalid_token_preservation/fund_with_non_token_contract_returns_invalid_token.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/invalid_token_preservation/fund_with_non_token_contract_returns_invalid_token.1.json
new file mode 100644
index 00000000..3073cb95
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/invalid_token_preservation/fund_with_non_token_contract_returns_invalid_token.1.json
@@ -0,0 +1,254 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": null
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/default_config_accepts_token_with_eight_decimals.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/default_config_accepts_token_with_eight_decimals.1.json
new file mode 100644
index 00000000..7a309cd0
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/default_config_accepts_token_with_eight_decimals.1.json
@@ -0,0 +1,375 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "fund",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ },
+ {
+ "i128": "100000000"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ]
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 8
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": [
+ {
+ "event": {
+ "ext": "v0",
+ "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "type_": "contract",
+ "body": {
+ "v0": {
+ "topics": [
+ {
+ "symbol": "escrow_funded"
+ }
+ ],
+ "data": {
+ "map": [
+ {
+ "key": {
+ "symbol": "amount"
+ },
+ "val": {
+ "i128": "100000000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "from"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "token"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "failed_call": false
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_eight_rejects_token_with_seven_decimals.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_eight_rejects_token_with_seven_decimals.1.json
new file mode 100644
index 00000000..dcf7ada7
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_eight_rejects_token_with_seven_decimals.1.json
@@ -0,0 +1,179 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 7
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_accepts_token_with_five_decimals.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_accepts_token_with_five_decimals.1.json
new file mode 100644
index 00000000..041970a6
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_accepts_token_with_five_decimals.1.json
@@ -0,0 +1,271 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ]
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_accepts_token_with_seven_decimals.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_accepts_token_with_seven_decimals.1.json
new file mode 100644
index 00000000..45013b04
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_accepts_token_with_seven_decimals.1.json
@@ -0,0 +1,383 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "fund",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "i128": "10000000"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ]
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 7
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": [
+ {
+ "event": {
+ "ext": "v0",
+ "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "type_": "contract",
+ "body": {
+ "v0": {
+ "topics": [
+ {
+ "symbol": "escrow_funded"
+ }
+ ],
+ "data": {
+ "map": [
+ {
+ "key": {
+ "symbol": "amount"
+ },
+ "val": {
+ "i128": "10000000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "from"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "token"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "failed_call": false
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_rejects_token_with_zero_decimals.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_rejects_token_with_zero_decimals.1.json
new file mode 100644
index 00000000..4d9cb984
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/min_decimals_five_rejects_token_with_zero_decimals.1.json
@@ -0,0 +1,179 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/set_config_rejects_min_decimals_greater_than_max.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/set_config_rejects_min_decimals_greater_than_max.1.json
new file mode 100644
index 00000000..f912c43d
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/min_decimals_policy/set_config_rejects_min_decimals_greater_than_max.1.json
@@ -0,0 +1,136 @@
+{
+ "generators": {
+ "address": 2,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/test_snapshots/withdraw_surplus_policy/withdraw_surplus_rejects_min_decimals_policy_violation.1.json b/app/onchain/contracts/aid_escrow/test_snapshots/withdraw_surplus_policy/withdraw_surplus_rejects_min_decimals_policy_violation.1.json
new file mode 100644
index 00000000..8b2a8b91
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/test_snapshots/withdraw_surplus_policy/withdraw_surplus_rejects_min_decimals_policy_violation.1.json
@@ -0,0 +1,264 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "set_config",
+ "args": [
+ {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "admin"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "config"
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "allowed_tokens"
+ },
+ "val": {
+ "vec": []
+ }
+ },
+ {
+ "key": {
+ "symbol": "max_expires_in"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_amount"
+ },
+ "val": {
+ "i128": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "min_decimals"
+ },
+ "val": {
+ "u32": 5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "version"
+ },
+ "val": {
+ "u32": 1
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "symbol": "decimals"
+ },
+ "val": {
+ "u32": 0
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/app/onchain/contracts/aid_escrow/tests/aid_escrow_tests.rs b/app/onchain/contracts/aid_escrow/tests/aid_escrow_tests.rs
index e7b29212..1cac7e8f 100644
--- a/app/onchain/contracts/aid_escrow/tests/aid_escrow_tests.rs
+++ b/app/onchain/contracts/aid_escrow/tests/aid_escrow_tests.rs
@@ -59,6 +59,7 @@ impl TestSetup {
min_amount: 1, // Minimum 1 stroop
max_expires_in: 0,
allowed_tokens: Vec::new(&env),
+ min_decimals: 0,
});
Self {
@@ -152,6 +153,7 @@ mod create_package {
min_amount: TWO_TOKENS, // Min 2.0 tokens
max_expires_in: 0,
allowed_tokens: Vec::new(&t.env),
+ min_decimals: 0,
});
let result = t.client.try_create_package(
&t.admin,
@@ -239,6 +241,7 @@ mod token_interactions {
min_amount: 1,
max_expires_in: 0,
allowed_tokens,
+ min_decimals: 0,
});
assert_eq!(result, Err(Ok(Error::InvalidToken)));
diff --git a/app/onchain/contracts/aid_escrow/tests/authorization.rs b/app/onchain/contracts/aid_escrow/tests/authorization.rs
index bf45ce44..8c70e5a0 100644
--- a/app/onchain/contracts/aid_escrow/tests/authorization.rs
+++ b/app/onchain/contracts/aid_escrow/tests/authorization.rs
@@ -75,6 +75,7 @@ impl Setup {
min_amount: 1,
max_expires_in: 0,
allowed_tokens: Vec::new(&env),
+ min_decimals: 0,
});
token_sac.mint(&contract_id, &ONE_TOKEN);
diff --git a/app/onchain/contracts/aid_escrow/tests/boundary_validation_tests.rs b/app/onchain/contracts/aid_escrow/tests/boundary_validation_tests.rs
index b68de6b0..41fc84f8 100644
--- a/app/onchain/contracts/aid_escrow/tests/boundary_validation_tests.rs
+++ b/app/onchain/contracts/aid_escrow/tests/boundary_validation_tests.rs
@@ -57,6 +57,7 @@ impl TestSetup {
min_amount: 1,
max_expires_in: 0,
allowed_tokens: Vec::new(&env),
+ min_decimals: 0,
});
Self {
diff --git a/app/onchain/contracts/aid_escrow/tests/common/mod.rs b/app/onchain/contracts/aid_escrow/tests/common/mod.rs
new file mode 100644
index 00000000..a096cef6
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/tests/common/mod.rs
@@ -0,0 +1,48 @@
+//! Shared test helpers for the `aid_escrow` integration-test suite.
+//!
+//! Currently exposes a minimal Soroban contract whose `decimals()`
+//! function returns a configurable value, so the test suite can drive the
+//! `validate_token` boundary branches without spinning up a full Stellar
+//! asset contract (which always reports 7 decimals).
+
+#![cfg(test)]
+
+use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol};
+
+const KEY_DECIMALS: Symbol = symbol_short!("decimals");
+
+/// A mock token whose `decimals()` is set in storage by `init()` and
+/// returns that value on every call. `balance` and `transfer` are
+/// intentional no-ops so the contract never accidentally moves tokens in
+/// tests that only care about decimal validation.
+#[contract]
+pub struct MockDecimalsToken;
+
+#[contractimpl]
+impl MockDecimalsToken {
+ /// Sets the decimals value returned by subsequent `decimals()` calls.
+ ///
+ /// Intentionally has **no authorization check** — this is a test
+ /// helper only, and forcing `require_auth()` on the constructor
+ /// would mean every test would have to call `mock_all_auths()` for
+ /// the mock side, which is unnecessary friction.
+ pub fn init(env: Env, decimals: u32) {
+ env.storage().instance().set(&KEY_DECIMALS, &decimals);
+ }
+
+ /// Returns the decimals value previously stored by `init()`.
+ /// Defaults to 0 for uninitialized contracts.
+ pub fn decimals(env: Env) -> u32 {
+ env.storage().instance().get(&KEY_DECIMALS).unwrap_or(0)
+ }
+
+ /// Returns 0 — kept here so the SDK can match the SAC token
+ /// interface shape; never relied on by validate_token.
+ pub fn balance(_env: Env, _account: Address) -> i128 {
+ 0
+ }
+
+ /// No-op — kept for SAC compatibility; transfer validation is
+ /// deliberately not exercised here.
+ pub fn transfer(_env: Env, _from: Address, _to: Address, _amount: i128) {}
+}
diff --git a/app/onchain/contracts/aid_escrow/tests/decimal_edge_tests.rs b/app/onchain/contracts/aid_escrow/tests/decimal_edge_tests.rs
new file mode 100644
index 00000000..b2497789
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/tests/decimal_edge_tests.rs
@@ -0,0 +1,502 @@
+#![cfg(test)]
+
+//! Decimal-edge tests for `AidEscrow::validate_token` (issue #235).
+//!
+//! These tests pin down three pieces of behaviour that are easy to regress:
+//!
+//! 1. The boundary decimals 0, 7, 38 and the just-above-boundary 39 are
+//! classified correctly: 0/7/38 pass validation (with default policy),
+//! 39 returns `Error::InvalidTokenDecimals`.
+//! 2. The admin-configurable `min_decimals` policy field on `Config`
+//! correctly raises the floor and rejects tokens with decimals below
+//! it (also returning `Error::InvalidTokenDecimals`), while leaving the
+//! upper bound at 38 unchanged. The floor is **strict** (`<`), so
+//! `decimals == min_decimals` is accepted.
+//! 3. `Error::InvalidToken` is **only** used when the `decimals()` call
+//! itself fails (RPC / contract error). Token-response failures and
+//! decimal-range failures are kept distinct — collapsing them would
+//! hide config errors as "bad token" errors.
+//!
+//! All assertions go through the public entry points (`fund`,
+//! `create_package`, `set_config`, `withdraw_surplus`) so the private
+//! `validate_token` is exercised through real production call paths.
+
+// Shared helper module — contains the configurable `MockDecimalsToken`
+// contract used to drive arbitrary decimals values.
+mod common;
+
+use aid_escrow::{AidEscrow, AidEscrowClient, Config, Error};
+use common::{MockDecimalsToken, MockDecimalsTokenClient};
+use soroban_sdk::{testutils::Address as _, Address, Env, Map, Vec};
+
+// ---------------------------------------------------------------------------
+// Test helpers
+// ---------------------------------------------------------------------------
+
+/// Registers a fresh mock token whose `decimals()` returns `decimals`.
+fn register_mock_token(env: &Env, decimals: u32) -> Address {
+ // `env.register(...)` returns an `Address` directly in this SDK
+ // version (older versions returned `BytesN<32>` requiring
+ // `.address()` — that shim is no longer needed).
+ let token_id = env.register(MockDecimalsToken, ());
+ let client = MockDecimalsTokenClient::new(env, &token_id);
+ client.init(&decimals);
+ token_id
+}
+
+/// Initializes the `AidEscrow` contract with `admin` and the supplied
+/// `Config`. Returns the client and an admin `Address` for further setup.
+fn setup_escrow(env: &Env, config: Config) -> (AidEscrowClient<'static>, Address) {
+ let admin = Address::generate(env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(env, &contract_id);
+ client.init(&admin);
+ client.set_config(&config);
+ (client, admin)
+}
+
+fn default_config(env: &Env) -> Config {
+ Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens: Vec::new(env),
+ min_decimals: 0,
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Boundary decimals — accept / reject classification
+// ---------------------------------------------------------------------------
+
+mod boundary_decimals {
+ use super::*;
+
+ #[test]
+ fn fund_with_zero_decimal_token_passes_validate_token() {
+ // Issue acceptance: "fund with decimals=0 continues to work unless
+ // the policy is configured otherwise." With the default
+ // `min_decimals=0` the validation step must pass.
+ let env = Env::default();
+ env.mock_all_auths();
+ let (client, admin) = setup_escrow(&env, default_config(&env));
+ let token = register_mock_token(&env, 0);
+
+ let result = client.try_fund(&token, &admin, &100);
+ assert!(
+ !matches!(result, Err(Ok(Error::InvalidTokenDecimals))),
+ "0-decimal token must not be rejected with InvalidTokenDecimals under default policy"
+ );
+ }
+
+ #[test]
+ fn fund_with_seven_decimal_token_passes_validate_token() {
+ // 7 is the canonical SAC decimals value and must be accepted.
+ let env = Env::default();
+ env.mock_all_auths();
+ let (client, admin) = setup_escrow(&env, default_config(&env));
+ let token = register_mock_token(&env, 7);
+
+ let result = client.try_fund(&token, &admin, &10_000_000);
+ assert!(
+ !matches!(result, Err(Ok(Error::InvalidTokenDecimals))),
+ "7-decimal token must not be rejected with InvalidTokenDecimals"
+ );
+ }
+
+ #[test]
+ fn fund_with_thirty_eight_decimal_token_passes_validate_token() {
+ // 38 is the largest legal decimals value; validate_token must
+ // accept it. Because `10^38` does not fit any reasonable amount
+ // we pass through the precision check, we only assert the *not*
+ // case here (decimals rejection) rather than pinning to a specific
+ // downstream error — the precision check precedence may be
+ // reorganised in the future and this test must not regress.
+ let env = Env::default();
+ env.mock_all_auths();
+ let (client, admin) = setup_escrow(&env, default_config(&env));
+ let token = register_mock_token(&env, 38);
+
+ let result = client.try_fund(&token, &admin, &1);
+ assert!(
+ !matches!(result, Err(Ok(Error::InvalidTokenDecimals))),
+ "38-decimal token must not be rejected with InvalidTokenDecimals"
+ );
+ }
+
+ #[test]
+ fn fund_with_thirty_nine_decimal_token_returns_invalid_token_decimals() {
+ // Issue acceptance: "fund with decimals=39 must return
+ // Error::InvalidTokenDecimals."
+ let env = Env::default();
+ env.mock_all_auths();
+ let (client, admin) = setup_escrow(&env, default_config(&env));
+ let token = register_mock_token(&env, 39);
+
+ assert_eq!(
+ client.try_fund(&token, &admin, &1),
+ Err(Ok(Error::InvalidTokenDecimals))
+ );
+ }
+
+ #[test]
+ fn create_package_with_thirty_nine_decimal_token_returns_invalid_token_decimals() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let (client, admin) = setup_escrow(&env, default_config(&env));
+ let token = register_mock_token(&env, 39);
+
+ assert_eq!(
+ client.try_create_package(
+ &admin,
+ &1u64,
+ &Address::generate(&env),
+ &1,
+ &token,
+ &3600,
+ &Map::new(&env),
+ ),
+ Err(Ok(Error::InvalidTokenDecimals))
+ );
+ }
+
+ #[test]
+ fn set_config_with_thirty_nine_decimal_token_in_allowlist_returns_invalid_token_decimals() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ let bad_token = register_mock_token(&env, 39);
+ let mut allowed_tokens = Vec::new(&env);
+ allowed_tokens.push_back(bad_token);
+
+ assert_eq!(
+ client.try_set_config(&Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens,
+ min_decimals: 0,
+ }),
+ Err(Ok(Error::InvalidTokenDecimals))
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// `min_decimals` policy enforcement
+// ---------------------------------------------------------------------------
+
+mod min_decimals_policy {
+ use super::*;
+
+ #[test]
+ fn set_config_rejects_min_decimals_greater_than_max() {
+ // 40 > MAX_TOKEN_DECIMALS (38), so the policy itself is invalid.
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ assert_eq!(
+ client.try_set_config(&Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens: Vec::new(&env),
+ min_decimals: 40,
+ }),
+ Err(Ok(Error::InvalidTokenDecimals))
+ );
+ }
+
+ #[test]
+ fn min_decimals_five_rejects_token_with_zero_decimals() {
+ // 0 < 5: floor is strict, so a 0-decimal token is rejected.
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ let too_low = register_mock_token(&env, 0);
+ let mut allowed_tokens = Vec::new(&env);
+ allowed_tokens.push_back(too_low);
+
+ assert_eq!(
+ client.try_set_config(&Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens,
+ min_decimals: 5,
+ }),
+ Err(Ok(Error::InvalidTokenDecimals))
+ );
+ }
+
+ #[test]
+ fn min_decimals_five_accepts_token_with_five_decimals() {
+ // Floor is `decimals < min_decimals`. Equality `==` must be
+ // accepted — locking this in prevents a stray `<=` flip.
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ let exact_floor = register_mock_token(&env, 5);
+ let mut allowed_tokens = Vec::new(&env);
+ allowed_tokens.push_back(exact_floor);
+
+ // set_config returns `()` on success and would trap on failure
+ // (because the contract macro unwraps the inner Result). If this
+ // call does not panic, the equality boundary is accepted.
+ client.set_config(&Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens,
+ min_decimals: 5,
+ });
+ }
+
+ #[test]
+ fn min_decimals_five_accepts_token_with_seven_decimals() {
+ // 7 > 5: comfortably above the floor.
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ let acceptable = register_mock_token(&env, 7);
+ let funder = Address::generate(&env);
+ let mut allowed_tokens = Vec::new(&env);
+ allowed_tokens.push_back(acceptable.clone());
+
+ client.set_config(&Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens,
+ min_decimals: 5,
+ });
+
+ // After the policy is in place the same token must still pass
+ // validate_token via the fund entrypoint.
+ let result = client.try_fund(&acceptable, &funder, &10_000_000);
+ assert!(
+ !matches!(result, Err(Ok(Error::InvalidTokenDecimals))),
+ "7-decimal token must stay valid under min_decimals=5 policy"
+ );
+ }
+
+ #[test]
+ fn min_decimals_eight_rejects_token_with_seven_decimals() {
+ // 7 < 8: 7-decimal token falls below the floor.
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ let too_low = register_mock_token(&env, 7);
+ let mut allowed_tokens = Vec::new(&env);
+ allowed_tokens.push_back(too_low);
+
+ assert_eq!(
+ client.try_set_config(&Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens,
+ min_decimals: 8,
+ }),
+ Err(Ok(Error::InvalidTokenDecimals))
+ );
+ }
+
+ #[test]
+ fn default_config_accepts_token_with_eight_decimals() {
+ // Belt-and-braces: with the default min_decimals=0 the policy
+ // adds nothing and an 8-decimal token (above the default floor
+ // and below the cap) must pass validate_token just as 7 did.
+ let env = Env::default();
+ env.mock_all_auths();
+ let (client, admin) = setup_escrow(&env, default_config(&env));
+ let token = register_mock_token(&env, 8);
+
+ let result = client.try_fund(&token, &admin, &100_000_000);
+ assert!(
+ !matches!(result, Err(Ok(Error::InvalidTokenDecimals))),
+ "8-decimal token must not be rejected under default policy"
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// `Error::InvalidToken` (host-decoder failure) preservation
+// ---------------------------------------------------------------------------
+//
+// These tests register an AidEscrow contract itself as the "token". The
+// AidEscrow public surface has no `decimals()` method so `try_invoke_contract`
+// fails with a contract-error, and `validate_token` must surface that as
+// `Error::InvalidToken` — not the new `InvalidTokenDecimals` error. This
+// guards against a future refactor accidentally conflating the two errors.
+
+mod invalid_token_preservation {
+ use super::*;
+
+ #[test]
+ fn fund_with_non_token_contract_returns_invalid_token() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let (client, admin) = setup_escrow(&env, default_config(&env));
+
+ // Register AidEscrow as a stand-in "token" — it does NOT expose
+ // a `decimals()` function, so the cross-contract call fails at the
+ // host layer and validate_token returns InvalidToken.
+ let fake_token = env.register(AidEscrow, ());
+
+ assert_eq!(
+ client.try_fund(&fake_token, &admin, &1),
+ Err(Ok(Error::InvalidToken))
+ );
+ }
+
+ #[test]
+ fn create_package_with_non_token_contract_returns_invalid_token() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let (client, admin) = setup_escrow(&env, default_config(&env));
+
+ let fake_token = env.register(AidEscrow, ());
+
+ assert_eq!(
+ client.try_create_package(
+ &admin,
+ &1u64,
+ &Address::generate(&env),
+ &1,
+ &fake_token,
+ &3600,
+ &Map::new(&env),
+ ),
+ Err(Ok(Error::InvalidToken))
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// `withdraw_surplus` honours the `min_decimals` policy
+// ---------------------------------------------------------------------------
+//
+// `withdraw_surplus` also calls `validate_token`, so the configured
+// `min_decimals` floor must apply symmetrically to funding + surplus
+// withdrawal. Without this, a config update that bans 0-decimal tokens
+// would still let admins drain surplus from them.
+
+mod withdraw_surplus_policy {
+ use super::*;
+
+ #[test]
+ fn withdraw_surplus_rejects_min_decimals_policy_violation() {
+ // Use a 0-decimal mock token. After raising min_decimals=5, the
+ // token is no longer valid and withdraw_surplus must reject.
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ let zero_decimal = register_mock_token(&env, 0);
+
+ // Switch to a strict min_decimals=5 policy. No allowed_tokens are
+ // set, so the only check that runs is `min_decimals` itself on
+ // the token argument to withdraw_surplus.
+ client.set_config(&Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens: Vec::new(&env),
+ min_decimals: 5,
+ });
+
+ assert_eq!(
+ client.try_withdraw_surplus(&Address::generate(&env), &1, &zero_decimal,),
+ Err(Ok(Error::InvalidTokenDecimals))
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// v1 -> v2 migration
+// ---------------------------------------------------------------------------
+//
+// Issue #235's `Config` schema change is silently breaking for any
+// deployment that stored a v1 `Config`. The contract's `migrate(1, 2)`
+// arm resets the stored config to a v2-compatible default; simply
+// bumping the version without that reset would leave `get_config()`
+// returning a fabricated `unwrap_or(...)` default while the admin
+// believed their v1 settings were still active.
+
+mod config_migration {
+ use super::*;
+
+ #[test]
+ fn migrate_v1_to_v2_resets_stored_config_to_default_v2_layout() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ // Sanity: init writes a v1 config. The migration rewrites it
+ // explicitly to the v2 shape so get_config stays consistent.
+ assert_eq!(client.get_version(), 1);
+
+ client.migrate(&2u32);
+
+ assert_eq!(client.get_version(), 2);
+
+ let config = client.get_config();
+ assert_eq!(config.min_amount, 1);
+ assert_eq!(config.max_expires_in, 0);
+ assert_eq!(config.min_decimals, 0);
+ assert!(config.allowed_tokens.is_empty());
+ }
+
+ #[test]
+ fn migrate_v1_to_v2_allows_admin_to_re_set_config() {
+ // After migrate, the admin must be able to re-establish their
+ // policy via set_config. This is the documented recovery path.
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(AidEscrow, ());
+ let client = AidEscrowClient::new(&env, &contract_id);
+ client.init(&admin);
+
+ client.migrate(&2u32);
+
+ // Re-supply a real policy and verify it sticks.
+ let acceptable = register_mock_token(&env, 7);
+ let mut allowed_tokens = Vec::new(&env);
+ allowed_tokens.push_back(acceptable);
+
+ client.set_config(&Config {
+ min_amount: 1,
+ max_expires_in: 0,
+ allowed_tokens,
+ min_decimals: 5,
+ });
+
+ let cfg = client.get_config();
+ assert_eq!(cfg.min_decimals, 5);
+ assert_eq!(cfg.allowed_tokens.len(), 1);
+ }
+}
diff --git a/app/onchain/contracts/aid_escrow/tests/gas_profiling.rs b/app/onchain/contracts/aid_escrow/tests/gas_profiling.rs
index 77023fb8..2228b751 100644
--- a/app/onchain/contracts/aid_escrow/tests/gas_profiling.rs
+++ b/app/onchain/contracts/aid_escrow/tests/gas_profiling.rs
@@ -56,6 +56,7 @@ impl TestSetup {
min_amount: 1,
max_expires_in: 0,
allowed_tokens: Vec::new(&env),
+ min_decimals: 0,
});
Self {
@@ -254,6 +255,7 @@ fn profile_single_claim() {
min_amount: 1,
max_expires_in: 0,
allowed_tokens: Vec::new(&env),
+ min_decimals: 0,
});
token_sac.mint(&client.address, &ONE_TOKEN);
@@ -352,6 +354,7 @@ fn profile_claim_with_proof() {
min_amount: 1,
max_expires_in: 0,
allowed_tokens: Vec::new(&env),
+ min_decimals: 0,
});
token_sac.mint(&client.address, &ONE_TOKEN);
diff --git a/app/onchain/contracts/aid_escrow/tests/integration.rs b/app/onchain/contracts/aid_escrow/tests/integration.rs
index 24538b83..6ef77772 100644
--- a/app/onchain/contracts/aid_escrow/tests/integration.rs
+++ b/app/onchain/contracts/aid_escrow/tests/integration.rs
@@ -140,6 +140,7 @@ fn test_set_get_config() {
min_amount: UNIT,
max_expires_in: 3600,
allowed_tokens: tokens,
+ min_decimals: 0,
};
client.set_config(&config);
assert_eq!(client.get_config(), config);
@@ -165,6 +166,7 @@ fn test_config_constraints_on_create_package() {
min_amount: 5 * UNIT,
max_expires_in: 1000,
allowed_tokens: allowed,
+ min_decimals: 0,
});
let now = env.ledger().timestamp();
@@ -431,6 +433,7 @@ fn test_config_constraints_on_extend_expiration() {
min_amount: UNIT,
max_expires_in: 500,
allowed_tokens: Vec::new(&env),
+ min_decimals: 0,
});
let now = env.ledger().timestamp();