ERC-3643 is an Ethereum token standard for regulated, permissioned issuance — it layers eligibility checks (KYC/AML), transfer restrictions, and forced-transfer/freeze capabilities on top of a fungible-token contract.
Key Characteristics
- Eligibility Verification: Transfers are only allowed between holders who have completed KYC/AML checks.
- Regulatory Compliance: Whitelists/blacklists, investor caps, and jurisdictional rules are enforced at the protocol level.
- Enforcement & Control: Unauthorized transfers are blocked, and an issuer can forcibly transfer or burn tokens to satisfy real-world regulatory obligations.
The EmpoorioChain Equivalent: pallet-emp-rwa
EmpoorioChain's compliance suite includes pallet-emp-rwa, a native pallet purpose-built for permissioned, real-world-asset-style tokens. It sits alongside pallet-emp-assets rather than requiring per-token compliance contracts — eligibility, freezing, and forced-transfer logic are enforced by the runtime itself for any asset registered under it.
Key capabilities (verify exact extrinsic names against the current pallet reference here)
- Holder Approval: A compliance authority approves or revokes eligible holders on-chain, gating transfers at the runtime level.
- Account Freezing: A compliance authority can freeze a specific holder, blocking further transfers without touching the asset's core logic.
- Forced Transfer / Recovery: The compliance authority can execute a forced transfer to satisfy legal orders or recover tokens from a compromised or fraudulent account.
- Auditability: Every compliance action (approval, freeze, forced transfer) is an on-chain extrinsic, giving auditors a native transaction history instead of relying on off-chain records.
How Does This Compare to a Custom Solidity Implementation?
On Ethereum, ERC-3643-style compliance is usually built as bespoke Solidity — a token contract with its own KYC mappings, a freeze mapping, and a privileged compliance-authority role, all written and audited per project.
On EmpoorioChain, the equivalent logic already exists as a shared, once-audited pallet. A team doesn't write and deploy compliance logic per token — it registers an asset under pallet-emp-rwa and calls the pallet's existing extrinsics to manage eligible holders.
pragma solidity ^0.8.28;
interface IEmpAssets {
function transfer(address to, address mintAddress, uint256 amount) external;
function getTokenAccount(address owner, address token) external view returns (uint256 balance, bool isFrozen);
function mintTokens(address to, address mintAddress, uint256 amount) external;
}
contract PermissionedAsset {
IEmpAssets public immutable empAssets;
address public immutable mintAddress;
mapping(address => bool) public isKYCApproved;
mapping(address => bool) public frozen;
address public complianceAuthority;
address public transferHookProgram;
event KYCApproved(address indexed user, bool status);
event AccountFrozen(address indexed user, bool status);
event TransferHookSet(address indexed hookProgram);
event ForcedTransfer(address indexed from, address indexed to, uint256 value);
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(address _empAssets, address _mint, address authority) {
empAssets = IEmpAssets(_empAssets);
mintAddress = _mint;
complianceAuthority = authority;
}
modifier onlyComplianceAuth() {
require(msg.sender == complianceAuthority, "not compliance auth");
_;
}
function approveKYC(address user, bool approved) external onlyComplianceAuth {
isKYCApproved[user] = approved;
emit KYCApproved(user, approved);
}
function freezeAccount(address user, bool freeze) external onlyComplianceAuth {
frozen[user] = freeze;
emit AccountFrozen(user, freeze);
}
function setTransferHook(address hookProgram) external onlyComplianceAuth {
transferHookProgram = hookProgram;
emit TransferHookSet(hookProgram);
}
function setComplianceAuthority(address newAuthority) external onlyComplianceAuth {
complianceAuthority = newAuthority;
}
function transfer(address to, uint256 amount) external {
require(!frozen[msg.sender] && !frozen[to], "account frozen");
require(isKYCApproved[msg.sender] && isKYCApproved[to], "KYC required");
if (transferHookProgram != address(0)) {
bool ok = ITransferHook(transferHookProgram).onTransfer(msg.sender, to, amount);
require(ok, "blocked by hook");
}
empAssets.transfer(to, mintAddress, amount);
emit Transfer(msg.sender, to, amount);
}
function forceTransfer(address from, address to, uint256 amount) external onlyComplianceAuth {
// temporarily unfreeze to bypass EmpAssets.transfer require(msg.sender == owner)
frozen[from] = false;
empAssets.transfer(to, mintAddress, amount);
frozen[from] = true;
emit ForcedTransfer(from, to, amount);
}
}
interface ITransferHook {
function onTransfer(address from, address to, uint256 amount) external returns (bool);
}
Holder Eligibility
Only accounts explicitly approved by the compliance authority can send or receive the asset — enforced by the pallet before a transfer is applied, the same guarantee ERC-3643's eligibility check provides on Ethereum.
Freeze Enforcement
A compliance authority can freeze an individual holder's ability to transfer at the pallet level — no custom contract logic required, the same outcome as ERC-3643's account-freeze feature.
Enforcement Point
Where ERC-3643 implementations typically run a transfer-hook callback for each transfer, pallet-emp-rwa performs the equivalent eligibility/freeze checks natively inside the pallet's transfer extrinsic, before the transfer is applied. In practice that means:
- Ineligible or frozen accounts are rejected before state changes
- No separate hook contract to deploy or wire up per asset
- Compliance logic is part of the audited pallet, not custom per-project code
This gives the same protocol-level enforcement ERC-3643's transfer hooks provide, without a separate hook contract per token.
Forced Transfer (Override Authority)
The compliance authority can execute a forced transfer between any two eligible accounts — the EmpoorioChain equivalent of ERC-3643's permanent-delegate / forced-transfer capability, used for legal orders or fraud recovery.
How to Use pallet-emp-rwa
1. Conceptual Map
| ERC-3643 Feature | pallet-emp-rwa Equivalent |
|---|---|
| KYC whitelist | Approved-holder storage, gated by the compliance authority |
| Account freeze | Native freeze extrinsic |
| Force / claw-back transfer | Native forced-transfer extrinsic |
| On-transfer custom logic | Built into the pallet's transfer extrinsic, not a separate hook |
| Compliance admin | A designated AccountId32 (or governance-controlled origin) |
2. End-to-End Flow (simplified)
User → pallet-emp-assets (transfer) ↘
pallet-emp-rwa eligibility check → OK / ERR
↘
pallet-emp-assets (balance change applied)
A transfer of a pallet-emp-rwa-registered asset is a single extrinsic; the runtime checks both accounts' eligibility and freeze status as part of applying it. If either check fails, the whole extrinsic is rejected and no state changes — mirroring ERC-3643's on-chain enforcement, without a separate hook call.
3. Minimal Client Example
Client code for compliance actions is a normal @polkadot/api call: sign and submit an approve-holder or freeze-holder extrinsic from the compliance-authority account, and submit ordinary transfers from holder accounts. There is no separate program to write, build, or deploy for the compliance logic itself.
// EmpoorioChain does not need a separate "transfer hook" contract for this —
// pallet-emp-rwa (part of the emp-compliance-suite) enforces eligibility and
// freeze rules natively at the runtime level, before a transfer is applied.
// The client side simply calls its extrinsics via @polkadot/api; there is no
// program to write, build, or deploy for the compliance logic itself.
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 complianceAdmin = keyring.addFromUri("//compliance-admin-seed-here");
const assetId = 1;
// Whitelist a holder after off-chain KYC/AML has cleared them.
async function approveHolder(accountId) {
const hash = await api.tx.empRwa
.approveHolder(assetId, accountId)
.signAndSend(complianceAdmin);
return hash.toString();
}
// Freeze/unfreeze an account — enforced natively, no custom logic required.
async function freezeHolder(accountId) {
const hash = await api.tx.empRwa
.freezeHolder(assetId, accountId)
.signAndSend(complianceAdmin);
return hash.toString();
}
4. emp-cli Quickstart (illustrative)
A typical flow: register a new permissioned asset under pallet-emp-rwa as the compliance admin, approve each holder after off-chain KYC/AML clears them, then transfers between approved holders succeed automatically while transfers involving a non-approved or frozen account are rejected by the runtime itself. Exact command names should be checked against the current emp-cli docs.
# emp-cli quickstart (illustrative — see EmpoorioChain CLI docs for the
# exact subcommands and flags, which evolve alongside the pallet).
# 1. Register a new permissioned asset under pallet-emp-rwa
emp-cli assets create --admin $COMPLIANCE_ADMIN
# 2. Approve holders after off-chain KYC/AML
emp-cli emp-rwa approve-holder --asset-id 1 --account $HOLDER
# 3. Transfers between approved holders succeed automatically;
# transfers involving a non-approved or frozen account are rejected
# by the runtime itself, not by client-side validation.
emp-cli assets transfer --asset-id 1 --to $HOLDER --amount 10


