Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding dynamic swap fee to fx pools #1101

Open
wants to merge 10 commits into
base: v3-canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilled-peas-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'backend': patch
---

adding dynamic swap fee to fx pools
1 change: 1 addition & 0 deletions graphql_schema_generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3322,6 +3322,7 @@ export const schema = gql`
poolReloadStakingForAllPools(stakingTypes: [GqlPoolStakingType!]!): String!
poolSyncAllCowSnapshots(chains: [GqlChain!]!): [GqlPoolMutationResult!]!
poolSyncAllPoolsFromSubgraph: [String!]!
poolSyncFxQuoteTokens(chains: [GqlChain!]!): [GqlPoolMutationResult!]!
poolUpdateLifetimeValuesForAllPools: String!
poolUpdateLiquidityValuesForAllPools: String!
protocolCacheMetrics: String!
Expand Down
19 changes: 12 additions & 7 deletions modules/actions/pool/v2/add-pools.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Chain } from '@prisma/client';
import { Chain, PrismaPool } from '@prisma/client';
import { prisma } from '../../../../prisma/prisma-client';
import { nestedPoolWithSingleLayerNesting } from '../../../../prisma/prisma-types';
import { V2SubgraphClient } from '../../../subgraphs/balancer-subgraph';
import { BalancerPoolFragment } from '../../../subgraphs/balancer-subgraph/generated/balancer-subgraph-types';
import { subgraphToPrismaCreate } from '../../../pool/subgraph-mapper';
import { upsertBptBalancesV2 } from '../../user/upsert-bpt-balances-v2';
import _ from 'lodash';
import { syncPoolTypeOnchainData } from './sync-pool-type-onchain-data';

export const addPools = async (subgraphService: V2SubgraphClient, chain: Chain): Promise<string[]> => {
const { block } = await subgraphService.legacyService.getMetadata();
Expand All @@ -25,9 +26,13 @@ export const addPools = async (subgraphService: V2SubgraphClient, chain: Chain):

const createdPools: string[] = [];
for (const subgraphPool of newPools) {
const created = await createPoolRecord(subgraphPool, chain, block.number, allNestedTypePools);
if (created) {
const dbPool = await createPoolRecord(subgraphPool, chain, block.number, allNestedTypePools);
if (dbPool) {
createdPools.push(subgraphPool.id);
// When new FX pool is added, we need to get the quote token
if (subgraphPool.poolType === 'FX') {
await syncPoolTypeOnchainData([dbPool], chain);
}
}
}

Expand All @@ -48,7 +53,7 @@ const createPoolRecord = async (
chain: Chain,
blockNumber: number,
nestedPools: { id: string; address: string }[],
): Promise<Boolean> => {
): Promise<PrismaPool | undefined> => {
const poolTokens = pool.tokens || [];

await prisma.prismaToken.createMany({
Expand All @@ -74,7 +79,7 @@ const createPoolRecord = async (
const prismaPoolRecordWithAssociations = subgraphToPrismaCreate(pool, chain, blockNumber, nestedPools);

try {
await prisma.prismaPool.create(prismaPoolRecordWithAssociations);
const pool = await prisma.prismaPool.create(prismaPoolRecordWithAssociations);

await prisma.prismaPoolTokenDynamicData.createMany({
data: poolTokens.map((token) => ({
Expand All @@ -90,11 +95,11 @@ const createPoolRecord = async (
});

await createAllTokensRelationshipForPool(pool.id, chain);

return pool;
} catch (e) {
console.error(`Could not create pool ${pool.id} on chain ${chain}. Skipping.`, e);
return false;
}
return true;
};

const createAllTokensRelationshipForPool = async (poolId: string, chain: Chain): Promise<void> => {
Expand Down
64 changes: 64 additions & 0 deletions modules/actions/pool/v2/sync-pool-type-onchain-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Abi } from 'abitype';
import FX from '../../../pool/abi/FxPool.json';
import { getViemClient, ViemClient } from '../../../sources/viem-client';
import { Chain, PrismaPoolType } from '@prisma/client';
import { prisma } from '../../../../prisma/prisma-client';
import { prismaBulkExecuteOperations } from '../../../../prisma/prisma-util';

const update = async (data: { id: string; chain: Chain; typeData: any }[]) => {
// Update the pool type data
const updates = data.map(({ id, chain, typeData }) =>
prisma.prismaPool.update({
where: { id_chain: { id, chain } },
data: { typeData },
}),
);

await prismaBulkExecuteOperations(updates, false);
};
Comment on lines +8 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think this works as a general approach. Some pool types will have mutable and immutable type data, meaning some will be synced at creation via subgraph and some will be synced via on-chain in regular intervals.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does it matter? do you mean to avoid unnecessary update calls?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually doesnt really here as you handle FX pools outside of the "update type data flow".


export const syncPoolTypeOnchainData = async (
pools: { id: string; chain: Chain; address: string; type: PrismaPoolType; typeData: any }[],
chain: Chain,
) => {
const viemClient = getViemClient(chain);

// Get FX pools
const fxPools = pools.filter((pool) => pool.type === 'FX');
const quoteTokens = await fetchFxQuoteTokens(fxPools, viemClient);
await update(quoteTokens);

return true;
};

export const fetchFxQuoteTokens = async (
pools: { id: string; chain: Chain; address: string; typeData: any }[],
viemClient: ViemClient,
) => {
// Fetch the tokens from the subgraph
const contracts = pools.map(({ address }) => {
return {
address: address as `0x${string}`,
abi: FX as Abi,
functionName: 'derivatives',
args: [1],
};
});

const results = await viemClient.multicall({ contracts, allowFailure: true });

return results
.map((call, index) => {
// If the call failed, return null
if (call.status === 'failure') return null;

const typeData = { ...pools[index].typeData, quoteToken: (call.result as string).toLowerCase() };

return {
id: pools[index].id,
chain: pools[index].chain,
typeData,
};
})
.filter((quoteToken): quoteToken is { id: string; chain: Chain; typeData: any } => quoteToken !== null);
};
16 changes: 14 additions & 2 deletions modules/actions/pool/v2/sync-swaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ export async function syncSwaps(subgraphClient: V2SubgraphClient, chain: Chain):
},
});

// Get list of FX pool addresses for the fee calculation
const fxPools = (await prisma.prismaPool.findMany({
where: {
chain: chain,
type: 'FX',
},
select: {
id: true,
typeData: true, // contains the quote token address
},
})) as { id: string; typeData: { quoteToken: string } }[];

// Querying by timestamp of Fantom, because it has events without a block number in the DB
const where = latestEvent
? chain === Chain.FANTOM
Expand All @@ -47,14 +59,14 @@ export async function syncSwaps(subgraphClient: V2SubgraphClient, chain: Chain):
console.time('BalancerSwaps');
const { swaps } = await subgraphClient.BalancerSwaps({
first: 1000,
where,
where: where,
orderBy: chain === Chain.FANTOM ? Swap_OrderBy.Timestamp : Swap_OrderBy.Block,
orderDirection: OrderDirection.Asc,
});
console.timeEnd('BalancerSwaps');

console.time('swapV2Transformer');
const dbSwaps = swaps.map((swap) => swapV2Transformer(swap, chain));
const dbSwaps = swaps.map((swap) => swapV2Transformer(swap, chain, fxPools));
console.timeEnd('swapV2Transformer');

// TODO: parse batchSwaps, if needed
Expand Down
9 changes: 9 additions & 0 deletions modules/controllers/fx-pools-controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import config from '../../config';
import { prisma } from '../../prisma/prisma-client';
import { syncPoolTypeOnchainData } from '../actions/pool/v2/sync-pool-type-onchain-data';
import { syncLatestFXPrices } from '../token/latest-fx-price';
import { Chain } from '@prisma/client';

Expand All @@ -11,5 +13,12 @@ export function FXPoolsController() {

return syncLatestFXPrices(balancer, chain);
},
async syncQuoteTokens(chain: Chain) {
const pools = await prisma.prismaPool.findMany({
where: { chain, type: 'FX' },
});

return syncPoolTypeOnchainData(pools, chain);
},
};
}
1 change: 1 addition & 0 deletions modules/pool/pool.gql
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ extend type Mutation {
poolLoadOnChainDataForAllPools(chains: [GqlChain!]!): [GqlPoolMutationResult!]!
poolReloadPools(chains: [GqlChain!]!): [GqlPoolMutationResult!]!
poolSyncAllCowSnapshots(chains: [GqlChain!]!): [GqlPoolMutationResult!]!
poolSyncFxQuoteTokens(chains: [GqlChain!]!): [GqlPoolMutationResult!]!
}

"""
Expand Down
25 changes: 24 additions & 1 deletion modules/pool/pool.resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { GqlChain, Resolvers } from '../../schema';
import { isAdminRoute } from '../auth/auth-context';
import { networkContext } from '../network/network-context.service';
import { headerChain } from '../context/header-chain';
import { CowAmmController, EventsQueryController, SnapshotsController, PoolController } from '../controllers';
import {
CowAmmController,
EventsQueryController,
SnapshotsController,
PoolController,
FXPoolsController,
} from '../controllers';
import { chainIdToChain } from '../network/chain-id-to-chain';

const balancerResolvers: Resolvers = {
Expand Down Expand Up @@ -202,6 +208,23 @@ const balancerResolvers: Resolvers = {
}
}

return result;
},
poolSyncFxQuoteTokens: async (parent, { chains }, context) => {
isAdminRoute(context);

const result: { type: string; chain: GqlChain; success: boolean; error: string | undefined }[] = [];

for (const chain of chains) {
try {
await FXPoolsController().syncQuoteTokens(chain);
result.push({ type: 'fx', chain, success: true, error: undefined });
} catch (e) {
result.push({ type: 'fx', chain, success: false, error: `${e}` });
console.log(`Could not sync fx quote tokens for chain ${chain}: ${e}`);
}
}

return result;
},
},
Expand Down
3 changes: 2 additions & 1 deletion modules/sources/enrichers/swaps-usd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ export async function swapsUsd(swaps: SwapEvent[], chain: Chain): Promise<SwapEv
const tokenOut = tokenPrices.find((price) => price.tokenAddress === swap.payload.tokenOut.address);
const feeToken = tokenPrices.find((price) => price.tokenAddress === swap.payload.fee.address);
const surplusToken = tokenPrices.find((price) => price.tokenAddress === swap.payload.surplus?.address);
const feeValueUSD = parseFloat(swap.payload.fee.amount) * (feeToken?.price || 0);

const payload = {
fee: {
...swap.payload.fee,
valueUSD: String((feeToken?.price || 0) * parseFloat(swap.payload.fee.amount)),
valueUSD: String(feeValueUSD > 0 ? feeValueUSD : swap.payload.fee.valueUSD),
Comment on lines -47 to +48
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you set it to swap.payload.fee.valueUSD because that is still 0 at this time? Think this is a but bogus, just set to 0?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a passthrough for a default USD price coming from the subgraph, in case there is no pricing in the DB. It's set earlier and it important to the FX fee, because it's set based on the subgraph data of latestFx rates.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha, this is v2. This is just used as fallback so its fine 👍

},
tokenIn: {
...swap.payload.tokenIn,
Expand Down
47 changes: 43 additions & 4 deletions modules/sources/transformers/swap-v2-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,53 @@ import { SwapEvent } from '../../../prisma/prisma-types';
* @param chain
* @returns
*/
export function swapV2Transformer(swap: BalancerSwapFragment, chain: Chain): SwapEvent {
export function swapV2Transformer(
swap: BalancerSwapFragment,
chain: Chain,
fxPools: { id: string; typeData: { quoteToken: string } }[] = [],
): SwapEvent {
// Avoiding scientific notation
const feeFloat = parseFloat(swap.tokenAmountIn) * parseFloat(swap.poolId.swapFee ?? 0);
const fee = feeFloat < 1e6 ? feeFloat.toFixed(18).replace(/0+$/, '').replace(/\.$/, '') : String(feeFloat);
const feeFloatUSD = parseFloat(swap.valueUSD) * parseFloat(swap.poolId.swapFee ?? 0);
const feeUSD =
let fee = feeFloat < 1e6 ? feeFloat.toFixed(18).replace(/0+$/, '').replace(/\.$/, '') : String(feeFloat);
let feeFloatUSD = parseFloat(swap.valueUSD) * parseFloat(swap.poolId.swapFee ?? 0);
let feeUSD =
feeFloatUSD < 1e6 ? feeFloatUSD.toFixed(18).replace(/0+$/, '').replace(/\.$/, '') : String(feeFloatUSD);

// FX pools have a different fee calculation
// Replica of the subgraph logic:
// https://github.com/balancer/balancer-subgraph-v2/blob/60453224453bd07a0a3a22a8ad6cc26e65fd809f/src/mappings/vault.ts#L551-L564
if (swap.poolId.poolType === 'FX') {
// Find the pool that has the quote token
const fxPool = fxPools.find((pool) => pool.id === swap.poolId.id);
if (fxPool && [swap.tokenOut, swap.tokenIn].includes(fxPool.typeData.quoteToken)) {
const quoteTokenAddress = fxPool.typeData.quoteToken;
const baseTokenAddress = swap.tokenIn === quoteTokenAddress ? swap.tokenOut : swap.tokenIn;
let isTokenInBase = swap.tokenOut === quoteTokenAddress;
let baseToken = swap.poolId.tokens?.find(({ token }) => token.address == baseTokenAddress);
let quoteToken = swap.poolId.tokens?.find(({ token }) => token.address == quoteTokenAddress);
let baseRate = baseToken != null ? baseToken.token.latestFXPrice : null;
let quoteRate = quoteToken != null ? quoteToken.token.latestFXPrice : null;

if (baseRate && quoteRate) {
if (isTokenInBase) {
feeFloatUSD +=
parseFloat(swap.tokenAmountIn) * parseFloat(baseRate) -
parseFloat(swap.tokenAmountOut) * parseFloat(quoteRate);
// Need to set the fee in the tokenIn price, because it's later recalculated based on the DB prices
fee = String(feeFloatUSD / parseFloat(baseRate)); // fee / tokenIn price
} else {
feeFloatUSD +=
parseFloat(swap.tokenAmountIn) * parseFloat(quoteRate) -
parseFloat(swap.tokenAmountOut) * parseFloat(baseRate);
// Need to set the fee in the tokenIn price, because it's later recalculated based on the DB prices
fee = String(feeFloatUSD / parseFloat(quoteRate)); // fee / tokenIn price
}
}

feeUSD = String(feeFloatUSD);
}
}

return {
id: swap.id, // tx + logIndex
tx: swap.tx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,13 @@ fragment BalancerSwap on Swap {
poolId {
id
swapFee
poolType
tokens {
token {
address
latestFXPrice
}
}
}
userAddress {
id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6744,7 +6744,19 @@ export type BalancerSwapsQuery = {
tx: string;
valueUSD: string;
block?: string | null | undefined;
poolId: { __typename?: 'Pool'; id: string; swapFee: string };
poolId: {
__typename?: 'Pool';
id: string;
swapFee: string;
poolType?: string | null | undefined;
tokens?:
| Array<{
__typename?: 'PoolToken';
token: { __typename?: 'Token'; address: string; latestFXPrice?: string | null | undefined };
}>
| null
| undefined;
};
userAddress: { __typename?: 'User'; id: string };
}>;
};
Expand All @@ -6763,7 +6775,19 @@ export type BalancerSwapFragment = {
tx: string;
valueUSD: string;
block?: string | null | undefined;
poolId: { __typename?: 'Pool'; id: string; swapFee: string };
poolId: {
__typename?: 'Pool';
id: string;
swapFee: string;
poolType?: string | null | undefined;
tokens?:
| Array<{
__typename?: 'PoolToken';
token: { __typename?: 'Token'; address: string; latestFXPrice?: string | null | undefined };
}>
| null
| undefined;
};
userAddress: { __typename?: 'User'; id: string };
};

Expand Down Expand Up @@ -7092,6 +7116,13 @@ export const BalancerSwapFragmentDoc = gql`
poolId {
id
swapFee
poolType
tokens {
token {
address
latestFXPrice
}
}
}
userAddress {
id
Expand Down
Loading