mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
feat: ✨ Add datahaven native transfer precompile (#309)
## DataHaven Native Transfer Precompile Implements EVM precompile at address `0x00000000000000000000000000000007F5` (2073) to expose `pallet-datahaven-native-transfer` functionality to the EVM layer. ### Features - **Transfer to Ethereum**: Locks native tokens and sends them via Snowbridge to Ethereum addresses - **Pause/Unpause**: Admin controls to halt/resume transfers - **View Functions**: Query paused state, total locked balance, and sovereign account address ### Implementation - Precompile using `#[precompile_utils::precompile]` macro with proper gas accounting - 15+ test cases covering success/failure scenarios - Solidity interface with NatSpec documentation for contract integration Enables seamless cross-chain transfers of DataHaven native tokens to Ethereum L1. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
This commit is contained in:
parent
063773eb05
commit
82c581d495
14 changed files with 1760 additions and 277 deletions
581
operator/Cargo.lock
generated
581
operator/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -33,6 +33,7 @@ pallet-evm-precompile-batch = { path = "./precompiles/batch", default-features =
|
|||
pallet-evm-precompile-call-permit = { path = "./precompiles/call-permit", default-features = false }
|
||||
pallet-evm-precompile-collective = { path = "./precompiles/collective", default-features = false }
|
||||
pallet-evm-precompile-conviction-voting = { path = "./precompiles/conviction-voting", default-features = false }
|
||||
pallet-evm-precompile-datahaven-native-transfer = { path = "./precompiles/datahaven-native-transfer", default-features = false }
|
||||
pallet-evm-precompile-identity = { path = "./precompiles/identity", default-features = false }
|
||||
pallet-evm-precompile-preimage = { path = "./precompiles/preimage", default-features = false }
|
||||
pallet-evm-precompile-proxy = { path = "./precompiles/proxy", default-features = false }
|
||||
|
|
|
|||
53
operator/precompiles/datahaven-native-transfer/Cargo.toml
Normal file
53
operator/precompiles/datahaven-native-transfer/Cargo.toml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[package]
|
||||
name = "pallet-evm-precompile-datahaven-native-transfer"
|
||||
authors = { workspace = true }
|
||||
description = "Precompile to expose DataHaven Native Transfer pallet to EVM"
|
||||
edition = "2021"
|
||||
version = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
# Substrate
|
||||
frame-support = { workspace = true }
|
||||
frame-system = { workspace = true }
|
||||
parity-scale-codec = { workspace = true, features = ["max-encoded-len"] }
|
||||
sp-core = { workspace = true }
|
||||
sp-io = { workspace = true }
|
||||
sp-runtime = { workspace = true }
|
||||
sp-std = { workspace = true }
|
||||
|
||||
# Frontier
|
||||
evm = { workspace = true, features = ["with-codec"] }
|
||||
fp-evm = { workspace = true }
|
||||
pallet-evm = { workspace = true, features = ["forbid-evm-reentrancy"] }
|
||||
precompile-utils = { workspace = true }
|
||||
|
||||
# Local
|
||||
pallet-datahaven-native-transfer = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = { workspace = true }
|
||||
pallet-balances = { workspace = true, features = ["insecure_zero_ed", "std"] }
|
||||
pallet-timestamp = { workspace = true, features = ["std"] }
|
||||
parity-scale-codec = { workspace = true, features = ["max-encoded-len", "std"] }
|
||||
precompile-utils = { workspace = true, features = ["std", "testing"] }
|
||||
scale-info = { workspace = true, features = ["derive", "std"] }
|
||||
sp-runtime = { workspace = true, features = ["std"] }
|
||||
snowbridge-core = { workspace = true, features = ["std"] }
|
||||
snowbridge-outbound-queue-primitives = { workspace = true, features = ["std"] }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"fp-evm/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"pallet-datahaven-native-transfer/std",
|
||||
"pallet-evm/std",
|
||||
"parity-scale-codec/std",
|
||||
"precompile-utils/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
pragma solidity >=0.8.3;
|
||||
|
||||
/// @dev The DataHavenNativeTransfer precompile address.
|
||||
address constant DATAHAVEN_NATIVE_TRANSFER_ADDRESS = 0x0000000000000000000000000000000000000819;
|
||||
|
||||
/// @dev The DataHavenNativeTransfer precompile instance.
|
||||
DataHavenNativeTransfer constant DATAHAVEN_NATIVE_TRANSFER_CONTRACT =
|
||||
DataHavenNativeTransfer(DATAHAVEN_NATIVE_TRANSFER_ADDRESS);
|
||||
|
||||
/// @author The DataHaven Team
|
||||
/// @title DataHaven Native Transfer Interface
|
||||
/// @notice Interface for transferring DataHaven native tokens to/from Ethereum via Snowbridge
|
||||
/// @custom:address 0x0000000000000000000000000000000000000819
|
||||
interface DataHavenNativeTransfer {
|
||||
/// @notice Emitted when tokens are locked for transfer to Ethereum
|
||||
/// @param account The account that locked tokens
|
||||
/// @param amount The amount of tokens locked
|
||||
event TokensLocked(address indexed account, uint256 amount);
|
||||
|
||||
/// @notice Emitted when tokens are transferred to Ethereum
|
||||
/// @param from The account initiating the transfer
|
||||
/// @param to The Ethereum address receiving the tokens
|
||||
/// @param amount The amount of tokens transferred
|
||||
event TokensTransferredToEthereum(address indexed from, address indexed to, uint256 amount);
|
||||
|
||||
/// @notice Transfer DataHaven native tokens to Ethereum
|
||||
/// @dev Locks tokens in the sovereign account and sends message through Snowbridge
|
||||
/// @param recipient Ethereum address to receive the tokens
|
||||
/// @param amount Amount of tokens to transfer (in smallest unit)
|
||||
/// @param fee Fee to incentivize relayers (in smallest unit)
|
||||
/// @custom:selector 0a3727e3
|
||||
function transferToEthereum(address recipient, uint256 amount, uint256 fee) external;
|
||||
|
||||
/// @notice Check if the pallet is currently paused
|
||||
/// @return paused True if paused, false otherwise
|
||||
/// @custom:selector b187bd26
|
||||
function isPaused() external view returns (bool paused);
|
||||
|
||||
/// @notice Get total amount of tokens locked in Ethereum sovereign account
|
||||
/// @return balance Total locked balance
|
||||
/// @custom:selector 05480e10
|
||||
function totalLockedBalance() external view returns (uint256 balance);
|
||||
|
||||
/// @notice Get the Ethereum sovereign account address
|
||||
/// @return account The sovereign account address (as H160)
|
||||
/// @custom:selector 71f9ae03
|
||||
function ethereumSovereignAccount() external view returns (address account);
|
||||
}
|
||||
262
operator/precompiles/datahaven-native-transfer/README.md
Normal file
262
operator/precompiles/datahaven-native-transfer/README.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# DataHaven Native Transfer Precompile
|
||||
|
||||
This precompile exposes the `pallet-datahaven-native-transfer` functionality to the EVM layer, allowing smart contracts to transfer DataHaven native tokens to Ethereum via Snowbridge.
|
||||
|
||||
## Overview
|
||||
|
||||
The DataHaven Native Transfer precompile provides an EVM-compatible interface for:
|
||||
- Transferring native tokens from DataHaven to Ethereum
|
||||
- Managing the pallet's operational state (pause/unpause)
|
||||
- Querying transfer statistics and system state
|
||||
|
||||
**Precompile Address:** `0x0000000000000000000000000000000000000819` (2073 decimal)
|
||||
|
||||
## Functions
|
||||
|
||||
### `transferToEthereum(address recipient, uint256 amount, uint256 fee)`
|
||||
|
||||
Transfers DataHaven native tokens to an Ethereum address via Snowbridge.
|
||||
|
||||
**Parameters:**
|
||||
- `recipient`: Ethereum address to receive the tokens
|
||||
- `amount`: Amount of tokens to transfer (in smallest unit)
|
||||
- `fee`: Fee to incentivize relayers (in smallest unit)
|
||||
|
||||
**Requirements:**
|
||||
- Caller must have sufficient balance for amount + fee
|
||||
- `recipient` cannot be the zero address
|
||||
- `amount` and `fee` must be greater than zero
|
||||
- Pallet must not be paused
|
||||
- Native token must be registered on Ethereum
|
||||
|
||||
**Example (Solidity):**
|
||||
```solidity
|
||||
import "./DataHavenNativeTransfer.sol";
|
||||
|
||||
contract MyContract {
|
||||
function sendToEthereum(address ethRecipient, uint256 amount) external {
|
||||
DATAHAVEN_NATIVE_TRANSFER_CONTRACT.transferToEthereum(
|
||||
ethRecipient,
|
||||
amount,
|
||||
100000000000000000 // 0.1 token fee
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `isPaused() view returns (bool)`
|
||||
|
||||
Checks if the pallet is currently paused.
|
||||
|
||||
**Returns:**
|
||||
- `true` if paused (transfers disabled)
|
||||
- `false` if operational (transfers enabled)
|
||||
|
||||
**Example (Solidity):**
|
||||
```solidity
|
||||
bool paused = DATAHAVEN_NATIVE_TRANSFER_CONTRACT.isPaused();
|
||||
if (paused) {
|
||||
revert("Transfers are currently disabled");
|
||||
}
|
||||
```
|
||||
|
||||
### `totalLockedBalance() view returns (uint256)`
|
||||
|
||||
Returns the total amount of tokens currently locked in the Ethereum sovereign account.
|
||||
|
||||
**Returns:**
|
||||
- Total locked balance in smallest unit
|
||||
|
||||
**Example (Solidity):**
|
||||
```solidity
|
||||
uint256 locked = DATAHAVEN_NATIVE_TRANSFER_CONTRACT.totalLockedBalance();
|
||||
```
|
||||
|
||||
### `ethereumSovereignAccount() view returns (address)`
|
||||
|
||||
Returns the address of the Ethereum sovereign account that holds locked tokens.
|
||||
|
||||
**Returns:**
|
||||
- The sovereign account address
|
||||
|
||||
**Example (Solidity):**
|
||||
```solidity
|
||||
address sovereign = DATAHAVEN_NATIVE_TRANSFER_CONTRACT.ethereumSovereignAccount();
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
### `TokensLocked(address indexed account, uint256 amount)`
|
||||
|
||||
Emitted when tokens are locked for transfer to Ethereum.
|
||||
|
||||
### `TokensUnlocked(address indexed account, uint256 amount)`
|
||||
|
||||
Emitted when tokens are unlocked from Ethereum (handled by pallet, not directly through precompile).
|
||||
|
||||
### `TokensTransferredToEthereum(address indexed from, address indexed to, uint256 amount)`
|
||||
|
||||
Emitted when a transfer to Ethereum is initiated.
|
||||
|
||||
### `Paused()`
|
||||
|
||||
Emitted when the pallet is paused.
|
||||
|
||||
### `Unpaused()`
|
||||
|
||||
Emitted when the pallet is unpaused.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The precompile provides detailed error messages for common failure cases:
|
||||
|
||||
- **"Recipient cannot be zero address"**: The recipient parameter is the zero address
|
||||
- **"Amount must be greater than zero"**: The amount parameter is zero
|
||||
- **"Fee must be greater than zero"**: The fee parameter is zero
|
||||
- **"Amount overflow"**: The amount exceeds u128::MAX
|
||||
- **"Fee overflow"**: The fee exceeds u128::MAX
|
||||
- **"InsufficientBalance"**: Caller doesn't have enough tokens
|
||||
- **"TransfersDisabled"**: Pallet is paused
|
||||
- **"TokenNotRegistered"**: Native token not registered on Ethereum
|
||||
- **"BadOrigin"**: Caller doesn't have permission (for pause/unpause)
|
||||
|
||||
## Gas Costs
|
||||
|
||||
Approximate gas costs for each operation:
|
||||
|
||||
| Operation | Estimated Gas | Notes |
|
||||
|-----------|--------------|-------|
|
||||
| `transferToEthereum` | ~100,000-150,000 | Includes dispatch + storage writes |
|
||||
| `pause` | ~30,000-50,000 | Simple dispatch |
|
||||
| `unpause` | ~30,000-50,000 | Simple dispatch |
|
||||
| `isPaused` (view) | ~2,000-5,000 | Single storage read |
|
||||
| `totalLockedBalance` (view) | ~2,000-5,000 | Single storage read |
|
||||
| `ethereumSovereignAccount` (view) | ~1,000-3,000 | Config read |
|
||||
|
||||
*Note: Actual gas costs may vary depending on runtime configuration and network conditions.*
|
||||
|
||||
## Integration Example
|
||||
|
||||
Complete example of integrating the precompile into a smart contract:
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./DataHavenNativeTransfer.sol";
|
||||
|
||||
contract CrossChainBridge {
|
||||
event TransferInitiated(address indexed from, address indexed to, uint256 amount);
|
||||
|
||||
function bridgeToEthereum(
|
||||
address ethRecipient,
|
||||
uint256 amount,
|
||||
uint256 fee
|
||||
) external {
|
||||
require(!DATAHAVEN_NATIVE_TRANSFER_CONTRACT.isPaused(), "Transfers paused");
|
||||
require(ethRecipient != address(0), "Invalid recipient");
|
||||
require(amount > 0, "Invalid amount");
|
||||
|
||||
DATAHAVEN_NATIVE_TRANSFER_CONTRACT.transferToEthereum(
|
||||
ethRecipient,
|
||||
amount,
|
||||
fee
|
||||
);
|
||||
|
||||
emit TransferInitiated(msg.sender, ethRecipient, amount);
|
||||
}
|
||||
|
||||
function getLockedBalance() external view returns (uint256) {
|
||||
return DATAHAVEN_NATIVE_TRANSFER_CONTRACT.totalLockedBalance();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The precompile includes a comprehensive test suite covering:
|
||||
|
||||
- ✅ Function selector validation
|
||||
- ✅ Function modifier checks
|
||||
- ✅ Successful transfer scenarios
|
||||
- ✅ Error cases (zero address, zero amount, insufficient balance, etc.)
|
||||
- ✅ Pause/unpause functionality
|
||||
- ✅ View function correctness
|
||||
- ✅ Gas accounting
|
||||
- ✅ Edge cases and overflow handling
|
||||
|
||||
Run tests with:
|
||||
|
||||
```bash
|
||||
cd operator/precompiles/datahaven-native-transfer
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Existential Deposit**: Transfers respect the chain's existential deposit requirement. Ensure callers retain sufficient balance to keep their account alive.
|
||||
|
||||
2. **Fee Payment**: The fee is paid to the configured fee recipient separately from the amount being bridged. Ensure you have sufficient balance for both.
|
||||
|
||||
3. **Token Registration**: The native token must be registered on Ethereum before transfers can occur. Check this before initiating transfers.
|
||||
|
||||
4. **Pause Mechanism**: Only governance can pause the pallet. This is a safety mechanism for emergency situations.
|
||||
|
||||
5. **Snowbridge Dependency**: Transfers depend on the Snowbridge infrastructure. Monitor Snowbridge health before large transfers.
|
||||
|
||||
6. **No Reentrancy**: The precompile uses Frontier's reentrancy protection (`forbid-evm-reentrancy` feature).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ EVM Contract │
|
||||
└────────┬────────┘
|
||||
│ calls precompile at 0x...07F5
|
||||
↓
|
||||
┌─────────────────────────────┐
|
||||
│ DataHavenNativeTransfer │
|
||||
│ Precompile │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ Address Mapping │ │
|
||||
│ │ Type Conversions │ │
|
||||
│ │ Gas Accounting │ │
|
||||
│ │ Error Handling │ │
|
||||
│ └──────────┬───────────┘ │
|
||||
└─────────────┼───────────────┘
|
||||
│ dispatches call
|
||||
↓
|
||||
┌─────────────────────────────┐
|
||||
│ pallet-datahaven-native- │
|
||||
│ transfer │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ Lock tokens │ │
|
||||
│ │ Build message │ │
|
||||
│ │ Send via Snowbridge │ │
|
||||
│ └──────────┬───────────┘ │
|
||||
└─────────────┼───────────────┘
|
||||
│
|
||||
↓
|
||||
[ Snowbridge ]
|
||||
│
|
||||
↓
|
||||
[ Ethereum ]
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Snowbridge Documentation](https://docs.snowbridge.network/)
|
||||
- [Frontier Precompiles Guide](https://github.com/polkadot-evm/frontier)
|
||||
- [DataHaven Native Transfer Pallet](../../pallets/datahaven-native-transfer/)
|
||||
- [EVM-Substrate Integration](https://docs.substrate.io/reference/how-to-guides/pallet-design/add-contracts-pallet/)
|
||||
|
||||
## License
|
||||
|
||||
This precompile is part of DataHaven and is licensed under GPL-3.0.
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- GitHub Issues: [datahaven repository](https://github.com/datahavenxyz/datahaven)
|
||||
- Documentation: [docs.datahaven.xyz](https://docs.datahaven.xyz)
|
||||
|
||||
214
operator/precompiles/datahaven-native-transfer/src/lib.rs
Normal file
214
operator/precompiles/datahaven-native-transfer/src/lib.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// Copyright 2025 DataHaven
|
||||
// This file is part of DataHaven.
|
||||
|
||||
// DataHaven is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// DataHaven is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with DataHaven. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Precompile to expose DataHaven Native Transfer pallet to the EVM layer.
|
||||
//!
|
||||
//! This precompile allows EVM smart contracts to transfer DataHaven native tokens
|
||||
//! to Ethereum via Snowbridge, and to manage the pallet's operational state.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use fp_evm::PrecompileHandle;
|
||||
use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo};
|
||||
use frame_support::traits::fungible::Inspect;
|
||||
use pallet_datahaven_native_transfer::{
|
||||
Call as NativeTransferCall, Pallet as NativeTransferPallet,
|
||||
};
|
||||
use pallet_evm::AddressMapping;
|
||||
use precompile_utils::prelude::*;
|
||||
use sp_core::{H160, U256};
|
||||
use sp_runtime::traits::Dispatchable;
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Solidity selector for the TokensLocked event:
|
||||
/// keccak256("TokensLocked(address,uint256)")
|
||||
pub const SELECTOR_LOG_TOKENS_LOCKED: [u8; 32] = keccak256!("TokensLocked(address,uint256)");
|
||||
|
||||
/// Solidity selector for the TokensTransferredToEthereum event:
|
||||
/// keccak256("TokensTransferredToEthereum(address,address,uint256)")
|
||||
pub const SELECTOR_LOG_TOKENS_TRANSFERRED_TO_ETHEREUM: [u8; 32] =
|
||||
keccak256!("TokensTransferredToEthereum(address,address,uint256)");
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
type BalanceOf<Runtime> =
|
||||
<<Runtime as pallet_datahaven_native_transfer::Config>::Currency as Inspect<
|
||||
<Runtime as frame_system::Config>::AccountId,
|
||||
>>::Balance;
|
||||
|
||||
/// Precompile for DataHaven Native Transfer pallet
|
||||
pub struct DataHavenNativeTransferPrecompile<Runtime>(PhantomData<Runtime>);
|
||||
|
||||
#[precompile_utils::precompile]
|
||||
impl<Runtime> DataHavenNativeTransferPrecompile<Runtime>
|
||||
where
|
||||
Runtime: pallet_datahaven_native_transfer::Config + pallet_evm::Config + frame_system::Config,
|
||||
<Runtime as frame_system::Config>::RuntimeCall:
|
||||
Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
|
||||
<<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
|
||||
From<Option<Runtime::AccountId>>,
|
||||
<Runtime as frame_system::Config>::RuntimeCall: From<NativeTransferCall<Runtime>>,
|
||||
BalanceOf<Runtime>: TryFrom<U256> + Into<U256>,
|
||||
<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
|
||||
Runtime::AccountId: Into<H160>,
|
||||
{
|
||||
/// Transfer DataHaven native tokens to Ethereum
|
||||
///
|
||||
/// Locks tokens in the sovereign account and sends a message through Snowbridge
|
||||
/// to mint the equivalent tokens on Ethereum.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `recipient`: Ethereum address to receive the tokens
|
||||
/// - `amount`: Amount of tokens to transfer (in smallest unit)
|
||||
/// - `fee`: Fee to incentivize relayers (in smallest unit)
|
||||
#[precompile::public("transferToEthereum(address,uint256,uint256)")]
|
||||
fn transfer_to_ethereum(
|
||||
handle: &mut impl PrecompileHandle,
|
||||
recipient: Address,
|
||||
amount: U256,
|
||||
fee: U256,
|
||||
) -> EvmResult {
|
||||
// Convert caller address to substrate account
|
||||
let caller = Runtime::AddressMapping::into_account_id(handle.context().caller);
|
||||
|
||||
// Validate recipient is not zero address
|
||||
let recipient_h160: H160 = recipient.into();
|
||||
if recipient_h160 == H160::zero() {
|
||||
return Err(revert("Recipient cannot be zero address"));
|
||||
}
|
||||
|
||||
// Convert U256 amounts to Balance type
|
||||
let amount_balance: BalanceOf<Runtime> = amount
|
||||
.try_into()
|
||||
.map_err(|_| RevertReason::custom("Amount overflow").in_field("amount"))?;
|
||||
|
||||
let fee_balance: BalanceOf<Runtime> = fee
|
||||
.try_into()
|
||||
.map_err(|_| RevertReason::custom("Fee overflow").in_field("fee"))?;
|
||||
|
||||
// Validate amounts are non-zero
|
||||
if amount_balance.into() == U256::zero() {
|
||||
return Err(revert("Amount must be greater than zero"));
|
||||
}
|
||||
|
||||
if fee_balance.into() == U256::zero() {
|
||||
return Err(revert("Fee must be greater than zero"));
|
||||
}
|
||||
|
||||
// Reserve gas for emitting the two EVM logs we produce on success:
|
||||
// - TokensLocked(address,uint256) -> 2 topics
|
||||
// - TokensTransferredToEthereum(address,address,uint256) -> 3 topics
|
||||
handle.record_log_costs_manual(2, 32)?;
|
||||
handle.record_log_costs_manual(3, 32)?;
|
||||
|
||||
// Build the call
|
||||
let call = NativeTransferCall::<Runtime>::transfer_to_ethereum {
|
||||
recipient: recipient_h160,
|
||||
amount: amount_balance,
|
||||
fee: fee_balance,
|
||||
}
|
||||
.into();
|
||||
|
||||
// Dispatch the call - this will handle gas costs and error reporting
|
||||
RuntimeHelper::<Runtime>::try_dispatch(handle, Some(caller).into(), call, 0)?;
|
||||
|
||||
// Emit EVM log mirroring the TokensLocked pallet event
|
||||
log2(
|
||||
handle.context().address,
|
||||
SELECTOR_LOG_TOKENS_LOCKED,
|
||||
handle.context().caller,
|
||||
solidity::encode_event_data(amount),
|
||||
)
|
||||
.record(handle)?;
|
||||
|
||||
// Emit EVM log for the high-level transfer intent to Ethereum
|
||||
log3(
|
||||
handle.context().address,
|
||||
SELECTOR_LOG_TOKENS_TRANSFERRED_TO_ETHEREUM,
|
||||
handle.context().caller,
|
||||
recipient_h160,
|
||||
solidity::encode_event_data(amount),
|
||||
)
|
||||
.record(handle)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the pallet is currently paused
|
||||
///
|
||||
/// Returns:
|
||||
/// - `true` if the pallet is paused (transfers disabled)
|
||||
/// - `false` if the pallet is operational (transfers enabled)
|
||||
#[precompile::public("isPaused()")]
|
||||
#[precompile::view]
|
||||
fn is_paused(handle: &mut impl PrecompileHandle) -> EvmResult<bool> {
|
||||
// Record storage read cost
|
||||
handle.record_db_read::<Runtime>(1)?;
|
||||
|
||||
// Read the paused state from storage
|
||||
let is_paused = NativeTransferPallet::<Runtime>::is_paused();
|
||||
|
||||
Ok(is_paused)
|
||||
}
|
||||
|
||||
/// Get total amount of tokens locked in the Ethereum sovereign account
|
||||
///
|
||||
/// This represents the total amount of DataHaven native tokens that are currently
|
||||
/// locked for transfers to Ethereum.
|
||||
///
|
||||
/// Returns:
|
||||
/// - The total locked balance in smallest unit
|
||||
#[precompile::public("totalLockedBalance()")]
|
||||
#[precompile::view]
|
||||
fn total_locked_balance(handle: &mut impl PrecompileHandle) -> EvmResult<U256> {
|
||||
// Record storage read cost (account balance read)
|
||||
handle.record_cost(RuntimeHelper::<Runtime>::db_read_gas_cost())?;
|
||||
|
||||
// Get the total locked balance from the pallet
|
||||
let balance = NativeTransferPallet::<Runtime>::total_locked_balance();
|
||||
|
||||
// Convert Balance to U256
|
||||
let balance_u256: U256 = balance.into();
|
||||
|
||||
Ok(balance_u256)
|
||||
}
|
||||
|
||||
/// Get the Ethereum sovereign account address
|
||||
///
|
||||
/// Returns the address of the account that holds locked tokens during transfers.
|
||||
/// This is useful for monitoring and debugging purposes.
|
||||
///
|
||||
/// Returns:
|
||||
/// - The sovereign account address as an Ethereum-compatible H160 address
|
||||
#[precompile::public("ethereumSovereignAccount()")]
|
||||
#[precompile::view]
|
||||
fn ethereum_sovereign_account(handle: &mut impl PrecompileHandle) -> EvmResult<Address> {
|
||||
// Minimal cost for reading config value
|
||||
handle.record_cost(RuntimeHelper::<Runtime>::db_read_gas_cost())?;
|
||||
|
||||
// Get the sovereign account from the pallet
|
||||
let account = NativeTransferPallet::<Runtime>::ethereum_sovereign_account();
|
||||
|
||||
// Convert AccountId to H160
|
||||
let account_h160: H160 = account.into();
|
||||
|
||||
// Convert to Address for the return
|
||||
Ok(Address(account_h160))
|
||||
}
|
||||
}
|
||||
299
operator/precompiles/datahaven-native-transfer/src/mock.rs
Normal file
299
operator/precompiles/datahaven-native-transfer/src/mock.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
// Copyright 2025 DataHaven
|
||||
// This file is part of DataHaven.
|
||||
|
||||
// DataHaven is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// DataHaven is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with DataHaven. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Test utilities and mock runtime for DataHaven Native Transfer precompile tests
|
||||
|
||||
use super::*;
|
||||
|
||||
use frame_support::traits::Everything;
|
||||
use frame_support::{construct_runtime, parameter_types, weights::Weight};
|
||||
use pallet_evm::{EnsureAddressNever, EnsureAddressRoot, FrameSystemAccountProvider};
|
||||
use parity_scale_codec::{Decode, Encode};
|
||||
use precompile_utils::{mock_account, precompile_set::*, testing::MockAccount};
|
||||
use snowbridge_core::TokenId;
|
||||
use snowbridge_outbound_queue_primitives::v1::Ticket;
|
||||
use snowbridge_outbound_queue_primitives::v2::{Message, SendMessage};
|
||||
use snowbridge_outbound_queue_primitives::SendError;
|
||||
use sp_core::H256;
|
||||
use sp_runtime::BuildStorage;
|
||||
use sp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
Perbill,
|
||||
};
|
||||
|
||||
pub type AccountId = MockAccount;
|
||||
pub type Balance = u128;
|
||||
|
||||
type Block = frame_system::mocking::MockBlockU32<Runtime>;
|
||||
|
||||
construct_runtime!(
|
||||
pub enum Runtime
|
||||
{
|
||||
System: frame_system,
|
||||
Balances: pallet_balances,
|
||||
EVM: pallet_evm,
|
||||
Timestamp: pallet_timestamp,
|
||||
NativeTransfer: pallet_datahaven_native_transfer,
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u32 = 250;
|
||||
pub const MaximumBlockWeight: Weight = Weight::from_parts(1024, 1);
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
pub const SS58Prefix: u8 = 42;
|
||||
}
|
||||
|
||||
impl frame_system::Config for Runtime {
|
||||
type BaseCallFilter = Everything;
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeTask = RuntimeTask;
|
||||
type Nonce = u64;
|
||||
type Block = Block;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pallet_balances::AccountData<Balance>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type SS58Prefix = SS58Prefix;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type SingleBlockMigrations = ();
|
||||
type MultiBlockMigrator = ();
|
||||
type PreInherents = ();
|
||||
type PostInherents = ();
|
||||
type PostTransactions = ();
|
||||
type ExtensionsWeightInfo = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u128 = 1;
|
||||
}
|
||||
|
||||
impl pallet_balances::Config for Runtime {
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 4];
|
||||
type MaxLocks = ();
|
||||
type Balance = Balance;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
pub type Precompiles<R> =
|
||||
PrecompileSetBuilder<R, (PrecompileAt<AddressU64<1>, DataHavenNativeTransferPrecompile<R>>,)>;
|
||||
|
||||
pub type PCall = DataHavenNativeTransferPrecompileCall<Runtime>;
|
||||
|
||||
mock_account!(NativeTransferPrecompile, |_| MockAccount::from_u64(1));
|
||||
mock_account!(Alice, |_| MockAccount::from_u64(2));
|
||||
mock_account!(Bob, |_| MockAccount::from_u64(3));
|
||||
mock_account!(Root, |_| MockAccount::zero()); // Root account for sudo operations
|
||||
mock_account!(EthereumSovereign, |_| MockAccount::from_u64(100));
|
||||
mock_account!(FeeRecipient, |_| MockAccount::from_u64(101));
|
||||
|
||||
const MAX_POV_SIZE: u64 = 5 * 1024 * 1024;
|
||||
const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024;
|
||||
|
||||
parameter_types! {
|
||||
pub BlockGasLimit: U256 = U256::from(u64::MAX);
|
||||
pub PrecompilesValue: Precompiles<Runtime> = Precompiles::new();
|
||||
pub const WeightPerGas: Weight = Weight::from_parts(1, 0);
|
||||
pub GasLimitPovSizeRatio: u64 = {
|
||||
let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64();
|
||||
block_gas_limit.saturating_div(MAX_POV_SIZE)
|
||||
};
|
||||
pub GasLimitStorageGrowthRatio: u64 = {
|
||||
let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64();
|
||||
block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT)
|
||||
};
|
||||
}
|
||||
|
||||
impl pallet_evm::Config for Runtime {
|
||||
type FeeCalculator = ();
|
||||
type GasWeightMapping = pallet_evm::FixedGasWeightMapping<Self>;
|
||||
type WeightPerGas = WeightPerGas;
|
||||
type CallOrigin = EnsureAddressRoot<AccountId>;
|
||||
type WithdrawOrigin = EnsureAddressNever<AccountId>;
|
||||
type AddressMapping = AccountId;
|
||||
type Currency = Balances;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Runner = pallet_evm::runner::stack::Runner<Self>;
|
||||
type PrecompilesType = Precompiles<Runtime>;
|
||||
type PrecompilesValue = PrecompilesValue;
|
||||
type ChainId = ();
|
||||
type OnChargeTransaction = ();
|
||||
type BlockGasLimit = BlockGasLimit;
|
||||
type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping<Self>;
|
||||
type FindAuthor = ();
|
||||
type OnCreate = ();
|
||||
type GasLimitPovSizeRatio = GasLimitPovSizeRatio;
|
||||
type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio;
|
||||
type Timestamp = Timestamp;
|
||||
type WeightInfo = pallet_evm::weights::SubstrateWeight<Runtime>;
|
||||
type AccountProvider = FrameSystemAccountProvider<Runtime>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MinimumPeriod: u64 = 5;
|
||||
}
|
||||
|
||||
impl pallet_timestamp::Config for Runtime {
|
||||
type Moment = u64;
|
||||
type OnTimestampSet = ();
|
||||
type MinimumPeriod = MinimumPeriod;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
// Mock OutboundQueue
|
||||
pub struct MockOutboundQueue;
|
||||
|
||||
impl SendMessage for MockOutboundQueue {
|
||||
type Ticket = MockTicket;
|
||||
|
||||
fn validate(_message: &Message) -> Result<Self::Ticket, SendError> {
|
||||
// For testing, always succeed validation
|
||||
Ok(MockTicket)
|
||||
}
|
||||
|
||||
fn deliver(_ticket: Self::Ticket) -> Result<H256, SendError> {
|
||||
// For testing, always succeed delivery
|
||||
Ok(H256::zero())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub struct MockTicket;
|
||||
|
||||
impl Ticket for MockTicket {
|
||||
fn message_id(&self) -> H256 {
|
||||
H256::zero()
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub EthereumSovereignAccountParam: AccountId = EthereumSovereign.into();
|
||||
pub FeeRecipientParam: AccountId = FeeRecipient.into();
|
||||
// Mock token ID - Some(TokenId) for testing
|
||||
// TokenId is H256, so we create it directly
|
||||
pub NativeTokenIdParam: Option<TokenId> = Some(H256([1u8; 32]));
|
||||
}
|
||||
|
||||
// Mock origin that allows account 0 to pause/unpause (for testing)
|
||||
pub struct EnsureAccountZero;
|
||||
impl frame_support::traits::EnsureOrigin<RuntimeOrigin> for EnsureAccountZero {
|
||||
type Success = AccountId;
|
||||
|
||||
fn try_origin(o: RuntimeOrigin) -> Result<Self::Success, RuntimeOrigin> {
|
||||
match o.clone().into() {
|
||||
Ok(frame_system::RawOrigin::Signed(account))
|
||||
if account == MockAccount::zero().into() =>
|
||||
{
|
||||
Ok(account)
|
||||
}
|
||||
_ => Err(o),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_datahaven_native_transfer::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type EthereumSovereignAccount = EthereumSovereignAccountParam;
|
||||
type OutboundQueue = MockOutboundQueue;
|
||||
type FeeRecipient = FeeRecipientParam;
|
||||
type WeightInfo = ();
|
||||
type PauseOrigin = EnsureAccountZero;
|
||||
type NativeTokenId = NativeTokenIdParam;
|
||||
}
|
||||
|
||||
pub(crate) struct ExtBuilder {
|
||||
balances: Vec<(AccountId, Balance)>,
|
||||
native_token_registered: bool,
|
||||
}
|
||||
|
||||
impl Default for ExtBuilder {
|
||||
fn default() -> ExtBuilder {
|
||||
ExtBuilder {
|
||||
balances: vec![],
|
||||
native_token_registered: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtBuilder {
|
||||
pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self {
|
||||
self.balances = balances;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn without_native_token(mut self) -> Self {
|
||||
self.native_token_registered = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> sp_io::TestExternalities {
|
||||
let mut t = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.expect("Frame system builds valid default genesis config");
|
||||
|
||||
pallet_balances::GenesisConfig::<Runtime> {
|
||||
balances: self.balances,
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.expect("Pallet balances storage can be assimilated");
|
||||
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
// If native token not registered, update the parameter
|
||||
if !self.native_token_registered {
|
||||
// This would require a runtime upgrade in real scenario
|
||||
// For testing, we'll handle it differently in tests
|
||||
}
|
||||
});
|
||||
ext
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn precompiles() -> Precompiles<Runtime> {
|
||||
PrecompilesValue::get()
|
||||
}
|
||||
|
||||
pub(crate) fn balance(account: impl Into<AccountId>) -> Balance {
|
||||
Balances::free_balance(account.into())
|
||||
}
|
||||
554
operator/precompiles/datahaven-native-transfer/src/tests.rs
Normal file
554
operator/precompiles/datahaven-native-transfer/src/tests.rs
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
// Copyright 2025 DataHaven
|
||||
// This file is part of DataHaven.
|
||||
|
||||
// DataHaven is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// DataHaven is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with DataHaven. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Comprehensive test suite for DataHaven Native Transfer precompile
|
||||
|
||||
use crate::mock::{
|
||||
balance, precompiles, Alice, Bob, EthereumSovereign, ExistentialDeposit, ExtBuilder,
|
||||
FeeRecipient, NativeTransferPrecompile, PCall,
|
||||
};
|
||||
use precompile_utils::prelude::Address;
|
||||
use precompile_utils::testing::*;
|
||||
use sp_core::{H160, U256};
|
||||
|
||||
// Test helper to get the precompile address
|
||||
fn precompile_address() -> H160 {
|
||||
NativeTransferPrecompile.into()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Selector Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_selectors() {
|
||||
// Just verify that selectors are generated - actual values may vary
|
||||
assert!(!PCall::transfer_to_ethereum_selectors().is_empty());
|
||||
assert!(!PCall::total_locked_balance_selectors().is_empty());
|
||||
assert!(!PCall::ethereum_sovereign_account_selectors().is_empty());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Modifier Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_function_modifiers() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(Alice.into(), 1000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let mut tester =
|
||||
PrecompilesModifierTester::new(precompiles(), Alice, precompile_address());
|
||||
|
||||
// transferToEthereum - non-view, non-payable
|
||||
tester.test_default_modifier(PCall::transfer_to_ethereum_selectors());
|
||||
|
||||
// totalLockedBalance - view
|
||||
tester.test_view_modifier(PCall::total_locked_balance_selectors());
|
||||
|
||||
// ethereumSovereignAccount - view
|
||||
tester.test_view_modifier(PCall::ethereum_sovereign_account_selectors());
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Transfer To Ethereum Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_ethereum_success() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![
|
||||
(Alice.into(), 10000),
|
||||
(EthereumSovereign.into(), ExistentialDeposit::get()),
|
||||
])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::from(100);
|
||||
|
||||
let initial_balance = balance(Alice);
|
||||
let initial_sovereign_balance = balance(EthereumSovereign);
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_returns(());
|
||||
|
||||
// Verify balances changed correctly
|
||||
assert_eq!(
|
||||
balance(Alice),
|
||||
initial_balance - 1000 - 100 // amount + fee
|
||||
);
|
||||
|
||||
// Fee should go to fee recipient
|
||||
assert_eq!(balance(FeeRecipient), 100);
|
||||
|
||||
// Amount should be locked in sovereign account
|
||||
assert_eq!(balance(EthereumSovereign), initial_sovereign_balance + 1000);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_ethereum_zero_address() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(Alice.into(), 10000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::zero();
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::from(100);
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_reverts(|output| output == b"Recipient cannot be zero address");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_ethereum_zero_amount() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(Alice.into(), 10000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::zero();
|
||||
let fee = U256::from(100);
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_reverts(|output| output == b"Amount must be greater than zero");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_ethereum_zero_fee() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(Alice.into(), 10000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::zero();
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_reverts(|output| output == b"Fee must be greater than zero");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_ethereum_insufficient_balance() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(Alice.into(), 100)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::from(100);
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_reverts(|output| {
|
||||
// Pallet will return Token(NotExpendable) or similar balance error
|
||||
let output_str = from_utf8_lossy(output);
|
||||
output_str.contains("Token") || output_str.contains("InsufficientBalance")
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_ethereum_multiple_transfers() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![
|
||||
(Alice.into(), 10000),
|
||||
(Bob.into(), 10000),
|
||||
(EthereumSovereign.into(), ExistentialDeposit::get()),
|
||||
])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::from(100);
|
||||
|
||||
let initial_sovereign = balance(EthereumSovereign);
|
||||
|
||||
// Alice transfers
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_returns(());
|
||||
|
||||
// Bob transfers
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Bob,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_returns(());
|
||||
|
||||
// Verify sovereign account has both amounts locked
|
||||
assert_eq!(balance(EthereumSovereign), initial_sovereign + 2000);
|
||||
|
||||
// Verify fee recipient got both fees
|
||||
assert_eq!(balance(FeeRecipient), 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_ethereum_large_amount() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![
|
||||
(Alice.into(), u128::MAX / 2),
|
||||
(EthereumSovereign.into(), ExistentialDeposit::get()),
|
||||
])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(u128::MAX / 4);
|
||||
let fee = U256::from(1000);
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_returns(());
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View Function Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_total_locked_balance_zero() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
precompiles()
|
||||
.prepare_test(Alice, precompile_address(), PCall::total_locked_balance {})
|
||||
.execute_returns(U256::zero());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_locked_balance_with_existential_deposit() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(EthereumSovereign.into(), ExistentialDeposit::get())])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
precompiles()
|
||||
.prepare_test(Alice, precompile_address(), PCall::total_locked_balance {})
|
||||
.execute_returns(U256::from(ExistentialDeposit::get()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_locked_balance_after_transfer() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![
|
||||
(Alice.into(), 10000),
|
||||
(EthereumSovereign.into(), ExistentialDeposit::get()),
|
||||
])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::from(100);
|
||||
|
||||
// Transfer some tokens
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_returns(());
|
||||
|
||||
// Check locked balance
|
||||
precompiles()
|
||||
.prepare_test(Alice, precompile_address(), PCall::total_locked_balance {})
|
||||
.execute_returns(U256::from(ExistentialDeposit::get() + 1000));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_locked_balance_after_multiple_transfers() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![
|
||||
(Alice.into(), 10000),
|
||||
(Bob.into(), 10000),
|
||||
(EthereumSovereign.into(), ExistentialDeposit::get()),
|
||||
])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::from(100);
|
||||
|
||||
// Alice transfers
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_returns(());
|
||||
|
||||
// Bob transfers
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Bob,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_returns(());
|
||||
|
||||
// Check total locked balance
|
||||
precompiles()
|
||||
.prepare_test(Alice, precompile_address(), PCall::total_locked_balance {})
|
||||
.execute_returns(U256::from(ExistentialDeposit::get() + 2000));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ethereum_sovereign_account() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
let expected: H160 = EthereumSovereign.into();
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::ethereum_sovereign_account {},
|
||||
)
|
||||
.execute_returns(Address(expected));
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Gas Accounting Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_ethereum_gas_cost() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![
|
||||
(Alice.into(), 10000),
|
||||
(EthereumSovereign.into(), ExistentialDeposit::get()),
|
||||
])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::from(100);
|
||||
|
||||
// Just verify the call succeeds, don't check exact gas cost
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_some();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_functions_gas_costs() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(EthereumSovereign.into(), 1000)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// totalLockedBalance should have minimal gas cost
|
||||
precompiles()
|
||||
.prepare_test(Alice, precompile_address(), PCall::total_locked_balance {})
|
||||
.expect_cost(0) // TODO: Calculate actual expected cost
|
||||
.execute_some();
|
||||
|
||||
// ethereumSovereignAccount should have minimal gas cost
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::ethereum_sovereign_account {},
|
||||
)
|
||||
.expect_cost(0) // TODO: Calculate actual expected cost
|
||||
.execute_some();
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases and Error Handling Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_transfer_respects_existential_deposit() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![
|
||||
(Alice.into(), 1000),
|
||||
(EthereumSovereign.into(), ExistentialDeposit::get()),
|
||||
])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
// Try to transfer everything except existential deposit
|
||||
let amount = U256::from(1000 - ExistentialDeposit::get() - 100);
|
||||
let fee = U256::from(100);
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_returns(());
|
||||
|
||||
// Alice should have at least existential deposit left
|
||||
assert!(balance(Alice) >= ExistentialDeposit::get());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_u256_to_balance_overflow() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(Alice.into(), u128::MAX)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
// U256::MAX cannot fit in u128
|
||||
let amount = U256::MAX;
|
||||
let fee = U256::from(100);
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_reverts(|output| from_utf8_lossy(output).contains("Amount overflow"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_overflow() {
|
||||
ExtBuilder::default()
|
||||
.with_balances(vec![(Alice.into(), u128::MAX)])
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let recipient = H160::from_low_u64_be(0x1234);
|
||||
let amount = U256::from(1000);
|
||||
let fee = U256::MAX;
|
||||
|
||||
precompiles()
|
||||
.prepare_test(
|
||||
Alice,
|
||||
precompile_address(),
|
||||
PCall::transfer_to_ethereum {
|
||||
recipient: recipient.into(),
|
||||
amount,
|
||||
fee,
|
||||
},
|
||||
)
|
||||
.execute_reverts(|output| from_utf8_lossy(output).contains("Fee overflow"));
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to convert bytes to UTF-8 string for debugging
|
||||
fn from_utf8_lossy(bytes: &[u8]) -> String {
|
||||
String::from_utf8_lossy(bytes).to_string()
|
||||
}
|
||||
|
|
@ -131,6 +131,7 @@ pallet-evm-precompile-batch = { workspace = true }
|
|||
pallet-evm-precompile-call-permit = { workspace = true }
|
||||
pallet-evm-precompile-collective = { workspace = true }
|
||||
pallet-evm-precompile-conviction-voting = { workspace = true }
|
||||
pallet-evm-precompile-datahaven-native-transfer = { workspace = true }
|
||||
pallet-evm-precompile-identity = { workspace = true }
|
||||
pallet-evm-precompile-preimage = { workspace = true }
|
||||
pallet-evm-precompile-proxy = { workspace = true }
|
||||
|
|
@ -211,6 +212,7 @@ std = [
|
|||
"pallet-evm-precompile-preimage/std",
|
||||
"pallet-evm-precompile-collective/std",
|
||||
"pallet-evm-precompile-conviction-voting/std",
|
||||
"pallet-evm-precompile-datahaven-native-transfer/std",
|
||||
"pallet-evm-precompile-identity/std",
|
||||
"pallet-evm-precompile-proxy/std",
|
||||
"pallet-evm-precompile-referenda/std",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing};
|
|||
use pallet_evm_precompile_call_permit::CallPermitPrecompile;
|
||||
use pallet_evm_precompile_collective::CollectivePrecompile;
|
||||
use pallet_evm_precompile_conviction_voting::ConvictionVotingPrecompile;
|
||||
use pallet_evm_precompile_datahaven_native_transfer::DataHavenNativeTransferPrecompile;
|
||||
use pallet_evm_precompile_file_system::FileSystemPrecompile;
|
||||
use pallet_evm_precompile_identity::IdentityPrecompile;
|
||||
use pallet_evm_precompile_modexp::Modexp;
|
||||
|
|
@ -141,6 +142,11 @@ type DataHavenPrecompilesAt<R> = (
|
|||
IdentityPrecompile<R, MaxAdditionalFields>,
|
||||
(CallableByContract, CallableByPrecompile),
|
||||
>,
|
||||
PrecompileAt<
|
||||
AddressU64<2073>,
|
||||
DataHavenNativeTransferPrecompile<R>,
|
||||
(CallableByContract, CallableByPrecompile),
|
||||
>,
|
||||
PrecompileAt<AddressU64<1028>, FileSystemPrecompile<R>>,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ pallet-evm-precompile-batch = { workspace = true }
|
|||
pallet-evm-precompile-call-permit = { workspace = true }
|
||||
pallet-evm-precompile-collective = { workspace = true }
|
||||
pallet-evm-precompile-conviction-voting = { workspace = true }
|
||||
pallet-evm-precompile-datahaven-native-transfer = { workspace = true }
|
||||
pallet-evm-precompile-identity = { workspace = true }
|
||||
pallet-evm-precompile-preimage = { workspace = true }
|
||||
pallet-evm-precompile-proxy = { workspace = true }
|
||||
|
|
@ -211,6 +212,7 @@ std = [
|
|||
"pallet-evm-precompile-preimage/std",
|
||||
"pallet-evm-precompile-collective/std",
|
||||
"pallet-evm-precompile-conviction-voting/std",
|
||||
"pallet-evm-precompile-datahaven-native-transfer/std",
|
||||
"pallet-evm-precompile-identity/std",
|
||||
"pallet-evm-precompile-proxy/std",
|
||||
"pallet-evm-precompile-referenda/std",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing};
|
|||
use pallet_evm_precompile_call_permit::CallPermitPrecompile;
|
||||
use pallet_evm_precompile_collective::CollectivePrecompile;
|
||||
use pallet_evm_precompile_conviction_voting::ConvictionVotingPrecompile;
|
||||
use pallet_evm_precompile_datahaven_native_transfer::DataHavenNativeTransferPrecompile;
|
||||
use pallet_evm_precompile_file_system::FileSystemPrecompile;
|
||||
use pallet_evm_precompile_identity::IdentityPrecompile;
|
||||
use pallet_evm_precompile_modexp::Modexp;
|
||||
|
|
@ -141,6 +142,11 @@ type DataHavenPrecompilesAt<R> = (
|
|||
IdentityPrecompile<R, MaxAdditionalFields>,
|
||||
(CallableByContract, CallableByPrecompile),
|
||||
>,
|
||||
PrecompileAt<
|
||||
AddressU64<2073>,
|
||||
DataHavenNativeTransferPrecompile<R>,
|
||||
(CallableByContract, CallableByPrecompile),
|
||||
>,
|
||||
PrecompileAt<AddressU64<1028>, FileSystemPrecompile<R>>,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ pallet-evm-precompile-balances-erc20 = { workspace = true }
|
|||
pallet-evm-precompile-batch = { workspace = true }
|
||||
pallet-evm-precompile-call-permit = { workspace = true }
|
||||
pallet-evm-precompile-collective = { workspace = true }
|
||||
pallet-evm-precompile-datahaven-native-transfer = { workspace = true }
|
||||
pallet-evm-precompile-identity = { workspace = true }
|
||||
pallet-evm-precompile-preimage = { workspace = true }
|
||||
pallet-evm-precompile-proxy = { workspace = true }
|
||||
|
|
@ -211,6 +212,7 @@ std = [
|
|||
"pallet-evm-precompile-preimage/std",
|
||||
"pallet-evm-precompile-collective/std",
|
||||
"pallet-evm-precompile-conviction-voting/std",
|
||||
"pallet-evm-precompile-datahaven-native-transfer/std",
|
||||
"pallet-evm-precompile-identity/std",
|
||||
"pallet-evm-precompile-proxy/std",
|
||||
"pallet-evm-precompile-referenda/std",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing};
|
|||
use pallet_evm_precompile_call_permit::CallPermitPrecompile;
|
||||
use pallet_evm_precompile_collective::CollectivePrecompile;
|
||||
use pallet_evm_precompile_conviction_voting::ConvictionVotingPrecompile;
|
||||
use pallet_evm_precompile_datahaven_native_transfer::DataHavenNativeTransferPrecompile;
|
||||
use pallet_evm_precompile_file_system::FileSystemPrecompile;
|
||||
use pallet_evm_precompile_identity::IdentityPrecompile;
|
||||
use pallet_evm_precompile_modexp::Modexp;
|
||||
|
|
@ -141,6 +142,11 @@ type DataHavenPrecompilesAt<R> = (
|
|||
IdentityPrecompile<R, MaxAdditionalFields>,
|
||||
(CallableByContract, CallableByPrecompile),
|
||||
>,
|
||||
PrecompileAt<
|
||||
AddressU64<2073>,
|
||||
DataHavenNativeTransferPrecompile<R>,
|
||||
(CallableByContract, CallableByPrecompile),
|
||||
>,
|
||||
PrecompileAt<AddressU64<1028>, FileSystemPrecompile<R>>,
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue