Created
April 29, 2025 08:47
-
-
Save johngrantuk/0701c41fcc0c2e5933d2c869672155ae to your computer and use it in GitHub Desktop.
Confirm balance tracking works via events
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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