Разработка системы крипто-зарплат (crypto payroll)
Ключевая проблема крипто-пейрола — это не технология, а операционная сложность: разные сотрудники хотят разные токены в разных сетях, налоговое законодательство требует фиксации рублёвого (или фиатного) эквивалента на момент выплаты, 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 дней до выплаты — снижает волатильность, но требует advance planning и может расходиться с рынком на момент выплаты.
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
Каждая выплата должна быть зафиксирована с:
- Фиатным эквивалентом на момент выплаты (для НДФЛ/income tax)
- Источником курса (для аудита)
- Идентификатором транзакции 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.







