mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
## Summary This PR modernizes DataHaven's currency system by standardizing all three runtimes (mainnet, stagenet, testnet) to use Ethereum-compatible Wei-based units with HAVE as the native token name. ### Key Changes #### 🔄 Currency Unit Standardization - **Migrated from decimal-based to Wei-based system** (18 decimals) - **Wei units**: WEI → KILOWEI → MEGAWEI → GIGAWEI - **HAVE units**: MICROHAVE → MILLIHAVE → HAVE → KILOHAVE - **Zero Existential Deposit**: Enables dust account support with `insecure_zero_ed` feature #### 🏷️ Token Naming - **UNIT → HAVE**: Native token renamed to reflect DataHaven branding - **Consistent terminology**: All constants, comments, and documentation updated - **Supply factors preserved**: Mainnet (100x), Stagenet/Testnet (1x) #### ⚖️ Block Weights & Gas Configuration - **Solochain-optimized**: Updated MAX_POV_SIZE and block weight parameters - **EVM compatibility**: Fixed GasLimitPovSizeRatio (u32 → u64) and storage growth ratios - **Proper fee structure**: Aligned with Ethereum standards #### 🧪 Test Updates - **Treasury tests fixed**: Updated to handle zero existential deposit behavior - **All tests passing**: Currency references updated across all runtime tests - **Storage hub parameters**: Updated to use HAVE token terminology ### Breaking Changes ⚠️ **Currency precision changed from 12 to 18 decimals** - Applications using currency constants must update to new HAVE-based naming - ExistentialDeposit now 0 (was previously enforced minimum balance) ### Runtime Coverage - ✅ **Mainnet runtime** (supply factor: 100) - ✅ **Stagenet runtime** (supply factor: 1) - ✅ **Testnet runtime** (supply factor: 1) - ✅ **Storage hub parameters** (runtime params updated) ### Technical Details #### Fee Structure ```rust pub const TRANSACTION_BYTE_FEE: Balance = 1 * GIGAWEI * SUPPLY_FACTOR; pub const STORAGE_BYTE_FEE: Balance = 100 * MICROHAVE * SUPPLY_FACTOR; pub const WEIGHT_FEE: Balance = 50 * KILOWEI * SUPPLY_FACTOR / 4; ``` #### Block Configuration ```rust pub const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts( WEIGHT_REF_TIME_PER_SECOND.saturating_mul(2), MAX_POV_SIZE as u64, ); ``` ## Test Plan - [x] All runtime builds compile successfully - [x] All unit tests pass across three runtimes - [x] Treasury fee handling verified with zero existential deposit - [x] Storage hub parameter compatibility confirmed - [x] EVM gas limit calculations validated ## Files Modified **27 files changed, 728 insertions(+), 342 deletions(-)** - Runtime lib.rs files (currency module definitions) - Cargo.toml files (insecure_zero_ed feature) - Configuration modules (block weights, gas limits) - Test suites (currency constant references) - Storage hub runtime parameters --- 🤖 *Generated with [Claude Code](https://claude.ai/code)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Introduced a unified currency module with HAVE units (18 decimals), fees, and a deposit helper. - Adopted dynamic block weight/length configuration (5 MB blocks) and re-exported gas-to-weight constants. - Improvements - Switched all runtimes and pricing parameters from UNIT/NANO_UNIT to HAVE/GIGAWEI. - Updated deposits, fees, and rewards to HAVE-based values across modules (including StorageHub and Snowbridge). - Made existential deposit runtime-configurable; enabled zero-ED mode on selected networks. - Updated metadata hash and token metadata to reference HAVE (symbol wHAVE, 18 decimals). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
444 lines
14 KiB
Rust
444 lines
14 KiB
Rust
// Copyright 2025 Moonbeam Foundation.
|
|
// This file is part of DataHaven.
|
|
|
|
#[path = "common.rs"]
|
|
mod common;
|
|
|
|
use codec::Encode;
|
|
use common::*;
|
|
use datahaven_mainnet_runtime::{
|
|
configs::EthereumSovereignAccount, currency::HAVE, AccountId, Balance, Balances,
|
|
DataHavenNativeTransfer, Runtime, RuntimeEvent, RuntimeOrigin, SnowbridgeSystemV2, System,
|
|
};
|
|
use dhp_bridge::NativeTokenTransferMessageProcessor;
|
|
use frame_support::{assert_noop, assert_ok, traits::fungible::Inspect};
|
|
use pallet_datahaven_native_transfer::Event as NativeTransferEvent;
|
|
use snowbridge_core::TokenIdOf;
|
|
use snowbridge_inbound_queue_primitives::v2::{
|
|
EthereumAsset, Message as SnowbridgeMessage, MessageProcessor, Payload,
|
|
};
|
|
use snowbridge_pallet_outbound_queue_v2::Event as OutboundQueueEvent;
|
|
use snowbridge_pallet_system::NativeToForeignId;
|
|
use sp_core::Get;
|
|
use sp_core::{H160, H256};
|
|
use sp_runtime::DispatchError;
|
|
use xcm::prelude::*;
|
|
use xcm_executor::traits::ConvertLocation;
|
|
|
|
const TRANSFER_AMOUNT: Balance = 1000 * HAVE;
|
|
const FEE_AMOUNT: Balance = 10 * HAVE;
|
|
const ETH_ALICE: H160 = H160([0x11; 20]);
|
|
const ETH_BOB: H160 = H160([0x22; 20]);
|
|
|
|
// Get the gateway address from runtime configuration
|
|
fn gateway_address() -> H160 {
|
|
use datahaven_mainnet_runtime::configs::runtime_params::dynamic_params::runtime_config::EthereumGatewayAddress;
|
|
EthereumGatewayAddress::get()
|
|
}
|
|
|
|
fn register_native_token() -> H256 {
|
|
let asset_location = Location::here();
|
|
let _ = SnowbridgeSystemV2::register_token(
|
|
root_origin(),
|
|
Box::new(VersionedLocation::V5(asset_location.clone())),
|
|
Box::new(VersionedLocation::V5(asset_location.clone())),
|
|
datahaven_token_metadata(),
|
|
);
|
|
let reanchored = SnowbridgeSystemV2::reanchor(asset_location).unwrap();
|
|
TokenIdOf::convert_location(&reanchored).unwrap()
|
|
}
|
|
|
|
fn setup_sovereign_balance(amount: Balance) {
|
|
let _ = Balances::force_set_balance(root_origin(), EthereumSovereignAccount::get(), amount);
|
|
}
|
|
|
|
fn create_message(token_id: H256, amount: Balance, claimer: H160, nonce: u64) -> SnowbridgeMessage {
|
|
SnowbridgeMessage {
|
|
gateway: gateway_address(),
|
|
nonce,
|
|
origin: H160::zero(),
|
|
assets: vec![EthereumAsset::ForeignTokenERC20 {
|
|
token_id,
|
|
value: amount,
|
|
}],
|
|
xcm: Payload::Raw(vec![0x01, 0x02, 0x03]),
|
|
claimer: Some(claimer.encode()),
|
|
value: 0,
|
|
execution_fee: 100,
|
|
relayer_fee: 50,
|
|
}
|
|
}
|
|
|
|
// === Token Registration Tests ===
|
|
|
|
#[test]
|
|
fn native_token_registration_works() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let asset_location = Location::here();
|
|
|
|
// Register the native HAVE token with Snowbridge
|
|
assert_ok!(SnowbridgeSystemV2::register_token(
|
|
root_origin(),
|
|
Box::new(VersionedLocation::V5(asset_location.clone())),
|
|
Box::new(VersionedLocation::V5(asset_location.clone())),
|
|
datahaven_token_metadata()
|
|
));
|
|
|
|
// Verify token was registered and assigned a valid token ID
|
|
let reanchored = SnowbridgeSystemV2::reanchor(asset_location).unwrap();
|
|
let token_id = TokenIdOf::convert_location(&reanchored).unwrap();
|
|
|
|
assert_ne!(token_id, H256::zero());
|
|
assert_eq!(
|
|
NativeToForeignId::<Runtime>::get(&reanchored),
|
|
Some(token_id)
|
|
);
|
|
});
|
|
}
|
|
|
|
// === Outbound Transfer Tests ===
|
|
|
|
#[test]
|
|
fn transfer_to_ethereum_works() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let _token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
|
|
// Record initial balances
|
|
let alice_initial = Balances::balance(&alice);
|
|
let sovereign_initial = Balances::balance(&EthereumSovereignAccount::get());
|
|
|
|
// Transfer native tokens to Ethereum
|
|
assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum(
|
|
RuntimeOrigin::signed(alice.clone()),
|
|
ETH_ALICE,
|
|
TRANSFER_AMOUNT,
|
|
FEE_AMOUNT
|
|
));
|
|
|
|
// Verify Alice's balance decreased by transfer amount + fee
|
|
assert_eq!(
|
|
Balances::balance(&alice),
|
|
alice_initial - TRANSFER_AMOUNT - FEE_AMOUNT
|
|
);
|
|
|
|
// Verify tokens were locked in sovereign account
|
|
assert_eq!(
|
|
Balances::balance(&EthereumSovereignAccount::get()),
|
|
sovereign_initial + TRANSFER_AMOUNT
|
|
);
|
|
|
|
// Check event was emitted
|
|
assert!(System::events().iter().any(|e| matches!(
|
|
&e.event,
|
|
RuntimeEvent::DataHavenNativeTransfer(
|
|
NativeTransferEvent::TokensTransferredToEthereum { .. }
|
|
)
|
|
)));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn transfer_fails_when_paused() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let _token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
|
|
// Pause the pallet
|
|
assert_ok!(DataHavenNativeTransfer::pause(root_origin()));
|
|
|
|
// Verify transfers are disabled when paused
|
|
assert_noop!(
|
|
DataHavenNativeTransfer::transfer_to_ethereum(
|
|
RuntimeOrigin::signed(alice),
|
|
ETH_ALICE,
|
|
TRANSFER_AMOUNT,
|
|
FEE_AMOUNT
|
|
),
|
|
pallet_datahaven_native_transfer::Error::<Runtime>::TransfersDisabled
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_transfers_work() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let _token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
let bob = account_id(BOB);
|
|
|
|
assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum(
|
|
RuntimeOrigin::signed(alice),
|
|
ETH_ALICE,
|
|
TRANSFER_AMOUNT,
|
|
FEE_AMOUNT
|
|
));
|
|
|
|
assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum(
|
|
RuntimeOrigin::signed(bob),
|
|
ETH_BOB,
|
|
TRANSFER_AMOUNT,
|
|
FEE_AMOUNT
|
|
));
|
|
|
|
let expected_sovereign_balance = TRANSFER_AMOUNT * 2;
|
|
assert_eq!(
|
|
Balances::balance(&EthereumSovereignAccount::get()),
|
|
expected_sovereign_balance
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn treasury_collects_fees_from_multiple_transfers() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let _token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
let bob = account_id(BOB);
|
|
let treasury_account = datahaven_mainnet_runtime::configs::TreasuryAccount::get();
|
|
let initial_treasury_balance = Balances::balance(&treasury_account);
|
|
|
|
let fee1 = 5 * HAVE;
|
|
let fee2 = 10 * HAVE;
|
|
|
|
assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum(
|
|
RuntimeOrigin::signed(alice),
|
|
ETH_ALICE,
|
|
TRANSFER_AMOUNT,
|
|
fee1
|
|
));
|
|
|
|
assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum(
|
|
RuntimeOrigin::signed(bob),
|
|
ETH_BOB,
|
|
TRANSFER_AMOUNT,
|
|
fee2
|
|
));
|
|
|
|
let expected_treasury_balance = initial_treasury_balance + fee1 + fee2;
|
|
assert_eq!(
|
|
Balances::balance(&treasury_account),
|
|
expected_treasury_balance
|
|
);
|
|
});
|
|
}
|
|
|
|
// === Inbound Message Processing Tests ===
|
|
|
|
#[test]
|
|
fn message_processor_accepts_registered_token() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
let message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1);
|
|
|
|
// Verify processor accepts messages containing registered native token
|
|
assert!(
|
|
NativeTokenTransferMessageProcessor::<Runtime>::can_process_message(&alice, &message)
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn message_processor_rejects_unregistered_token() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let fake_token_id = H256::from_low_u64_be(0x9999);
|
|
let alice = account_id(ALICE);
|
|
let message = create_message(fake_token_id, TRANSFER_AMOUNT, ETH_ALICE, 1);
|
|
|
|
assert!(
|
|
!NativeTokenTransferMessageProcessor::<Runtime>::can_process_message(&alice, &message)
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn message_processor_rejects_empty_assets() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
|
|
let mut message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1);
|
|
message.assets = vec![];
|
|
|
|
assert!(
|
|
!NativeTokenTransferMessageProcessor::<Runtime>::can_process_message(&alice, &message)
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn inbound_message_processing_works() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
let message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1);
|
|
|
|
// Setup sovereign account with sufficient balance
|
|
setup_sovereign_balance(TRANSFER_AMOUNT * 2);
|
|
let sovereign_initial = Balances::balance(&EthereumSovereignAccount::get());
|
|
|
|
// Process inbound message from Ethereum
|
|
assert_ok!(
|
|
snowbridge_pallet_inbound_queue_v2::Pallet::<Runtime>::process_message(alice, message)
|
|
);
|
|
|
|
// Verify tokens were unlocked to recipient
|
|
let recipient: AccountId = ETH_ALICE.into();
|
|
assert_eq!(Balances::balance(&recipient), TRANSFER_AMOUNT);
|
|
|
|
// Verify sovereign balance decreased by transfer amount
|
|
assert_eq!(
|
|
Balances::balance(&EthereumSovereignAccount::get()),
|
|
sovereign_initial - TRANSFER_AMOUNT
|
|
);
|
|
|
|
// Check unlock event was emitted
|
|
assert!(System::events().iter().any(|e| matches!(
|
|
&e.event,
|
|
RuntimeEvent::DataHavenNativeTransfer(NativeTransferEvent::TokensUnlocked { .. })
|
|
)));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_assets_processing_sums_amounts() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
|
|
let mut message = create_message(token_id, 0, ETH_ALICE, 1);
|
|
message.assets = vec![
|
|
EthereumAsset::ForeignTokenERC20 {
|
|
token_id,
|
|
value: 300 * HAVE,
|
|
},
|
|
EthereumAsset::ForeignTokenERC20 {
|
|
token_id,
|
|
value: 200 * HAVE,
|
|
},
|
|
EthereumAsset::ForeignTokenERC20 {
|
|
token_id,
|
|
value: 500 * HAVE,
|
|
},
|
|
];
|
|
|
|
setup_sovereign_balance(2000 * HAVE);
|
|
|
|
assert_ok!(
|
|
snowbridge_pallet_inbound_queue_v2::Pallet::<Runtime>::process_message(alice, message)
|
|
);
|
|
|
|
let recipient: AccountId = ETH_ALICE.into();
|
|
let total_amount = 1000 * HAVE;
|
|
assert_eq!(Balances::balance(&recipient), total_amount);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn processing_fails_without_claimer() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
|
|
let mut message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1);
|
|
message.claimer = None;
|
|
|
|
assert_noop!(
|
|
snowbridge_pallet_inbound_queue_v2::Pallet::<Runtime>::process_message(alice, message),
|
|
DispatchError::Other("No claimer specified in message")
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn processing_fails_with_zero_amount() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
let message = create_message(token_id, 0, ETH_ALICE, 1);
|
|
|
|
assert_noop!(
|
|
snowbridge_pallet_inbound_queue_v2::Pallet::<Runtime>::process_message(alice, message),
|
|
DispatchError::Other("No native token found in assets")
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn processing_fails_with_insufficient_sovereign_balance() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
let message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1);
|
|
|
|
setup_sovereign_balance(TRANSFER_AMOUNT / 2); // Insufficient balance
|
|
|
|
let result =
|
|
snowbridge_pallet_inbound_queue_v2::Pallet::<Runtime>::process_message(alice, message);
|
|
assert!(result.is_err());
|
|
});
|
|
}
|
|
|
|
// === Integration Tests ===
|
|
|
|
#[test]
|
|
fn end_to_end_transfer_flow() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
|
|
// Step 1: Outbound transfer - Alice sends tokens to Ethereum
|
|
assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum(
|
|
RuntimeOrigin::signed(alice.clone()),
|
|
ETH_ALICE,
|
|
TRANSFER_AMOUNT,
|
|
FEE_AMOUNT
|
|
));
|
|
|
|
// Verify message was queued for Snowbridge
|
|
assert!(System::events().iter().any(|e| matches!(
|
|
&e.event,
|
|
RuntimeEvent::EthereumOutboundQueueV2(OutboundQueueEvent::MessageQueued { .. })
|
|
)));
|
|
|
|
// Step 2: Simulate inbound processing - tokens returning from Ethereum
|
|
let message = create_message(token_id, TRANSFER_AMOUNT, ETH_BOB, 1);
|
|
setup_sovereign_balance(TRANSFER_AMOUNT * 3);
|
|
|
|
assert_ok!(
|
|
snowbridge_pallet_inbound_queue_v2::Pallet::<Runtime>::process_message(alice, message)
|
|
);
|
|
|
|
// Verify tokens were delivered to recipient
|
|
let recipient: AccountId = ETH_BOB.into();
|
|
assert_eq!(Balances::balance(&recipient), TRANSFER_AMOUNT);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn message_routing_works_correctly() {
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
let token_id = register_native_token();
|
|
let alice = account_id(ALICE);
|
|
|
|
// Native token message should be accepted
|
|
let native_message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1);
|
|
assert!(
|
|
NativeTokenTransferMessageProcessor::<Runtime>::can_process_message(
|
|
&alice,
|
|
&native_message
|
|
)
|
|
);
|
|
|
|
// Non-native token message should be rejected
|
|
let fake_token_id = H256::from_low_u64_be(0x8888);
|
|
let non_native_message = create_message(fake_token_id, TRANSFER_AMOUNT, ETH_ALICE, 2);
|
|
assert!(
|
|
!NativeTokenTransferMessageProcessor::<Runtime>::can_process_message(
|
|
&alice,
|
|
&non_native_message
|
|
)
|
|
);
|
|
});
|
|
}
|