Розробка системи крипто-зарплат
Ключова проблема крипто-пейролу — це не технологія, а операційна складність: різні сотрудники хочуть різні токени в різних мережах, податкове законодавство вимагає фіксації фіатного еквіваленту в момент виплати, compliance вимагає KYC, а фінансовий директор хочет планові видатки у стабільному виразі. Система повинна вирішити все це одночасно — і робити це автоматично.
Самостійна розробка має сенс коли: команда > 20 чоловік, є нестандартні вимоги до токенів/мереж, потрібна інтеграція з HR-системою або бухгалтерським обліком, або готові рішення (Request Finance, Superfluid, Deel Crypto) не закривають вашу специфіку.
Архітектура системи
HR System / Manual Input
↓
Payroll Engine (розрахунок, конвертація)
↓
Approval Workflow (multi-sig авторизація)
↓
Disbursement Module (батчові виплати)
↓
Accounting Module (податковий облік, звіти)
Модель даних
interface Employee {
id: string
legalName: string
taxId?: string // для звітності
walletAddresses: {
chain: string
address: string
verified: boolean // підтверджений через sign message
}[]
paymentPreferences: PaymentPreference[]
kycStatus: 'pending' | 'approved' | 'rejected'
}
interface PaymentPreference {
percentage: number // % від зарплати у цьому токені
token: string // contract address
chain: string
minAmount?: Decimal // мінімум для виплати (інакше накопичувати)
}
interface PayrollRun {
id: string
periodStart: Date
periodEnd: Date
paymentDate: Date
currency: 'USD' | 'EUR' | string // base currency для розрахунку
employees: PayrollEntry[]
status: 'draft' | 'approved' | 'processing' | 'completed' | 'failed'
approvals: Approval[]
}
interface PayrollEntry {
employeeId: string
grossAmountFiat: Decimal // у base currency
deductions: Deduction[]
netAmountFiat: Decimal
payments: CryptoPayment[] // розбивка по токенам/мережам
exchangeRates: ExchangeRateSnapshot[] // зафіксовані курси
}
Розрахунок виплат та конвертація курсів
Найболючіше питання — який курс використовувати. Три варіанти:
Spot rate у момент виплати — найпростіший. Роботодавець бере курс у момент відправки транзакції. Волатильність несе сотрудник.
Фіксований курс за N днів до виплати — знижує волатильність, але вимагає попередньої планування та може розходитися з ринком у момент виплати.
TWA (Time-Weighted Average) за розрахунковий період — стандарт у традиційних FX-розрахунках, найсправедливіший, але складніше пояснити сотрудникам.
class ExchangeRateService {
// Джерела курсів з fallback
private sources = [
new ChainlinkPriceFeed(), // on-chain, manipulation-resistant
new CoinGeckoAPI(), // CoinGecko з API key
new BinanceAPI(), // CEX spot price
]
async getRate(
fromCurrency: string,
toCurrency: string,
method: 'spot' | 'twap_7d' | 'twap_30d' = 'spot'
): Promise<{ rate: Decimal; source: string; timestamp: Date }> {
if (method === 'spot') {
for (const source of this.sources) {
try {
const rate = await source.getSpotRate(fromCurrency, toCurrency)
return { rate, source: source.name, timestamp: new Date() }
} catch (e) {
console.warn(`${source.name} failed:`, e)
}
}
throw new Error(`Cannot get spot rate for ${fromCurrency}/${toCurrency}`)
}
// TWAP: усереднення по історичним цінам
const days = method === 'twap_7d' ? 7 : 30
const historicalRates = await this.getHistoricalRates(fromCurrency, toCurrency, days)
const avgRate = historicalRates.reduce((sum, r) => sum.plus(r), new Decimal(0))
.div(historicalRates.length)
return { rate: avgRate, source: 'twap', timestamp: new Date() }
}
async snapshotForPayroll(run: PayrollRun): Promise<ExchangeRateSnapshot[]> {
// Зафіксувати всі потрібні курси та зберегти у БД для аудиту
const tokens = new Set(
run.employees.flatMap(e => e.payments.map(p => p.token))
)
const snapshots = await Promise.all(
[...tokens].map(async (token) => {
const rate = await this.getRate(run.currency, token, 'spot')
return { token, ...rate, payrollRunId: run.id }
})
)
await this.db.insertRateSnapshots(snapshots)
return snapshots
}
}
Батчові виплати: оптимізація газу
На Ethereum mainnet виплата кожному сотруднику окремою транзакцією — дорого. Батчові виплати через мультисенд контракт знижують вартість у 3-5 разів:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract PayrollDispatcher is Ownable {
event PaymentDispatched(
bytes32 indexed payrollRunId,
address indexed recipient,
address indexed token,
uint256 amount
);
struct Payment {
address recipient;
address token; // address(0) для native ETH/BNB
uint256 amount;
}
function dispatchPayroll(
bytes32 payrollRunId,
Payment[] calldata payments
) external onlyOwner {
for (uint256 i = 0; i < payments.length; i++) {
Payment calldata p = payments[i];
if (p.token == address(0)) {
(bool success,) = p.recipient.call{value: p.amount}("");
require(success, "ETH transfer failed");
} else {
require(
IERC20(p.token).transferFrom(msg.sender, p.recipient, p.amount),
"Token transfer failed"
);
}
emit PaymentDispatched(payrollRunId, p.recipient, p.token, p.amount);
}
}
receive() external payable {}
}
Для стейблкоінів (USDC, USDT) використовуємо transferFrom — джерело коштів залишається на multisig гаманці, контракт тільки спрямовує виплати. Це важливо для безпеки: контракт не зберігає коштів.
Мультичейн disbursement
Якщо сотрудники отримують зарплату у різних мережах — потрібен окремий disbursement модуль для кожної мережі. Паралельне виконання з агрегацією статусів:
class MultiChainDisbursementService {
private dispatchers: Map<string, ChainDispatcher>
async executePayrollRun(run: PayrollRun): Promise<DisbursementResult> {
// Групуємо платежі по цепочкам
const byChain = groupBy(
run.employees.flatMap(e => e.payments),
p => p.chain
)
// Паралельно запускаємо disbursement у кожній цепочці
const results = await Promise.allSettled(
Object.entries(byChain).map(([chain, payments]) =>
this.dispatchers.get(chain)!.dispatch(run.id, payments)
)
)
// Обробляємо часткові сбої
const failures = results.filter(r => r.status === 'rejected')
if (failures.length > 0) {
await this.handlePartialFailure(run.id, failures)
}
return this.aggregateResults(results)
}
}
Multi-sig авторизація
Для фондів з AML-вимогами недостатньо однієї підписи. Стандартна схема: CFO + CEO + фінансовий директор, 2-of-3.
Gnosis Safe (тепер Safe{Wallet}) — стандарт для мультисиг операцій. Інтеграція через Safe SDK:
import Safe, { EthersAdapter } from '@safe-global/protocol-kit'
import SafeApiKit from '@safe-global/api-kit'
class PayrollApprovalService {
async proposePayrollTransaction(
safeAddress: string,
payrollData: PayrollRun,
payments: BatchPayment[]
): Promise<string> {
const safeSDK = await Safe.create({ ethAdapter, safeAddress })
const apiKit = new SafeApiKit({ txServiceUrl: 'https://safe-transaction-mainnet.safe.global' })
// Кодуємо виклик PayrollDispatcher.dispatchPayroll()
const data = payrollDispatcher.interface.encodeFunctionData(
'dispatchPayroll',
[payrollData.id, payments]
)
const safeTransaction = await safeSDK.createTransaction({
transactions: [{ to: PAYROLL_DISPATCHER_ADDRESS, data, value: '0' }]
})
const safeTxHash = await safeSDK.getTransactionHash(safeTransaction)
const senderSignature = await safeSDK.signTransactionHash(safeTxHash)
await apiKit.proposeTransaction({
safeAddress,
safeTransactionData: safeTransaction.data,
safeTxHash,
senderAddress: await signer.getAddress(),
senderSignature: senderSignature.data,
})
return safeTxHash
}
}
Податковий облік та compliance
Кожна виплата повинна бути зафіксована з:
- Фіатним еквівалентом у момент виплати (для податків)
- Джерелом курсу (для аудиту)
- ID транзакції on-chain
- Період за який виплачено
CREATE TABLE payroll_transactions (
id BIGSERIAL PRIMARY KEY,
payroll_run_id UUID NOT NULL REFERENCES payroll_runs(id),
employee_id UUID NOT NULL REFERENCES employees(id),
payment_date DATE NOT NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
-- Крипто деталі
token_address VARCHAR(42) NOT NULL,
token_symbol VARCHAR(20) NOT NULL,
chain VARCHAR(50) NOT NULL,
crypto_amount NUMERIC(36, 18) NOT NULL,
tx_hash VARCHAR(66),
-- Фіатний еквівалент для податків
fiat_currency VARCHAR(3) NOT NULL,
fiat_amount NUMERIC(20, 2) NOT NULL,
exchange_rate NUMERIC(20, 8) NOT NULL,
rate_source VARCHAR(100) NOT NULL,
rate_timestamp TIMESTAMPTZ NOT NULL,
-- Статус
status VARCHAR(20) NOT NULL DEFAULT 'pending',
confirmed_at TIMESTAMPTZ,
block_number BIGINT
);
Експорт для бухгалтерії: CSV з розбивкою по сотрудникам та періодам, сумісний з 1C або міжнародними стандартами (IAS 19 для employee benefits).
Streaming payments: Superfluid інтеграція
Для DAOs та компаній з реальним-таймовим потоком зарплати — інтеграція з Superfluid Protocol. Замість періодичних виплат — неперервний потік токенів по секунді:
import { Framework } from '@superfluid-finance/sdk-core'
async function createSalaryStream(
employeeAddress: string,
tokenAddress: string,
monthlyAmountWei: bigint
): Promise<void> {
const sf = await Framework.create({ chainId: 137, provider })
const superToken = await sf.loadSuperToken(tokenAddress)
// flowRate = wei/second
const flowRate = monthlyAmountWei / BigInt(30 * 24 * 3600)
const createFlowOp = superToken.createFlow({
sender: companyAddress,
receiver: employeeAddress,
flowRate: flowRate.toString(),
})
await createFlowOp.exec(signer)
}
Streaming payments знімають проблему періодичності та знижують операційну нагрузку, але вимагають обеспечення ліквідності (sufficient buffer) та ускладнюють tax accounting — дохід нараховується неперервно.
Повна система з мультичейн поддержкою, Safe інтеграцією, податковим модулем та HR-інтеграцією — від 3 до 6 тижнів розробки залежно від числа поддержуваних мереж та вимог до compliance.







