Veyla Protocol Docs
Complete technical documentation for Veyla Protocol — automated yield accounting and XCM routing infrastructure built natively on Polkadot Hub using PVM smart contracts and Polkadot-native cross-chain messaging.
Architecture
Veyla Protocol is built entirely on Polkadot Hub (Passet Hub Testnet). The frontend talks to a single deployed Solidity contract via standard EVM JSON-RPC, while the contract uses Polkadot-native precompiles for cross-chain operations.
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Next.js 16 + React 19 | User interface & blockchain interaction |
| Blockchain SDK | wagmi v3 + viem v2 | EVM wallet connection & contract calls |
| State/Cache | TanStack Query v5 | Data fetching, caching, invalidation |
| Smart Contract | Solidity 0.8.28 (PolkaVM) | Core vault logic + XCM routing |
| Network | Polkadot Hub Passet Hub Testnet | Chain ID: 420420417 |
| XCM Routing | XCM Precompile 0x...000a0000 | Cross-chain asset movement |
| USDT | pallet-assets ERC-20 Precompile | ERC-20 interface for Asset ID 1984 |
XCM Integration
XCM (Cross-Consensus Messaging) is Polkadot's native language for cross-chain communication. Veyla uses two XCM precompile functions to route assets between parachains — without any bridge, relayer, or wrapped token.
// XCM Precompile address on Polkadot Hub
address constant XCM_PRECOMPILE = 0x00000000000000000000000000000000000a0000;
interface IXcm {
struct Weight { uint64 refTime; uint64 proofSize; }
// Execute an XCM message locally (same chain context)
function execute(bytes calldata message, Weight calldata weight) external;
// Send an XCM message to another parachain or chain
function send(bytes calldata destination, bytes calldata message) external;
// Estimate execution weight for a given XCM message
function weighMessage(bytes calldata message) external view returns (Weight memory);
}| Function | Direction | Used For |
|---|---|---|
execute(message, weight) | Local (same chain) | Execute XCM within Polkadot Hub context — local asset management |
send(dest, message) | Cross-chain | Send XCM to Hydration, Moonbeam, Astar, Bifrost for yield deployment |
weighMessage(message) | Read-only | Estimate weight before calling execute — prevents out-of-weight errors |
Route Destinations
| Chain | Protocol | Asset | APY | Status |
|---|---|---|---|---|
| Hydration | Omnipool | DOT | 14.2% | ACTIVE |
| Moonbeam | Stellaswap | USDT | 9.8% | STANDBY |
| Astar | ArthSwap | DOT/USDT | 8.1% | STANDBY |
| Bifrost | Bifrost LP | DOT | 7.4% | PAUSED |
setApy() and currently reflect real parachain yields manually curated. Phase 2 will introduce keeper-based automation: off-chain agents monitor live APY feeds and call routeAssets() permissionlessly when better routes are detected.Smart Contract
VeylaVault.sol is deployed on Passet Hub Testnet and verified on Blockscout. It handles deposits, withdrawals, yield accounting, and XCM routing — all in one contract.
| Field | Value |
|---|---|
| Contract | VeylaVault.sol |
| Network | Passet Hub Testnet (Chain ID: 420420417) |
| Address | 0xc66ee6f7CA593fbbccEd23d8c50417C058F1EF77 |
| Compiler | Solidity 0.8.28 (PolkaVM) |
| Optimizer | 200 runs |
| Blockscout | View on Blockscout |
Supported Assets
| Asset | Type | Address / Sentinel | Decimals | Notes |
|---|---|---|---|---|
| DOT | Native (PAS on testnet) | address(0) | 18 (PolkaVM) | Deposited via msg.value, no ERC-20 precompile needed |
| USDT | pallet-assets ERC-20 precompile | 0x000007c0...01200000 | 6 | Asset ID 1984 on Polkadot Hub |
Yield Accounting
Veyla uses a snapshot-based yield accumulation pattern to ensure accuracy across multiple deposits.
// Yield formula (per-block accrual):
// pending = principal × apyBps × elapsed / (365 days × 10_000)
function earned(address user, address token) external view returns (uint256) {
return _accruedYield[user][token] + _pendingYield(user, token);
}
// _accrueYield() is called before every deposit/withdraw to snapshot pending yield
// This prevents dilution when new deposits are made mid-period
function _accrueYield(address user, address token) internal {
_accruedYield[user][token] += _pendingYield(user, token);
_depositTimestamps[user][token] = block.timestamp;
}Test Coverage
Technical Deep Dive
Architecture decisions, security patterns, and what makes this project uniquely possible on Polkadot.
Precompile Architecture
PolkaVM is Polkadot's smart contract VM that compiles Solidity to PolkaVM bytecode (not EVM bytecode). This gives Solidity contracts native access to Substrate runtime functions via precompiles — special addresses that map directly to Rust runtime logic. No bridges, no oracles, no middleware.
// XCM Precompile — Polkadot Hub native (0x...000a0000)
interface IXcm {
struct Weight { uint64 refTime; uint64 proofSize; }
// Execute XCM locally with caller's origin
function execute(bytes calldata message, Weight calldata weight) external;
// Send XCM to another parachain
function send(bytes calldata destination, bytes calldata message) external;
// Estimate weight for execution
function weighMessage(bytes calldata message) external view returns (Weight memory);
}
// pallet-assets ERC-20 Precompile — native USDT (Asset ID 1984)
interface IERC20Precompile {
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
}IXcm(0x...000a0000).execute(), PolkaVM routes the call directly to Polkadot's Rust XCM pallet. There is no EVM compatibility layer, no translation overhead. The precompile IS the runtime function. This is fundamentally different from bridges or cross-chain messaging on other ecosystems.Security Architecture
VeylaVault implements multiple layers of safety by design:
| Pattern | Implementation | Purpose |
|---|---|---|
| 2-Step Ownership | transferOwnership() → acceptOwnership() | Prevents accidental lockout from address typos |
| 2-Step Treasury | proposeTreasury() → acceptTreasury() | Prevents fees routing to wrong address |
| APY Cap | MAX_APY_BPS = 10,000 (100%) | Prevents catastrophic drain attack |
| Fee Cap | MAX_PROTOCOL_FEE_BPS = 500 (5%) | Limits owner fee extraction |
| XCM Size Cap | MAX_XCM_MESSAGE_SIZE = 1024 | Prevents calldata griefing |
| Destination Whitelist | trustedDestinations[keccak256(dest)] | XCM only routes to approved parachains |
| CEI Pattern | State updates before external calls | Prevents reentrancy on withdrawals |
| Principal Protection | Yield capped at available pool | Users always withdraw at least their principal |
| Pause Mechanism | notPaused modifier | Emergency stop for all deposits/withdrawals |
Yield Accounting Deep Dive
The yield formula uses a snapshot-based accumulation pattern. Before every balance-changing operation (deposit, withdraw), _accrueYield() snapshots all pending yield into storage. This ensures multi-deposit accuracy — depositing twice doesn't dilute the yield from the first deposit.
Yield Formula: pending = principal × APY_bps × elapsed_seconds / (365 days × 10,000) Example (DOT @ 14.20% APY): 1 DOT deposited → after 30 days: pending = 1e18 × 1420 × 2,592,000 / (31,536,000 × 10,000) pending = 0.01167 DOT ≈ 0.0117 DOT Withdrawal Payout: totalPayout = principal + earnedYield - protocolFee protocolFee = earnedYield × protocolFeeBps / 10,000 (default 0.5%) Safety Cap: actualYield = min(earnedYield, yieldPoolBalance) → Principal is NEVER locked, even if yield pool is empty → Unclaimed yield persists in _accruedYield for future claimYield() calls
Why Only on Polkadot
Veyla's architecture is fundamentally dependent on four Polkadot-specific primitives that do not exist in any other ecosystem:
Cross-chain messages are processed by the relay chain validators — no external bridges, relayers, or trust assumptions. Assets move natively between parachains with finality guaranteed by Polkadot's shared security.
Solidity contracts on PolkaVM can call Substrate runtime functions directly. IXcm.execute() is not a wrapper — it's the actual XCM executor. No other VM gives Solidity this level of runtime access.
Every parachain (Hydration, Moonbeam, Astar) is secured by the same Polkadot relay chain validators. When Veyla routes assets cross-chain, the security guarantee is identical to a local transaction.
USDT on Polkadot Hub is a pallet-assets native asset (ID 1984), exposed to Solidity via an auto-generated ERC-20 precompile. No wrapped tokens, no canonical bridge — it's the real asset from Polkadot's runtime.
Frontend
A full-featured React application built with Next.js 16 App Router, deployed on Vercel. All blockchain interactions use wagmi v3 + viem v2 connected to Passet Hub Testnet.
| Page / Route | Description |
|---|---|
/ | Landing page — Navbar, Hero, ProofBar, ProblemTeaser, HowItWorks, UnderTheHood, CTA, Footer |
/app | Dashboard — stat cards, positions table, route visualization, activity feed |
/app/vault | Vault — deposit & withdraw with live APY, balance, USD values |
/app/routes | Routes — animated hub-and-spoke XCM route map + APY comparison table |
/app/history | History — full on-chain transaction log with filter tabs & Blockscout links |
/docs | This page — comprehensive protocol documentation |
Blockchain Hooks
| Hook | What it does |
|---|---|
useDeposit() | Full deposit lifecycle — approve (USDT only) → deposit → wait confirmation → invalidate cache |
useWithdraw() | Withdraw lifecycle — write → wait → invalidate |
useUserPositions() | Single multicall: all user positions (balance, earned, APY) for DOT + USDT |
useVaultHistory() | Fetch Deposited/Withdrawn events, decode logs, fetch timestamps, localStorage cache |
useTokenPrices() | CoinGecko DOT + USDT price feed with 60s refetch interval |
useTokenApys() | Multicall currentApy(DOT) + currentApy(USDT) from contract |
useERC20Balance() | Read ERC-20 balanceOf for any token + address |
Transaction State Machine
DOT deposit:
idle → awaiting-signature → pending → success
▲ (MetaMask: approve sending PAS/DOT native)
USDT deposit:
idle → awaiting-approval → approving → awaiting-signature → pending → success
▲ (approve TX) ▲ (deposit TX)
Withdrawal (DOT or USDT):
idle → awaiting-signature → pending → successQuick Start
Get Veyla running locally in under 5 minutes. You need Node.js 18+, a browser wallet (MetaMask or Rabby), and testnet PAS tokens from the Paseo faucet.
git clone https://github.com/veyla-alliance/veyla cd veyla/frontend
npm install
.env.local in the frontend/ folder:NEXT_PUBLIC_CHAIN_ID=420420417 NEXT_PUBLIC_RPC_URL=https://eth-rpc-testnet.polkadot.io NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://blockscout-testnet.polkadot.io NEXT_PUBLIC_VAULT_ADDRESS=0xc66ee6f7CA593fbbccEd23d8c50417C058F1EF77 NEXT_PUBLIC_DOT_TOKEN_ADDRESS=0x0000000000000000000000000000000000000000 NEXT_PUBLIC_USDT_TOKEN_ADDRESS=0x000007c000000000000000000000000001200000
npm run dev # → http://localhost:3000
localhost:3000/app, connect MetaMask or Rabby, then click Switch Network. Veyla will automatically add Passet Hub with the correct PAS nativeCurrency config.contract/, set your PRIVATE_KEY in contract/.env, then run forge script script/Deploy.s.sol --rpc-url passet_hub --broadcast --verify. Update NEXT_PUBLIC_VAULT_ADDRESS with your deployed address.Contract API Reference
Complete function reference for VeylaVault.sol.
Write Functions
// Deposit DOT (native) or USDT into the vault. // For DOT: send msg.value, set token = address(0), amount is ignored. // For USDT: approve vault first, set token = USDT precompile address, amount = wei. function deposit(address token, uint256 amount) external payable; // Withdraw deposited principal + available yield back to caller. // Principal is always returnable even if yield pool is empty. function withdraw(address token, uint256 amount) external; // Harvest accrued yield without closing the position. function claimYield(address token) external; // [Owner only] Execute an XCM message locally via precompile. function routeAssets(address token, bytes calldata xcmMessage) external; // [Owner only] Send XCM cross-chain to target parachain. function sendCrossChain(address token, bytes calldata destination, bytes calldata xcmMessage) external; // [Owner only] Update APY for an asset (basis points, capped at 10_000 = 100%). function setApy(address token, uint256 apyBps) external; // [Owner only] Pause or unpause deposits and withdrawals. function setPaused(bool _paused) external; // [Owner only] Seed the yield pool (called by owner or via XCM receive()). function fundYieldPool() external payable; // [Owner only] Step 1 of 2-step ownership transfer — nominate new owner. function transferOwnership(address newOwner) external; // [Pending owner only] Step 2 — accept and finalise ownership transfer. function acceptOwnership() external; // [Owner only] Step 1 of 2-step treasury (fee recipient) transfer. function proposeTreasury(address newTreasury) external; // [Pending treasury only] Step 2 — accept and activate new treasury address. function acceptTreasury() external; // [Owner only] Whitelist a cross-chain XCM destination (raw bytes encoding). function addTrustedDestination(bytes calldata destination) external; // [Owner only] Remove a previously whitelisted XCM destination. function removeTrustedDestination(bytes calldata destination) external; // [Owner only] Update protocol fee (basis points, capped at MAX_FEE_BPS). function setProtocolFee(uint256 feeBps) external; // [Owner only] Update the minimum rebalance interval in seconds. function setRebalanceInterval(uint256 interval) external; // [Owner only] Update the human-readable route label for a token. function setTokenRoute(address token, string calldata route) external;
Read Functions
// User's deposited principal for a given token. function balanceOf(address user, address token) external view returns (uint256); // User's total earned yield (snapshotted + pending since last deposit). function earned(address user, address token) external view returns (uint256); // Current APY for a token, in basis points (divide by 100 for %). function currentApy(address token) external view returns (uint256); // Total combined TVL across all assets (raw units). function tvl() external view returns (uint256); // TVL for a specific token. function tvlOf(address token) external view returns (uint256); // Timestamp of the user's last deposit for a given token. function depositTimestampOf(address user, address token) external view returns (uint256); // Human-readable route label for a token (e.g. "Hydration"). function tokenRoute(address token) external view returns (string memory); // Public state address public owner; address public pendingOwner; address public treasury; address public pendingTreasury; bool public paused;
Events
event Deposited(address indexed user, address indexed token, uint256 amount); event Withdrawn(address indexed user, address indexed token, uint256 amount); event YieldClaimed(address indexed user, address indexed token, uint256 amount); event RoutedLocally(address indexed token, uint256 amount); event RoutedCrossChain(address indexed token, bytes destination, uint256 amount); event YieldPoolFunded(address indexed from, uint256 amount); event ApyUpdated(address indexed token, uint256 newApyBps); event Paused(bool isPaused); event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); event TreasuryProposed(address indexed newTreasury); event TreasuryUpdated(address indexed newTreasury); event TrustedDestinationAdded(bytes destination); event TrustedDestinationRemoved(bytes destination); event ProtocolFeeUpdated(uint256 newFeeBps); event RebalanceIntervalUpdated(uint256 newInterval); event TokenRouteUpdated(address indexed token, string route);
Custom Errors
| Error | Triggered when |
|---|---|
ZeroAmount() | Deposit or withdraw with amount = 0 |
InsufficientBalance() | Withdraw more than deposited |
NotOwner() | Non-owner calls admin function |
TransferFailed() | Native DOT transfer or ERC-20 transfer returns false |
UnsupportedToken() | Token address is neither DOT sentinel nor USDT precompile |
ContractPaused() | Deposit or withdraw while paused = true |
MsgValueMismatch() | USDT deposit sent with msg.value > 0 |
ApyExceedsCap() | setApy() called with value above MAX_APY_BPS (10,000 = 100%) |
ZeroAddress() | transferOwnership() called with address(0) |
NoPendingOwner() | acceptOwnership() called when no transfer is pending |
UntrustedDestination() | sendCrossChain() called with a non-whitelisted destination |
XcmMessageTooLarge() | XCM message exceeds MAX_XCM_MESSAGE_SIZE (1024 bytes) |
NotPendingTreasury() | acceptTreasury() called by non-pending treasury address |
FeeExceedsCap() | setProtocolFee() called with value above the fee cap |
InvalidInterval() | setRebalanceInterval() called with zero interval |
RouteTooLong() | setTokenRoute() called with a route string exceeding max length |
YieldPoolEmpty() | claimYield() called but yield pool has insufficient funds |
Roadmap
Below is what is currently live today for Veyla Protocol and our plans for the future.
- VeylaVault.sol deployed & verified on Passet Hub Testnet
- Full frontend: Dashboard, Vault, Routes, History pages
- DOT (native) + USDT (pallet-assets precompile) deposits & withdrawals
- XCM precompile integration (execute + send)
- Automated yield accounting (snapshot pattern)
- 82/82 tests passing (4 fuzz tests, 256 runs each)
- Security audit completed — Critical, High, Medium, Low all fixed
- Live deployment on Vercel
- On-chain APY oracle that reads from Hydration + Moonbeam
- Keeper bot (off-chain) triggers routeAssets when APY delta > threshold
- Real routing to Hydration Omnipool via XCM send()
- Multi-asset positions tracking with real USD price feeds
- Add Astar (ArthSwap) and Bifrost LP as routing destinations
- Governance token + fee distribution to stakers
- Strategy marketplace (user selects risk profile)
- Mainnet deployment (DOT mainnet, real USDT)
- Audit by reputable security firm
- Support for additional Polkadot native assets (USDC, HDX, GLMR)
- Cross-parachain yield aggregation dashboard
- Mobile app with push notifications for yield events
- Partnership with Hydration, Moonbeam, Astar liquidity programs
FAQ
Common questions about Veyla Protocol.
What are the risks of using Veyla?
Why does MetaMask show PAS instead of DOT?
Is there a withdrawal fee?
How does yield get generated?
How often does rebalancing happen?
How are APY rates determined?
Do you use bridges?
Is the contract audited?
Which wallets are supported?
Can I see the source code?
Team
Veyla Protocol is built by Veyla Alliance for the Polkadot Solidity Hackathon 2026.
Full-stack developer & protocol designer. Builder of Veyla Protocol — on-chain yield accounting & XCM routing on Polkadot Hub.
5HBz4tNpNnbpwnYjERnEjebTiDeEgeRjMytapvFr7V8sKchY