Skip to content

Instantly share code, notes, and snippets.

@johngrantuk
Created April 29, 2025 08:47
Show Gist options
  • Select an option

  • Save johngrantuk/0701c41fcc0c2e5933d2c869672155ae to your computer and use it in GitHub Desktop.

Select an option

Save johngrantuk/0701c41fcc0c2e5933d2c869672155ae to your computer and use it in GitHub Desktop.
Confirm balance tracking works via events
// pnpm example ./examples/balanceTracking-28-04-25.ts
// Checks events for pool: https://balancer.fi/pools/base/v3/0xaf5b7999f491c42c05b5a2ca80f1d200d617cc8c
import { Address, createPublicClient, http, parseAbiItem, Log } from 'viem';
import { base } from 'viem/chains';
import fs from 'fs';
import path from 'path';
// Define event signatures
const swapEvent = parseAbiItem(
'event Swap(address indexed pool, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, uint256 swapFeePercentage, uint256 swapFeeAmount)',
);
const liquidityAddedEvent = parseAbiItem(
'event LiquidityAdded(address indexed pool, address indexed liquidityProvider, uint8 indexed kind, uint256 totalSupply, uint256[] amountsAddedRaw, uint256[] swapFeeAmountsRaw)',
);
const liquidityRemovedEvent = parseAbiItem(
'event LiquidityRemoved(address indexed pool, address indexed liquidityProvider, uint8 indexed kind, uint256 totalSupply, uint256[] amountsRemovedRaw, uint256[] swapFeeAmountsRaw)',
);
type PoolEvent =
| (Log<bigint, number, false, typeof swapEvent> & { type: 'Swap' })
| (Log<bigint, number, false, typeof liquidityAddedEvent> & {
type: 'LiquidityAdded';
})
| (Log<bigint, number, false, typeof liquidityRemovedEvent> & {
type: 'LiquidityRemoved';
});
// Constants for fee calculations
const FIXED_POINT_SCALE = 1_000_000_000_000_000_000n; // 1e18
const aggregateFee = 500_000_000_000_000_000n; // 0.5e18
function mulDown(a: bigint, b: bigint): bigint {
return (a * b) / FIXED_POINT_SCALE;
}
async function getPoolEventsInChunks(
contractAddress: Address,
poolAddress: Address,
startBlockNumber: number,
endBlockNumber: number,
) {
const publicClient = createPublicClient({
chain: base,
transport: http(
'RPC_URL',
),
});
const CHUNK_SIZE = 10000n;
let currentBlock = BigInt(startBlockNumber);
const allEvents: PoolEvent[] = [];
while (currentBlock <= BigInt(endBlockNumber)) {
const endBlock =
currentBlock + CHUNK_SIZE > BigInt(endBlockNumber)
? BigInt(endBlockNumber)
: currentBlock + CHUNK_SIZE;
console.log(
`Fetching events from blocks ${currentBlock} to ${endBlock}...`,
);
// Fetch Swap events
const swapEvents = await publicClient.getContractEvents({
address: contractAddress,
abi: [swapEvent],
fromBlock: currentBlock,
toBlock: endBlock,
args: {
pool: poolAddress,
},
});
// Fetch LiquidityAdded events
const liquidityAddedEvents = await publicClient.getContractEvents({
address: contractAddress,
abi: [liquidityAddedEvent],
fromBlock: currentBlock,
toBlock: endBlock,
args: {
pool: poolAddress,
},
});
// Fetch LiquidityRemoved events
const liquidityRemovedEvents = await publicClient.getContractEvents({
address: contractAddress,
abi: [liquidityRemovedEvent],
fromBlock: currentBlock,
toBlock: endBlock,
args: {
pool: poolAddress,
},
});
// Process Swap events
swapEvents.forEach((event) => {
console.log('Swap Event:', {
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
tokenIn: event.args.tokenIn,
tokenOut: event.args.tokenOut,
amountIn: event.args.amountIn,
amountOut: event.args.amountOut,
swapFeePercentage: event.args.swapFeePercentage,
swapFeeAmount: event.args.swapFeeAmount,
});
allEvents.push({ type: 'Swap', ...event });
});
// Process LiquidityAdded events
liquidityAddedEvents.forEach((event) => {
console.log('LiquidityAdded Event:', {
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
liquidityProvider: event.args.liquidityProvider,
kind: event.args.kind,
totalSupply: event.args.totalSupply,
amountsAddedRaw: event.args.amountsAddedRaw,
swapFeeAmountsRaw: event.args.swapFeeAmountsRaw,
});
allEvents.push({ type: 'LiquidityAdded', ...event });
});
// Process LiquidityRemoved events
liquidityRemovedEvents.forEach((event) => {
console.log('LiquidityRemoved Event:', {
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
liquidityProvider: event.args.liquidityProvider,
kind: event.args.kind,
totalSupply: event.args.totalSupply,
amountsRemovedRaw: event.args.amountsRemovedRaw,
swapFeeAmountsRaw: event.args.swapFeeAmountsRaw,
});
allEvents.push({ type: 'LiquidityRemoved', ...event });
});
currentBlock = endBlock + 1n;
}
// Sort events by block number
allEvents.sort((a, b) => {
if (a.blockNumber < b.blockNumber) return -1;
if (a.blockNumber > b.blockNumber) return 1;
return 0;
});
return allEvents;
}
function trackBalancesCorrect(events: PoolEvent[]) {
// Initialize balances array with 0n for each token
// Define token addresses
const tokens = [
'0x04c0599ae5a44757c0af6f9ec3b93da8976c150a',
'0x4200000000000000000000000000000000000006',
'0x50c5725949a6f0c72e6c4a641f24049a917db0cb',
'0x54330d28ca3357f294334bdc454a032e7f353416',
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
'0x9d0e8f5b25384c7310cb8c6ae32c8fbeb645d083',
'0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf',
'0xecac9c5f704e954931349da37f60e39f515c11c1',
];
const scalingFactors = [
1n,
1n,
1n,
1n,
1000000000000n,
1n,
10000000000n,
10000000000n,
];
const tokenRates = [
1000000000000000000n,
1000000000000000000n,
1000000000000000000n,
1000000000000000000n,
1000000000000000000n,
1000000000000000000n,
1000000000000000000n,
1000000000000000000n,
1000000000000000000n,
];
const balances: bigint[] = Array(tokens.length).fill(0n);
events.forEach((event) => {
if (event.type === 'Swap') {
const tokenIn = event.args.tokenIn as string;
const tokenOut = event.args.tokenOut as string;
const amountIn = BigInt(event.args.amountIn!);
const amountOut = BigInt(event.args.amountOut!);
const totalSwapFeeAmountRaw = BigInt(event.args.swapFeeAmount!);
const aggregateSwapFeeAmountRaw = mulDown(
totalSwapFeeAmountRaw,
aggregateFee,
);
// console.log(
// `in: ${tokenIn} ${amountIn} ${totalSwapFeeAmountRaw} ${aggregateSwapFeeAmountRaw}`,
// );
// Update balances based on token addresses with fee adjustment
const tokenInIndex = tokens.findIndex(
(t) => t.toLowerCase() === tokenIn.toLowerCase(),
);
const tokenOutIndex = tokens.findIndex(
(t) => t.toLowerCase() === tokenOut.toLowerCase(),
);
balances[tokenInIndex] += toScaled18ApplyRateRoundDown(
amountIn - aggregateSwapFeeAmountRaw,
scalingFactors[tokenInIndex],
tokenRates[tokenInIndex],
);
balances[tokenOutIndex] -= toScaled18ApplyRateRoundDown(
amountOut,
scalingFactors[tokenOutIndex],
tokenRates[tokenOutIndex],
);
} else if (event.type === 'LiquidityAdded') {
const amountsAdded = event.args.amountsAddedRaw!.map((a) =>
BigInt(a),
);
const swapFeeAmounts = event.args.swapFeeAmountsRaw!.map((a) =>
BigInt(a),
);
// Apply fee adjustments to each token
for (let i = 0; i < amountsAdded.length; i++) {
const totalSwapFeeAmountRaw = swapFeeAmounts[i];
const aggregateSwapFeeAmountRaw = mulDown(
totalSwapFeeAmountRaw,
aggregateFee,
);
balances[i] += toScaled18ApplyRateRoundDown(
amountsAdded[i] - aggregateSwapFeeAmountRaw,
scalingFactors[i],
tokenRates[i],
);
}
} else if (event.type === 'LiquidityRemoved') {
const amountsRemoved = event.args.amountsRemovedRaw!.map((a) =>
BigInt(a),
);
const swapFeeAmounts = event.args.swapFeeAmountsRaw!.map((a) =>
BigInt(a),
);
// Apply fee adjustments to each token
for (let i = 0; i < amountsRemoved.length; i++) {
const totalSwapFeeAmountRaw = swapFeeAmounts[i];
const aggregateSwapFeeAmountRaw = mulDown(
totalSwapFeeAmountRaw,
aggregateFee,
);
balances[i] -= toScaled18ApplyRateRoundDown(
amountsRemoved[i] + aggregateSwapFeeAmountRaw,
scalingFactors[i],
tokenRates[i],
);
}
}
// console.log(balances);
});
console.log('Final Balances (with fee adjustment):');
console.log(balances);
}
function toScaled18ApplyRateRoundDown(
amount: bigint,
scalingFactor: bigint,
tokenRate: bigint,
): bigint {
return mulDown(amount * scalingFactor, tokenRate);
}
async function main() {
const poolAddress = '0xaf5b7999f491c42c05b5a2ca80f1d200d617cc8c' as Address;
const vaultAddress =
'0xbA1333333333a1BA1108E8412f11850A5C319bA9' as Address;
const startBlock = 29292961;
const endBlock = 29508532;
const cacheFile = path.join(__dirname, 'paraswap-28-04-25.json');
let events: PoolEvent[];
// Try to load from cache first
if (fs.existsSync(cacheFile)) {
console.log('Loading events from cache...');
const cachedData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
events = cachedData;
} else {
console.log('Fetching events from blockchain...');
events = await getPoolEventsInChunks(
vaultAddress,
poolAddress,
startBlock,
endBlock,
);
// Save to cache file
console.log('Saving events to cache...');
fs.writeFileSync(
cacheFile,
JSON.stringify(
events,
(_, value) =>
typeof value === 'bigint' ? value.toString() : value,
2,
),
);
}
// Track and display balances with both methods
console.log('\nBalance tracking with fee adjustment:');
trackBalancesCorrect(events);
}
export default main;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment