DeFi Operations Tax Accounting System Development
DeFi transactions are the most complex part of crypto tax accounting. Uniswap V3 concentrated liquidity, Aave flash loans, Curve stablecoin swaps, Compound cTokens, Yearn vault deposits — each protocol has unique semantics that need to be decoded and classified.
Decoding DeFi Transactions
On-chain Protocol Identification
const KNOWN_PROTOCOLS: Record<string, ProtocolInfo> = {
"0xE592427A0AEce92De3Edee1F18E0157C05861564": { name: "Uniswap V3 Router", type: "DEX" },
"0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45": { name: "Uniswap V3 Router 2", type: "DEX" },
"0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F": { name: "SushiSwap Router", type: "DEX" },
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D": { name: "Uniswap V2 Router", type: "DEX" },
"0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2": { name: "Aave V3 Pool", type: "LENDING" },
"0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B": { name: "Compound Comptroller", type: "LENDING" },
"0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7": { name: "Curve 3pool", type: "STABLE_SWAP" },
"0xBA12222222228d8Ba445958a75a0704d566BF2C8": { name: "Balancer Vault", type: "DEX" },
};
async function identifyDeFiProtocol(tx: BlockchainTransaction): Promise<ProtocolInfo | null> {
return KNOWN_PROTOCOLS[tx.to?.toLowerCase()] ?? null;
}
Decoding by Protocol Type
class DeFiTransactionDecoder {
async decode(tx: BlockchainTransaction): Promise<TaxableEvent[]> {
const protocol = await identifyDeFiProtocol(tx);
if (!protocol) {
// Unknown protocol — analyze by ERC-20 Transfer events
return this.decodeByTransferEvents(tx);
}
switch (protocol.type) {
case "DEX":
return this.decodeDEXSwap(tx, protocol);
case "LENDING":
return this.decodeLendingOperation(tx, protocol);
case "STABLE_SWAP":
return this.decodeStableSwap(tx, protocol);
case "YIELD":
return this.decodeYieldVault(tx, protocol);
}
}
private async decodeDEXSwap(tx: BlockchainTransaction, protocol: ProtocolInfo): Promise<TaxableEvent[]> {
// Parse Swap event from logs
const swapLogs = tx.logs.filter(log =>
log.topics[0] === UNISWAP_V3_SWAP_TOPIC || log.topics[0] === UNISWAP_V2_SWAP_TOPIC
);
const events: TaxableEvent[] = [];
for (const swapLog of swapLogs) {
const [tokenIn, tokenOut, amountIn, amountOut] = await this.parseSwapLog(swapLog);
const priceIn = await this.priceService.getHistoricalPrice(tokenIn, tx.timestamp);
const priceOut = await this.priceService.getHistoricalPrice(tokenOut, tx.timestamp);
events.push({
type: TaxEventType.SWAP,
timestamp: tx.timestamp,
assetIn: tokenIn,
amountIn,
valueInUSD: amountIn * priceIn,
assetOut: tokenOut,
amountOut,
valueOutUSD: amountOut * priceOut,
protocol: protocol.name,
txHash: tx.hash,
});
}
return events;
}
private async decodeLendingOperation(tx: BlockchainTransaction, protocol: ProtocolInfo): Promise<TaxableEvent[]> {
const events: TaxableEvent[] = [];
// Aave Supply — not taxable event (collateral)
const supplyLog = tx.logs.find(l => l.topics[0] === AAVE_SUPPLY_TOPIC);
if (supplyLog) {
return [{ type: TaxEventType.COLLATERAL_DEPOSIT, ...parseAaveSupply(supplyLog) }];
}
// Aave Withdraw — return of collateral
const withdrawLog = tx.logs.find(l => l.topics[0] === AAVE_WITHDRAW_TOPIC);
if (withdrawLog) {
const { asset, amount } = parseAaveWithdraw(withdrawLog);
// Difference between withdrawn amount and deposited amount = interest earned
const originalDeposit = await this.db.getAaveDeposit(tx.from, asset);
const interest = amount - originalDeposit.amount;
if (interest > 0) {
events.push({
type: TaxEventType.LENDING_INTEREST,
asset,
amount: interest,
valueUSD: interest * await this.priceService.getHistoricalPrice(asset, tx.timestamp),
});
}
events.push({ type: TaxEventType.COLLATERAL_RETURN, asset, amount: originalDeposit.amount });
return events;
}
return [];
}
}
Uniswap V3 LP Positions
Uniswap V3 concentrated liquidity creates special complexity: position is defined by tick range, fees accumulate separately, and price range in/out-of-range affects composition.
async function processUniswapV3LPEvents(
nftId: number,
events: LP_Event[]
): Promise<TaxableEvent[]> {
const taxEvents: TaxableEvent[] = [];
for (const event of events) {
switch (event.type) {
case "MINT": {
// Creating a position — controversial, depends on jurisdiction
// In US: not taxable on deposit, taxable on withdrawal (disposal)
// LP token (NFT) gets cost basis = value of both tokens at deposit
taxEvents.push({
type: TaxEventType.LP_MINT,
token0: event.token0, amount0: event.amount0,
token1: event.token1, amount1: event.amount1,
totalValueUSD: await getPositionValue(event),
nftId,
});
break;
}
case "COLLECT_FEES": {
// Collecting accumulated fees — income event
const feeValueUSD = await getFeesValue(event, event.timestamp);
taxEvents.push({
type: TaxEventType.LIQUIDITY_FEES,
token0: event.token0, fee0: event.amount0Collected,
token1: event.token1, fee1: event.amount1Collected,
valueUSD: feeValueUSD,
timestamp: event.timestamp,
});
break;
}
case "BURN": {
// Withdrawing liquidity — position realization
const originalCostBasis = await db.getLPCostBasis(nftId);
const currentValue = await getPositionValue(event);
taxEvents.push({
type: TaxEventType.LP_BURN,
gainLossUSD: currentValue - originalCostBasis,
isLongTerm: isLongTerm(event.mintTimestamp, event.timestamp),
});
break;
}
}
}
return taxEvents;
}
Yearn and Yield Vault Accounting
async function processYearnVaultOperations(tx: BlockchainTransaction): Promise<TaxableEvent[]> {
// Deposit: ETH → yETH (shares)
// Not taxable on deposit — like buying a share
// Withdrawal: yETH → ETH (more than deposited due to yield)
// On withdrawal: disposal of yETH shares, receipt of ETH
// Gain = current ETH value - original ETH cost basis
const withdrawLog = tx.logs.find(l => l.address === YEARN_VAULT_ADDRESS && l.topics[0] === WITHDRAW_TOPIC);
if (withdrawLog) {
const { shares, assets } = parseYearnWithdraw(withdrawLog);
const costBasis = await db.getYearnSharesCostBasis(tx.from, YEARN_VAULT_ADDRESS, shares);
const currentValue = assets * await priceService.getHistoricalPrice("ETH", tx.timestamp);
return [{
type: TaxEventType.DISPOSAL,
assetSold: "yETH",
amountSold: shares,
proceeds: currentValue,
costBasis: costBasis,
gainLoss: currentValue - costBasis,
}];
}
return [];
}
Supported Protocols
| Protocol | Operations | Complexity |
|---|---|---|
| Uniswap V2/V3 | Swap, LP add/remove, fee collect | High |
| Aave V2/V3 | Supply, Borrow, Repay, Withdraw | Medium |
| Compound | cToken mint/redeem, interest | Medium |
| Curve | Swap, add/remove liquidity | Medium |
| Yearn | Vault deposit/withdraw | Medium |
| Lido | stETH staking rewards | High (rebasing) |
| Convex | CRV staking, reward claiming | High |
Stack
| Component | Technology |
|---|---|
| Blockchain data | The Graph + Moralis + Alchemy |
| ABI decoding | ethers.js / viem |
| Price history | CoinGecko + Chainlink historical |
| Storage | PostgreSQL + TimescaleDB |
| Processing | BullMQ queues |
Complete DeFi tax accounting system supporting 10+ protocols: 2-3 months development.







