Розробка системи сегрегації активів для кастодіана
Кастодіани — компанії, які професійно зберігають крипто-активи клієнтів. Ключова вимога регуляторів й клієнтів: активи різних клієнтів повинні бути строго розділені. "Спільний пул" неприйнятний: якщо кастодіан несе збитки або банкротується, клієнт повинен мати можливість однозначно ідентифікувати свої активи й отримати їх назад. Це й есть сегрегація.
Нормативний контекст
Вимоги до сегрегації активів визначаються юрисдикцією. MiCA (EU) вимагає відокремлення клієнтських активів від власних. FCA (UK) — аналогічно. SEC для крипто-кастодіанів у США — поки у розробці, але NYDFS BitLicense вже містить вимоги до сегрегації.
Ключова вимога: можливість аудиту. Аудитор у будь-який момент повинен мати можливість сопоставити on-chain адреси з записами про клієнтів й підтвердити, що активи клієнта A не перемішані з активами клієнта B.
Архітектурні моделі сегрегації
Один адрес на клієнта (Full Segregation)
Кожен клієнт отримує унікальну on-chain адресу (або набір адрес — по одній на кожен підтримуваний blockchain). Активи фізично розділені на рівні blockchain.
Переваги:
- Максимальна прозорість для клієнта й аудитора
- Простий доказ права власності
- Ізоляція ризиків: проблема одного клієнта не впливає на інших
Недоліки:
- Високі operational costs при великій кількості клієнтів (тисячі адрес, окремі gas поповнення для кожної)
- Складніше оптимізувати ліквідність
Віртуальна сегрегація зі спільним пулом
Активи зберігаються на спільних адресах, але внутрішня база даних веде точний облік долі кожного клієнта. Аналог: банківський рахунок (ви не знаєте в якому фізичному сховищі лежать ваші гроші).
Переваги:
- Низькі operational costs
- Простіше управляти ліквідністю
Недоліки:
- Нормативно складніше обґрунтувати як "сегрегацію"
- При банкротстві кастодіана — активи клієнтів можуть бути оспорені кредиторами
Гібридна модель
Для крупних клієнтів (інститути) — виділені адреси. Для рітейлу — віртуальна сегрегація з можливістю переводу на dedicated address за запитом.
Детальна архітектура системи
Управління адресами
Ключи зберігаються в HSM. Адреси генеруються детерміновано через HD-derivation:
m/44'/60'/{clientId}'/0/0 → основний адрес клієнта
m/44'/60'/{clientId}'/0/1 → адрес для конкретного активу
m/44'/60'/{clientId}'/1/0 → change адрес
Це дозволяє відновити все адреси з master seed без додаткового сховища.
interface ClientWallet {
clientId: string;
blockchain: string;
address: string;
derivationPath: string;
createdAt: Date;
keyStorageLocation: 'hsm_slot_1' | 'hsm_slot_2'; // для geo-redundancy
}
class AddressManager {
async createClientAddress(
clientId: string,
blockchain: string
): Promise<ClientWallet> {
// Атомарна операція: перевіряємо що адрес ще не створений
return this.db.transaction(async (trx) => {
const existing = await trx('client_wallets')
.where({ clientId, blockchain })
.first();
if (existing) return existing;
const index = await this.getNextIndex(trx, blockchain);
const derivationPath = `m/44'/60'/${clientId}'/0/${index}`;
const address = await this.hsm.deriveAddress(derivationPath);
return trx('client_wallets').insert({
clientId,
blockchain,
address,
derivationPath,
createdAt: new Date(),
}).returning('*');
});
}
}
Бухгалтерський облік (Ledger)
Двойна запис обов'язкова. Кожне рух активів повинен балансуватися:
CREATE TABLE ledger_entries (
id BIGSERIAL PRIMARY KEY,
entry_type VARCHAR(32) NOT NULL, -- debit/credit
client_id UUID NOT NULL REFERENCES clients(id),
asset VARCHAR(64) NOT NULL, -- ETH, USDC, BTC
blockchain VARCHAR(32) NOT NULL,
amount NUMERIC(36, 18) NOT NULL CHECK (amount > 0),
balance_after NUMERIC(36, 18) NOT NULL,
reference_type VARCHAR(32), -- deposit, withdrawal, fee, transfer
reference_id UUID,
tx_hash VARCHAR(66),
block_number BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
created_by VARCHAR(64) NOT NULL, -- system, user_id
CONSTRAINT no_negative_balance CHECK (balance_after >= 0)
);
CREATE TABLE client_balances (
client_id UUID REFERENCES clients(id),
asset VARCHAR(64),
blockchain VARCHAR(32),
balance NUMERIC(36, 18) NOT NULL DEFAULT 0,
last_updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (client_id, asset, blockchain)
);
Reconciliation. Щодня система повинна сверяти on-chain баланси з записами у ledger:
async function reconcile(blockchain: string, asset: string): Promise<ReconciliationResult> {
const clientWallets = await db.clientWallets.findAll({ blockchain });
const results = await Promise.all(
clientWallets.map(async (wallet) => {
const onChainBalance = await getOnChainBalance(wallet.address, asset);
const ledgerBalance = await db.clientBalances.getBalance(
wallet.clientId, asset, blockchain
);
const discrepancy = onChainBalance - ledgerBalance;
return {
clientId: wallet.clientId,
address: wallet.address,
onChainBalance,
ledgerBalance,
discrepancy,
status: Math.abs(discrepancy) < TOLERANCE ? 'ok' : 'mismatch',
};
})
);
const mismatches = results.filter(r => r.status === 'mismatch');
if (mismatches.length > 0) {
await this.alertService.sendAlert('RECONCILIATION_MISMATCH', mismatches);
}
return { results, mismatches, timestamp: new Date() };
}
Моніторинг вхідних депозитів
Система повинна відслідковувати все вхідні транзакції на адреси клієнтів й автоматично зараховувати:
class DepositMonitor {
async watchAddress(address: string, clientId: string) {
// Підписка на нові блоки через WebSocket
this.provider.on('block', async (blockNumber) => {
const block = await this.provider.getBlock(blockNumber, true);
for (const tx of block.transactions) {
if (tx.to?.toLowerCase() === address.toLowerCase()) {
await this.processDeposit({
clientId,
txHash: tx.hash,
amount: tx.value,
asset: 'ETH',
blockNumber,
});
}
}
});
// Також відслідковуємо ERC-20 Transfer події
this.monitorERC20Transfers(address, clientId);
}
private async processDeposit(deposit: DepositEvent) {
// Чекаємо confirmations (зазвичай 12-20 для finality)
await this.waitForConfirmations(deposit.txHash, REQUIRED_CONFIRMATIONS);
await this.db.transaction(async (trx) => {
// Перевіряємо ідемпотентність
const existing = await trx('deposits').where({ txHash: deposit.txHash }).first();
if (existing) return;
// Записуємо депозит
await trx('deposits').insert(deposit);
// Оновлюємо ledger
await this.ledger.credit(trx, deposit.clientId, deposit.asset, deposit.amount);
});
}
}
Вывід коштів (Withdrawal)
Вывід завжди вимагає approval workflow. Після approval:
- Перевірка балансу клієнта у ledger
- Резервування коштів (debit pending)
- Підпис транзакції у HSM
- Broadcast on-chain
- Очікування confirmations
- Фінальне списання з ledger (або reversal при failure)
Idempotency. Кожен withdrawal request має унікальний idempotency key. Повторний запит з тим же ключем повертає статус існуючого, не створює новий.
Proof of Reserves
Для публічного підтвердження платоспроможності — Merkle tree на основі client balances:
// Кожен клієнт отримує proof що його баланс включений в загальне дерево
// Без розкриття даних інших клієнтів
async function generateProofOfReserves(): Promise<ProofOfReserves> {
const balances = await db.getAllClientBalances();
// Будуємо Merkle tree
const leaves = balances.map(b =>
keccak256(encode(['address', 'uint256'], [b.address, b.balance]))
);
const tree = new MerkleTree(leaves, keccak256, { sort: true });
// Публікуємо root on-chain
await proofOfReservesContract.updateRoot(tree.getRoot());
return {
root: tree.getRoot(),
totalBalance: balances.reduce((sum, b) => sum + b.balance, 0n),
timestamp: Date.now(),
proofs: balances.map((b, i) => ({
clientId: b.clientId,
proof: tree.getProof(leaves[i]),
})),
};
}
Compliance й аудит
Audit trail. Все операції з неізмінною історією. Логи зберігаються в append-only сховищі (AWS QLDB або PostgreSQL з audit triggers).
Reporting. Щомісячні звіти для кожного клієнта: рух коштів, комісії, поточні баланси. Щоквартальні для аудиторів: reconciliation reports, proof of reserves.
KYT (Know Your Transaction). Інтеграція з Chainalysis, Elliptic або TRM Labs для перевірки вхідних транзакцій на зв'язок із санкційними адресами й mixer-ами.
Стек
| Компонент | Технологія |
|---|---|
| HSM | AWS CloudHSM або Thales |
| Database | PostgreSQL + AWS QLDB (audit log) |
| Blockchain monitoring | Alchemy/Infura + own indexer |
| Reconciliation | Cron job + alerting |
| KYT | Chainalysis Reactor API |
| API | Node.js + TypeScript, REST + gRPC |
| Frontend | React (admin dashboard) |
Сроки
- Core ledger + address management: 6-8 тижнів
- Deposit monitoring + withdrawal flow: 4-6 тижнів
- Reconciliation + proof of reserves: 3-4 тижні
- KYT integration + compliance reporting: 3-4 тижні
- Security audit: обов'язковий, +4-8 тижнів
Итого: 5-6 місяців до production-ready системи.







