Система обліку стейкинг-нагород для податків
Стейкинг-нагороди — одне з найспірніших питань крипто-оподаткування. У США IRS (Rev. Rul. 2023-14) офіційно підтвердив: стейкинг-нагороди — звичайний дохід при отриманні за справедливою ринковою вартістю. У Німеччині — інша трактовка. Система повинна враховувати юрисдикційні відмінності.
Типи стейкингу та їхня податкова трактовка
const STAKING_TAX_TREATMENT: Record<string, StakingTaxRule> = {
NATIVE_STAKING: {
// ETH 2.0, SOL, ADA — прямий стейкинг в мережі
US: { type: "ordinary_income", timing: "on_receipt" },
DE: { type: "other_income", timing: "on_receipt", annualExempt: 256 },
UK: { type: "miscellaneous_income", timing: "on_receipt" },
},
LIQUID_STAKING: {
// stETH, rETH, mSOL — отримуєш liquid токен
// Спірно: чи є отримання stETH opodatkovia?
US: { type: "unknown", note: "IRS не висловився явно" },
DE: { type: "non_taxable", note: "своп розглядається як продовження" },
},
REBASING: {
// stETH rebasing — баланс збільшується, не окремі транзакції нагород
US: { type: "ordinary_income", timing: "on_receipt", challenge: "hard_to_track" },
},
VALIDATOR_REWARDS: {
// Оператори ETH validator — у деяких трактуваннях business income
US: { type: "business_income_or_ordinary_income" },
},
};
Відстеження стейкинг-нагород
class StakingRewardTracker {
// Ethereum стейкинг через Lido
async trackLidoRewards(walletAddress: string, since: Date): Promise<StakingReward[]> {
// stETH використовує rebasing — баланс оновлюється щодня
// Потрібні snapshots на кожний rebase
const rebaseEvents = await this.getLidoRebaseEvents(since);
const rewards: StakingReward[] = [];
let previousBalance = await this.getStETHBalance(walletAddress, since);
for (const rebase of rebaseEvents) {
const newBalance = await this.getStETHBalance(walletAddress, rebase.timestamp);
const rewardAmount = newBalance - previousBalance;
if (rewardAmount > 0) {
const ethPrice = await this.priceService.getHistoricalPrice("stETH", rebase.timestamp);
rewards.push({
timestamp: rebase.timestamp,
protocol: "Lido",
asset: "stETH",
amount: rewardAmount,
usdValue: rewardAmount * ethPrice,
rewardType: "REBASING",
costBasis: rewardAmount * ethPrice, // cost basis = FMV при отриманні
});
}
previousBalance = newBalance;
}
return rewards;
}
// Ethereum 2.0 validator нагороди
async trackETH2ValidatorRewards(validatorIndex: number, since: Date): Promise<StakingReward[]> {
const beaconChainData = await fetch(
`https://beaconcha.in/api/v1/validator/${validatorIndex}/incomedetail?limit=100`
).then(r => r.json());
return beaconChainData.data
.filter((r: any) => new Date(r.epoch_timestamp) >= since)
.map(async (r: any) => {
const timestamp = new Date(r.epoch_timestamp);
const ethPrice = await this.priceService.getHistoricalPrice("ETH", timestamp);
const rewardETH = r.income.attestation_source_reward / 1e9; // gwei → ETH
return {
timestamp,
protocol: "Ethereum 2.0 Validator",
asset: "ETH",
amount: rewardETH,
usdValue: rewardETH * ethPrice,
validatorIndex,
epoch: r.epoch,
};
});
}
// Solana стейкинг нагороди
async trackSolanaRewards(walletAddress: string, since: Date): Promise<StakingReward[]> {
// Використовуємо Solana RPC getInflationReward
const connection = new Connection(SOLANA_RPC);
const rewardHistory = await connection.getInflationReward(
[walletAddress],
{ epoch: await this.getEpochSince(since) }
);
return rewardHistory.map(r => ({
timestamp: epochToTimestamp(r.epoch),
protocol: "Solana Staking",
asset: "SOL",
amount: r.amount / 1e9, // lamports → SOL
usdValue: (r.amount / 1e9) * solPriceAtEpoch,
}));
}
}
Зведена звітність по стейкингу
async function generateStakingTaxReport(
userId: string,
taxYear: number,
jurisdiction: string
): Promise<StakingTaxReport> {
const rewards = await db.getStakingRewards(userId, taxYear);
// Групуємо за протоколом та місяцем
const byProtocol = groupBy(rewards, r => r.protocol);
const monthly = groupByMonth(rewards);
const totalIncome = rewards.reduce((sum, r) => sum + r.usdValue, 0);
// Для Німеччини: перевіряємо Freigrenze €256
const taxableAmount = jurisdiction === "DE"
? Math.max(0, totalIncome - 256)
: totalIncome;
return {
taxYear,
jurisdiction,
totalRewardsUSD: totalIncome,
taxableAmountUSD: taxableAmount,
byProtocol: Object.entries(byProtocol).map(([protocol, rewards]) => ({
protocol,
totalRewards: rewards.reduce((sum, r) => sum + r.amount, 0),
totalUSD: rewards.reduce((sum, r) => sum + r.usdValue, 0),
})),
monthlyBreakdown: monthly,
rewards: rewards.map(r => ({
...r,
costBasisForFutureDisposal: r.usdValue, // cost basis = значення доходу
})),
};
}
Особливість: cost basis при майбутньому продажу
Важливий момент: стейкинг-нагорода отримана та включена в звичайний дохід створює cost basis для майбутнього продажу. Система повинна створити tax lot:
// При обліку стейкинг-нагороди — створюємо lot
await db.createTaxLot({
userId,
asset: reward.asset,
amount: reward.amount,
costPerUnitUSD: reward.usdValue / reward.amount,
totalCostUSD: reward.usdValue,
acquiredAt: reward.timestamp,
source: "staking_reward",
acquisitionType: "INCOME", // розрізняємо від звичайної покупки
});
Система обліку стейкинг-нагород з підтримкою Lido, ETH2 validator, Solana, Cosmos — 3-4 тижні розробки.







