What is ERC-4626 on EmpoorioChain?


ERC-4626 is a Tokenized Vault Standard: a canonical interface for vaults that accept a single underlying ERC-20 asset and issue share tokens representing proportional ownership, with a standard deposit/mint/withdraw/redeem lifecycle.

Key Characteristics

  • Single-asset custody: The vault holds one underlying asset; all accounting is expressed in that asset.
  • Share accounting: Total assets and total shares stay in a predictable, convertible ratio.
  • Composable interface: DEXes, aggregators, and front-ends can integrate any compliant vault without custom adapters.
  • Strategy-agnostic: The yield strategy (lending, staking, market-making) lives behind the same standard facade.

The EmpoorioChain Equivalent: pallet-yield-vault

What does a vault look like on EmpoorioChain?

pallet-yield-vault is a native runtime pallet: it holds the underlying pallet-emp-assets balance and tracks share issuance directly in its own storage. There's no separate vault contract to write, audit, and deploy per strategy — a vault is registered as an entry in the pallet, the same way an asset is registered in pallet-emp-assets.

latex
Program  ──┐
           └─> handles instructions
Accounts ───┘  (carry all mutable state)

Deployment feel: rather than deploying a new ERC-4626 contract per strategy the way you'd deploy a new Uniswap-v2-style pool contract, you register a new vault ID against the shared pallet-yield-vault — the pallet itself never changes, only the configuration.

Accounting Model

Because pallet-yield-vault is a native pallet, it can read and move balances in pallet-emp-assets directly through an internal pallet-to-pallet call, rather than an external cross-contract call with its own gas overhead. Share balances are tracked the same way asset balances are — as entries keyed by AccountId32 in the vault pallet's own storage.

Vault Custody

On Ethereum, a vault contract custodies the underlying asset itself, controlled by its own contract logic. On EmpoorioChain, pallet-yield-vault custodies the underlying pallet-emp-assets balance directly in its runtime-controlled storage — there's no separate derived custody address or private key involved, the pallet's logic is the only thing authorized to move it.

Pallet-to-Pallet Calls

Where an ERC-4626 vault on Ethereum makes an external call to the underlying ERC-20 contract (e.g. `IERC20.transferFrom`) to move assets, pallet-yield-vault calls pallet-emp-assets directly within the runtime. The runtime enforces the same kind of atomicity Ethereum gets from a single transaction — either the whole extrinsic succeeds or none of it does.

Deposit/Withdraw Flow

Deposit

  • Holder calls the vault pallet's deposit extrinsic with an asset amount.
  • The pallet moves the underlying asset in and mints proportional shares to the depositor, in a single atomic extrinsic.

Withdraw / Redeem

  • Reverse order: shares are burned, then the corresponding underlying asset amount is transferred back out.

State update happens inside the pallet's own storage (total assets, total shares) as part of the same extrinsic.

Because the whole operation is a single extrinsic against known pallet storage, wallets and front-ends can simulate the resulting share/asset amounts before submitting.

How Does This Compare to a Custom Solidity Vault?

A hand-written ERC-4626 vault on Ethereum tracks total assets, total shares, and per-holder share balances in its own contract storage, converting between the two via `totalAssets()`/`totalSupply()` ratios on every deposit and redeem.

pallet-yield-vault performs the same conversion logic, but as shared, once-audited pallet code rather than a bespoke contract per vault. Deposits pull the underlying asset in via a pallet-to-pallet call and mint proportional shares; redemptions burn shares and pay out the underlying asset in the same atomic extrinsic.

Because vault logic isn't redeployed per project, there's no separate reentrancy surface to audit per vault the way there is with independently written ERC-4626 contracts — the shared pallet's guarantees apply uniformly to every registered vault.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IEmpAssets {
    function mintTokens(address to, address mintAddr, uint256 amount) external;
    function transfer(address to, address mintAddr, uint256 amount) external;
    function getMint(address mintAddr) external view returns (uint8, uint256, address, address, address);
    function getTokenAccount(address owner, address mintAddr) external view returns (address, address, uint256, bool);
}

