diff --git a/contracts/commitment_core/src/lib.rs b/contracts/commitment_core/src/lib.rs
index 95d6cf3..b28342f 100644
--- a/contracts/commitment_core/src/lib.rs
+++ b/contracts/commitment_core/src/lib.rs
@@ -390,6 +390,20 @@ impl CommitmentCoreContract {
let nft_contract = e.storage().instance().get::<_, Address>(&DataKey::NftContract)
.unwrap_or_else(|| { set_reentrancy_guard(&e, false); fail(&e, CommitmentError::NotInitialized, "create") });
+ // Calculate creation fee if configured
+ let creation_fee_bps: u32 = e
+ .storage()
+ .instance()
+ .get(&DataKey::CreationFeeBps)
+ .unwrap_or(0);
+ let creation_fee = if creation_fee_bps > 0 {
+ fees::fee_from_bps(amount, creation_fee_bps)
+ } else {
+ 0
+ };
+ // Net amount locked in commitment (after fee deduction)
+ let net_amount = amount - creation_fee;
+
let commitment_id = Self::generate_commitment_id(&e, current_total);
let commitment = Commitment {
commitment_id: commitment_id.clone(),
@@ -435,18 +449,6 @@ impl CommitmentCoreContract {
let contract_address = e.current_contract_address();
transfer_assets(&e, &owner, &contract_address, &asset_address, amount);
- // Collect creation fee if configured
- let creation_fee_bps: u32 = e
- .storage()
- .instance()
- .get(&DataKey::CreationFeeBps)
- .unwrap_or(0);
- let creation_fee = if creation_fee_bps > 0 {
- fees::fee_from_bps(amount, creation_fee_bps)
- } else {
- 0
- };
-
// Add creation fee to collected fees
if creation_fee > 0 {
let fee_key = DataKey::CollectedFees(asset_address.clone());
@@ -456,9 +458,6 @@ impl CommitmentCoreContract {
.set(&fee_key, &(current_fees + creation_fee));
}
- // Net amount locked in commitment (after fee deduction)
- let net_amount = amount - creation_fee;
-
let nft_token_id = call_nft_mint(
&e,
&nft_contract,
@@ -1124,9 +1123,6 @@ mod tests;
#[cfg(test)]
mod emergency_tests;
-#[cfg(test)]
-mod fee_tests;
-
#[cfg(all(test, feature = "benchmark"))]
mod benchmarks;
diff --git a/contracts/shared_utils/src/lib.rs b/contracts/shared_utils/src/lib.rs
index b3deb80..b3fecea 100644
--- a/contracts/shared_utils/src/lib.rs
+++ b/contracts/shared_utils/src/lib.rs
@@ -40,19 +40,9 @@ pub use emergency::EmergencyControl;
pub use error_codes::{category, code, emit_error_event, message_for_code};
pub use errors::ErrorHelper;
pub use events::Events;
-pub use fees;
pub use math::SafeMath;
pub use pausable::Pausable;
pub use rate_limiting::RateLimiter;
pub use storage::Storage;
pub use time::TimeUtils;
pub use validation::Validation;
-pub use error_codes::*;
-pub use errors::*;
-pub use events::*;
-pub use math::*;
-pub use pausable::*;
-pub use rate_limiting::*;
-pub use storage::*;
-pub use time::*;
-pub use validation::*;
diff --git a/docs/CONTRACT_FUNCTIONS.md b/docs/CONTRACT_FUNCTIONS.md
index 67b129e..2552788 100644
--- a/docs/CONTRACT_FUNCTIONS.md
+++ b/docs/CONTRACT_FUNCTIONS.md
@@ -22,6 +22,12 @@ This document summarizes public entry points for each contract and their access
| allocate(commitment_id, target_pool, amount) | Allocate assets to pool. | No require_auth. | Transfers assets to target pool. |
| set_rate_limit(caller, function, window, max_calls) | Configure rate limits. | Admin only. | Uses shared RateLimiter. |
| set_rate_limit_exempt(caller, address, exempt) | Configure rate limit exemption. | Admin only. | Uses shared RateLimiter. |
+| set_creation_fee_bps(caller, bps) | Set creation fee rate in basis points. | Admin only. | Fee rate 0-10000 bps (100 bps = 1%). |
+| set_fee_recipient(caller, recipient) | Set protocol treasury for fee withdrawals. | Admin only. | Validates recipient is not zero address. |
+| withdraw_fees(caller, asset_address, amount) | Withdraw collected fees to recipient. | Admin only. | Requires recipient set, sufficient fees collected. |
+| get_creation_fee_bps() -> u32 | Get current creation fee rate. | View. | Returns 0 if not set. |
+| get_fee_recipient() -> Option
| Get configured fee recipient. | View. | Returns None if not set. |
+| get_collected_fees(asset_address) -> i128 | Get collected fees for an asset. | View. | Returns 0 if none collected. |
### commitment_core cross-contract notes
@@ -37,15 +43,15 @@ This document summarizes public entry points for each contract and their access
CI drift tests compare its source-defined types and expected signatures against
`commitment_core` and `attestation_engine`.
-| Function | Summary | Access control | Notes |
-| ------------------------------------------------------------------- | -------------------------------------------- | ------------------------- | ------------------------------------------------------------------------ |
-| initialize(admin, nft_contract) -> Result | Initialize admin and linked NFT contract. | Interface only. | Live core contract is single-use; no state exists in this crate. |
-| create_commitment(owner, amount, asset_address, rules) -> Result | Create a commitment and return string id. | Interface only. | Mirrors live `commitment_core` types, including `CommitmentRules`. |
-| get_commitment(commitment_id) -> Result | Fetch the canonical commitment record. | View in live contract. | `Commitment` shape is drift-checked against `commitment_core`. |
-| get_owner_commitments(owner) -> Result> | List commitment ids owned by an address. | View in live contract. | Used by UIs and indexers. |
-| get_total_commitments() -> Result | Read the total commitment counter. | View in live contract. | Counter is stored by the live core contract. |
-| settle(commitment_id) -> Result | Settle an expired commitment. | Mutating in live contract | Live implementation performs token and NFT cross-contract interactions. |
-| early_exit(commitment_id, caller) -> Result | Exit a commitment early with penalty logic. | Mutating in live contract | Live implementation must enforce caller auth and overflow-safe math. |
+| Function | Summary | Access control | Notes |
+| ------------------------------------------------------------------------ | ------------------------------------------- | ------------------------- | ----------------------------------------------------------------------- |
+| initialize(admin, nft_contract) -> Result | Initialize admin and linked NFT contract. | Interface only. | Live core contract is single-use; no state exists in this crate. |
+| create_commitment(owner, amount, asset_address, rules) -> Result | Create a commitment and return string id. | Interface only. | Mirrors live `commitment_core` types, including `CommitmentRules`. |
+| get_commitment(commitment_id) -> Result | Fetch the canonical commitment record. | View in live contract. | `Commitment` shape is drift-checked against `commitment_core`. |
+| get_owner_commitments(owner) -> Result> | List commitment ids owned by an address. | View in live contract. | Used by UIs and indexers. |
+| get_total_commitments() -> Result | Read the total commitment counter. | View in live contract. | Counter is stored by the live core contract. |
+| settle(commitment_id) -> Result | Settle an expired commitment. | Mutating in live contract | Live implementation performs token and NFT cross-contract interactions. |
+| early_exit(commitment_id, caller) -> Result | Exit a commitment early with penalty logic. | Mutating in live contract | Live implementation must enforce caller auth and overflow-safe math. |
## commitment_nft
@@ -70,28 +76,28 @@ CI drift tests compare its source-defined types and expected signatures against
## attestation_engine
-| Function | Summary | Access control | Notes |
-| ----------------------------------------------------------------------------- | --------------------------------- | ---------------------- | -------------------------------------------------------------- |
-| initialize(admin, commitment_core) -> Result | Set admin and core contract. | None (single-use). | Returns AlreadyInitialized on repeat. |
-| add_verifier(caller, verifier) -> Result | Authorize verifier address. | Admin require_auth. | Stores verifier flag. |
-| remove_verifier(caller, verifier) -> Result | Remove verifier authorization. | Admin require_auth. | Removes verifier flag. |
-| is_verifier(address) -> bool | Check verifier authorization. | View. | Admin is implicitly authorized. |
-| get_admin() -> Result | Fetch admin address. | View. | Fails if not initialized. |
-| get_core_contract() -> Result | Fetch core contract address. | View. | Fails if not initialized. |
-| get_stored_health_metrics(commitment_id) -> Option | Fetch cached health metrics. | View. | Returns None if missing. |
-| attest(caller, commitment_id, attestation_type, data, is_compliant) -> Result | Record attestation. | Verifier require_auth. | Validates commitment, uses rate limiting and reentrancy guard. |
-| get_attestations(commitment_id) -> Vec | List attestations for commitment. | View. | Returns empty Vec if none. |
-| get_attestations_page(commitment_id, offset, limit) -> AttestationsPage | Paginated attestations. | View. | Order: timestamp (oldest first). Max page size MAX_PAGE_SIZE=100. next_offset=0 when no more. |
-| get_attestation_count(commitment_id) -> u64 | Count attestations. | View. | Stored in persistent storage. |
-| get_health_metrics(commitment_id) -> HealthMetrics | Compute current health metrics. | View. | Reads commitment_core data. |
-| verify_compliance(commitment_id) -> bool | Check compliance vs rules. | View. | Uses health metrics and rules. |
-| record_fees(caller, commitment_id, fee_amount) -> Result | Convenience fee attestation. | Verifier require_auth. | Calls attest() internally. |
-| record_drawdown(caller, commitment_id, drawdown_percent) -> Result | Convenience drawdown attestation. | Verifier require_auth. | Calls attest() internally. |
-| calculate_compliance_score(commitment_id) -> u32 | Compute compliance score. | View. | Emits ScoreUpd event. |
-| get_protocol_statistics() -> (u64, u64, u64, i128) | Aggregate protocol stats. | View. | Reads commitment_core counters. |
-| get_verifier_statistics(verifier) -> u64 | Per-verifier attestation count. | View. | Stored in instance storage. |
-| set_rate_limit(caller, function, window, max_calls) -> Result | Configure rate limits. | Admin require_auth. | Uses shared RateLimiter. |
-| set_rate_limit_exempt(caller, verifier, exempt) -> Result | Configure rate limit exemption. | Admin require_auth. | Uses shared RateLimiter. |
+| Function | Summary | Access control | Notes |
+| ----------------------------------------------------------------------------- | --------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------- |
+| initialize(admin, commitment_core) -> Result | Set admin and core contract. | None (single-use). | Returns AlreadyInitialized on repeat. |
+| add_verifier(caller, verifier) -> Result | Authorize verifier address. | Admin require_auth. | Stores verifier flag. |
+| remove_verifier(caller, verifier) -> Result | Remove verifier authorization. | Admin require_auth. | Removes verifier flag. |
+| is_verifier(address) -> bool | Check verifier authorization. | View. | Admin is implicitly authorized. |
+| get_admin() -> Result | Fetch admin address. | View. | Fails if not initialized. |
+| get_core_contract() -> Result | Fetch core contract address. | View. | Fails if not initialized. |
+| get_stored_health_metrics(commitment_id) -> Option | Fetch cached health metrics. | View. | Returns None if missing. |
+| attest(caller, commitment_id, attestation_type, data, is_compliant) -> Result | Record attestation. | Verifier require_auth. | Validates commitment, uses rate limiting and reentrancy guard. |
+| get_attestations(commitment_id) -> Vec | List attestations for commitment. | View. | Returns empty Vec if none. |
+| get_attestations_page(commitment_id, offset, limit) -> AttestationsPage | Paginated attestations. | View. | Order: timestamp (oldest first). Max page size MAX_PAGE_SIZE=100. next_offset=0 when no more. |
+| get_attestation_count(commitment_id) -> u64 | Count attestations. | View. | Stored in persistent storage. |
+| get_health_metrics(commitment_id) -> HealthMetrics | Compute current health metrics. | View. | Reads commitment_core data. |
+| verify_compliance(commitment_id) -> bool | Check compliance vs rules. | View. | Uses health metrics and rules. |
+| record_fees(caller, commitment_id, fee_amount) -> Result | Convenience fee attestation. | Verifier require_auth. | Calls attest() internally. |
+| record_drawdown(caller, commitment_id, drawdown_percent) -> Result | Convenience drawdown attestation. | Verifier require_auth. | Calls attest() internally. |
+| calculate_compliance_score(commitment_id) -> u32 | Compute compliance score. | View. | Emits ScoreUpd event. |
+| get_protocol_statistics() -> (u64, u64, u64, i128) | Aggregate protocol stats. | View. | Reads commitment_core counters. |
+| get_verifier_statistics(verifier) -> u64 | Per-verifier attestation count. | View. | Stored in instance storage. |
+| set_rate_limit(caller, function, window, max_calls) -> Result | Configure rate limits. | Admin require_auth. | Uses shared RateLimiter. |
+| set_rate_limit_exempt(caller, verifier, exempt) -> Result | Configure rate limit exemption. | Admin require_auth. | Uses shared RateLimiter. |
### attestation_engine cross-contract notes
@@ -118,21 +124,21 @@ CI drift tests compare its source-defined types and expected signatures against
## price_oracle
-| Function | Summary | Access control | Notes |
-| ------------------------------------------------------ | ------------------------------------------------ | ------------------------- | ------------------------------------------------------------------------------ |
-| initialize(admin) -> Result | Set admin and default staleness window. | None (single-use). | Initializes whitelist authority and versioned config. |
-| add_oracle(caller, oracle_address) -> Result | Add a trusted price publisher. | Admin require_auth. | Whitelisted oracle can overwrite the latest price for any asset it updates. |
-| remove_oracle(caller, oracle_address) -> Result | Remove a trusted price publisher. | Admin require_auth. | Prevents further updates from that address. |
-| is_oracle_whitelisted(address) -> bool | Check whitelist membership. | View. | Reads the admin-managed trust list. |
-| set_price(caller, asset, price, decimals) -> Result | Publish latest price for an asset. | Oracle require_auth. | Validates non-negative price; does not aggregate or reconcile multiple feeds. |
-| get_price(asset) -> PriceData | Read the raw latest price snapshot. | View. | Returns zeroed `PriceData` if unset; does not enforce freshness. |
-| get_price_valid(asset, max_staleness_override) -> Result | Read a fresh price snapshot or fail. | View. | Rejects stale and future-dated data; preferred for security-sensitive reads. |
-| set_max_staleness(caller, seconds) -> Result | Update default freshness window. | Admin require_auth. | Tunes rejection threshold for delayed oracle updates. |
-| get_max_staleness() -> u64 | Read default freshness window. | View. | Used when `get_price_valid` has no override. |
-| get_admin() -> Address | Read oracle admin. | View. | Returns the current whitelist/config authority. |
-| set_admin(caller, new_admin) -> Result | Transfer oracle admin authority. | Admin require_auth. | Transfers control over whitelist and configuration. |
-| upgrade(caller, new_wasm_hash) -> Result | Upgrade contract code. | Admin require_auth. | Validates non-zero WASM hash. |
-| migrate(caller, from_version) -> Result | Migrate legacy storage to current version. | Admin require_auth. | Replays are blocked once current version is installed. |
+| Function | Summary | Access control | Notes |
+| ------------------------------------------------------------------- | ------------------------------------------ | -------------------- | ----------------------------------------------------------------------------- |
+| initialize(admin) -> Result | Set admin and default staleness window. | None (single-use). | Initializes whitelist authority and versioned config. |
+| add_oracle(caller, oracle_address) -> Result | Add a trusted price publisher. | Admin require_auth. | Whitelisted oracle can overwrite the latest price for any asset it updates. |
+| remove_oracle(caller, oracle_address) -> Result | Remove a trusted price publisher. | Admin require_auth. | Prevents further updates from that address. |
+| is_oracle_whitelisted(address) -> bool | Check whitelist membership. | View. | Reads the admin-managed trust list. |
+| set_price(caller, asset, price, decimals) -> Result | Publish latest price for an asset. | Oracle require_auth. | Validates non-negative price; does not aggregate or reconcile multiple feeds. |
+| get_price(asset) -> PriceData | Read the raw latest price snapshot. | View. | Returns zeroed `PriceData` if unset; does not enforce freshness. |
+| get_price_valid(asset, max_staleness_override) -> Result | Read a fresh price snapshot or fail. | View. | Rejects stale and future-dated data; preferred for security-sensitive reads. |
+| set_max_staleness(caller, seconds) -> Result | Update default freshness window. | Admin require_auth. | Tunes rejection threshold for delayed oracle updates. |
+| get_max_staleness() -> u64 | Read default freshness window. | View. | Used when `get_price_valid` has no override. |
+| get_admin() -> Address | Read oracle admin. | View. | Returns the current whitelist/config authority. |
+| set_admin(caller, new_admin) -> Result | Transfer oracle admin authority. | Admin require_auth. | Transfers control over whitelist and configuration. |
+| upgrade(caller, new_wasm_hash) -> Result | Upgrade contract code. | Admin require_auth. | Validates non-zero WASM hash. |
+| migrate(caller, from_version) -> Result | Migrate legacy storage to current version. | Admin require_auth. | Replays are blocked once current version is installed. |
### price_oracle manipulation-resistance notes
@@ -242,20 +248,20 @@ cargo test --package commitment_nft test_transfer
## time_lock
-| Function | Summary | Access control | Notes |
-| --- | --- | --- | --- |
-| initialize(admin) | Set the initial timelock admin. | None (single-use). | Establishes the authority allowed to queue and cancel actions. |
-| queue_action(action_type, target, data, delay) -> Result | Queue a delayed governance action. | Stored admin `require_auth`. | Delay must be at least the action-type minimum and no more than 30 days. |
-| execute_action(action_id) -> Result | Execute a matured action. | Permissionless after delay. | Anyone may execute once `executable_at` is reached. |
-| cancel_action(action_id) -> Result | Cancel a queued action. | Stored admin `require_auth`. | Fails if the action already executed or was already cancelled. |
-| get_action(action_id) -> Result | Read queued action metadata. | View. | Includes `queued_at`, `executable_at`, and execution state. |
-| get_all_actions() -> Vec | Read all queued action ids. | View. | Includes executed and cancelled actions. |
-| get_pending_actions() -> Vec | Read actions that are neither executed nor cancelled. | View. | Useful for operator review and execution scans. |
-| get_executable_actions() -> Vec | Read pending actions whose delay has elapsed. | View. | Actions are executable at exactly `executable_at`. |
-| get_admin() -> Address | Read the current admin. | View. | Returns the authority for queue/cancel operations. |
-| get_min_delay(action_type) -> u64 | Read the minimum delay for an action type. | View. | Current floors: 1 day for parameter/fee, 2 days for admin, 3 days for upgrade. |
-| get_max_delay() -> u64 | Read the global maximum allowed delay. | View. | Hard cap is 30 days. |
-| get_action_count() -> u64 | Read total number of queued actions. | View. | Monotonic counter for action ids. |
+| Function | Summary | Access control | Notes |
+| ------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
+| initialize(admin) | Set the initial timelock admin. | None (single-use). | Establishes the authority allowed to queue and cancel actions. |
+| queue_action(action_type, target, data, delay) -> Result | Queue a delayed governance action. | Stored admin `require_auth`. | Delay must be at least the action-type minimum and no more than 30 days. |
+| execute_action(action_id) -> Result | Execute a matured action. | Permissionless after delay. | Anyone may execute once `executable_at` is reached. |
+| cancel_action(action_id) -> Result | Cancel a queued action. | Stored admin `require_auth`. | Fails if the action already executed or was already cancelled. |
+| get_action(action_id) -> Result | Read queued action metadata. | View. | Includes `queued_at`, `executable_at`, and execution state. |
+| get_all_actions() -> Vec | Read all queued action ids. | View. | Includes executed and cancelled actions. |
+| get_pending_actions() -> Vec | Read actions that are neither executed nor cancelled. | View. | Useful for operator review and execution scans. |
+| get_executable_actions() -> Vec | Read pending actions whose delay has elapsed. | View. | Actions are executable at exactly `executable_at`. |
+| get_admin() -> Address | Read the current admin. | View. | Returns the authority for queue/cancel operations. |
+| get_min_delay(action_type) -> u64 | Read the minimum delay for an action type. | View. | Current floors: 1 day for parameter/fee, 2 days for admin, 3 days for upgrade. |
+| get_max_delay() -> u64 | Read the global maximum allowed delay. | View. | Hard cap is 30 days. |
+| get_action_count() -> u64 | Read total number of queued actions. | View. | Monotonic counter for action ids. |
### time_lock operational notes