contract EmpYieldVault {
    IEmpAssets  public immutable empAssets;
    address public immutable mintAddr;
    uint8   public immutable assetDecimals;
    uint256 public totalShareSupply;

    mapping(address => uint256) public shareBalance;
    mapping(address => mapping(address => uint256)) public shareAllowance;

    bool private locked;

    event Approval(address indexed owner, address indexed spender, uint256 value);
    event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
    event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);

    modifier nonReentrant() {
        require(!locked, "REENTRANCY");
        locked = true;
        _;
        locked = false;
    }

    constructor(IEmpAssets _empAssets, address _mintAddr) {
        empAssets = _empAssets;
        mintAddr = _mintAddr;
        (uint8 dec,, , ,) = _empAssets.getMint(_mintAddr);
        assetDecimals = dec;
    }

    function totalAssets() public view returns (uint256 assets) {
        (, , assets, ) = empAssets.getTokenAccount(address(this), mintAddr);
    }

    function convertToShares(uint256 assets) public view returns (uint256) {
        return totalShareSupply == 0 ? assets : (assets * totalShareSupply) / totalAssets();
    }

    function convertToAssets(uint256 shares) public view returns (uint256) {
        return totalShareSupply == 0 ? shares : (shares * totalAssets()) / totalShareSupply;
    }

    function _mint(address to, uint256 amount) internal {
        totalShareSupply += amount;
        shareBalance[to] += amount;
    }

    function _burn(address from, uint256 amount) internal {
        shareBalance[from] -= amount;
        totalShareSupply -= amount;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        shareAllowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function deposit(uint256 assets, address receiver) external nonReentrant returns (uint256 shares) {
        require(assets > 0, "zero assets");
        empAssets.transfer(address(this), mintAddr, assets);
        shares = convertToShares(assets);
        _mint(receiver, shares);
        emit Deposit(msg.sender, receiver, assets, shares);
    }

    function redeem(uint256 shares, address receiver, address owner) external nonReentrant returns (uint256 assets) {
        require(shares > 0, "zero shares");
        if (msg.sender != owner) {
            uint256 allowed = shareAllowance[owner][msg.sender];
            require(allowed >= shares, "allowance too low");
            if (allowed != type(uint256).max) {
                shareAllowance[owner][msg.sender] = allowed - shares;
            }
        }
        assets = convertToAssets(shares);
        _burn(owner, shares);
        empAssets.transfer(receiver, mintAddr, assets);
        emit Withdraw(msg.sender, receiver, owner, assets, shares);
    }
}

How to Use pallet-yield-vault

1. Conceptual map

PieceOn-chain ObjectPurpose
Underlying AssetExisting pallet-emp-assets asset IDThe ERC-20-equivalent asset being deposited (e.g. DUSD)
Share Accountingpallet-yield-vault storageTracks each depositor's proportional ownership of the vault
Vault CustodyPallet-controlled asset balanceHolds the underlying assets on behalf of all depositors
Vault Statepallet-yield-vault storage entryStores share_minttotal assets, total sharespda_bump, and any configured fees
Vault AdminAccountId32 or governance originAuthority over vault parameters (where configurable)

One extrinsic always moves exactly one underlying asset amount and, if necessary, mints/burns the exact proportional number of shares in the same atomic call.

2. End-to-end flow (simplified)

1) deposit (assets -> shares)

2) client (wallet or dApp) builds the extrinsic

  • Client calls the vault pallet's deposit extrinsic with (vaultId, amount)
  • Signs and submits via @polkadot/api

3) pallet-yield-vault

  • Pallet transfers the underlying asset from depositor to vault custody
  • Computes shares = deposit_amount * total_shares / total_assets
  • Mints shares to the depositor in the vault pallet's own storage
  • Updates vault_state.total_assets
  • Emits a Deposited event

4) wallet preview: because the vault's state is known on-chain, a client can estimate -X assets & +Y shares before signing

5) redeem/withdraw (shares -> assets) is the same sequence in reverse

3. What You Actually Write

Unlike Ethereum, where launching a new vault means writing and auditing a new ERC-4626 contract, using pallet-yield-vault on EmpoorioChain is a client-side integration: register a vault ID (or use an existing one), then call deposit/redeem extrinsics via @polkadot/api. There is no Rust program to write, build, or deploy per vault — the share-accounting logic (first-depositor 1:1 rate, subsequent deposits at supply*assets/totalAssets) lives once inside the audited pallet.

javascript
// EmpoorioChain implements the deposit/redeem lifecycle natively via
// pallet_yield_vault (index 77) — there is no separate on-chain "vault
// program" for an app team to write, build, and deploy. The pallet holds
// the underlying pallet_emp_assets balance and tracks share issuance in
// its own storage; client code just calls its extrinsics.
import { ApiPromise, WsProvider, Keyring } from "@polkadot/api";

const api = await ApiPromise.create({
  provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
const keyring = new Keyring({ type: "sr25519" });
const depositor = keyring.addFromUri("//your-seed-here");

const vaultId = 1;

async function deposit(assets) {
  const hash = await api.tx.yieldVault
    .deposit(vaultId, assets)
    .signAndSend(depositor);
  return hash.toString();
}

async function redeem(shares) {
  const hash = await api.tx.yieldVault
    .redeem(vaultId, shares)
    .signAndSend(depositor);
  return hash.toString();
}

EVM TO EMPOORIOCHAIN

Start building on EmpoorioChain

Intro to EmpoorioChain Development

Read

EmpoorioChain Pallet Reference

Read

emp-cli Quickstart

Read

More EmpoorioChain Developer Tools

Read

Node storing all history and participating in consensus

  • Ethereum: Archive Node
  • EmpoorioChain: Full node with archive pruning enabled

Node producing/finalizing blocks

  • Ethereum: Full Node (execution + consensus client)
  • EmpoorioChain: Validator node (Aura block production + GRANDPA finality, single Rust binary)

Node serving RPC without validating

  • Ethereum: Light Node
  • EmpoorioChain: RPC node (non-validator, full state)
What is ERC-4626 on EmpoorioChain? | empoorio