Skip to content

Commit

Permalink
Feature: add token properties provider (#394)
Browse files Browse the repository at this point in the history
* token properties provider

* separate token fetcher from token properties provider

* prettier

* revert alpha-router prettier

* fix tokenfeedetector factory import

* address feedbacks

* feedback on cache key format

* add cache node unit test

* address feedbacks

* fix import

* allowlist tokens don't call out to token validator provider at all

* update with the unit test coverage against token properties provider so far (missing failure modes)

* complete the unit test coverages

* fix prettier for the new files

* token fee fetcher returns when there's either buy fee or sell fee

* in token properties provider, double check that only buy fee or sell fee populated should get cached

* change token fee detector address to the same one interface uses

* prettier
  • Loading branch information
jsy1218 authored Sep 7, 2023
1 parent ec9dc50 commit 59ce2bc
Show file tree
Hide file tree
Showing 11 changed files with 720 additions and 7 deletions.
133 changes: 133 additions & 0 deletions src/abis/TokenFeeDetector.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_factoryV2",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "PairLookupFailed",
"type": "error"
},
{
"inputs": [],
"name": "SameToken",
"type": "error"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "tokens",
"type": "address[]"
},
{
"internalType": "address",
"name": "baseToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountToBorrow",
"type": "uint256"
}
],
"name": "batchValidate",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "buyFeeBps",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "sellFeeBps",
"type": "uint256"
}
],
"internalType": "struct TokenFees[]",
"name": "fotResults",
"type": "tuple[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "uniswapV2Call",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "baseToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountToBorrow",
"type": "uint256"
}
],
"name": "validate",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "buyFeeBps",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "sellFeeBps",
"type": "uint256"
}
],
"internalType": "struct TokenFees",
"name": "fotResult",
"type": "tuple"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
13 changes: 13 additions & 0 deletions src/providers/cache-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ export class NodeJSCache<T> implements ICache<T> {
return this.nodeCache.get<T>(key);
}

async batchGet(keys: Set<string>): Promise<Record<string, T | undefined>> {
const keysArr = Array.from(keys);
const values = await Promise.all(keysArr.map((key) => this.get(key)));

const result: Record<string, T | undefined> = {};

keysArr.forEach((key, index) => {
result[key] = values[index];
});

return result;
}

async set(key: string, value: T): Promise<boolean> {
return this.nodeCache.set(key, value);
}
Expand Down
2 changes: 2 additions & 0 deletions src/providers/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
export interface ICache<T> {
get(key: string): Promise<T | undefined>;

batchGet(keys: Set<string>): Promise<Record<string, T | undefined>>;

set(key: string, value: T): Promise<boolean>;

has(key: string): Promise<boolean>;
Expand Down
3 changes: 2 additions & 1 deletion src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './cache-node';
export * from './caching-gas-provider';
export * from './caching-token-list-provider';
export * from './caching-token-provider';
export * from './caching/route';
export * from './eip-1559-gas-price-provider';
export * from './eth-estimate-gas-provider';
export * from './eth-gas-station-info-gas-price-provider';
Expand All @@ -16,10 +17,10 @@ export * from './simulation-provider';
export * from './static-gas-price-provider';
export * from './swap-router-provider';
export * from './tenderly-simulation-provider';
export * from './token-properties-provider';
export * from './token-provider';
export * from './token-validator-provider';
export * from './uri-subgraph-provider';
export * from './caching/route';
export * from './v2/caching-pool-provider';
export * from './v2/caching-subgraph-provider';
export * from './v2/pool-provider';
Expand Down
117 changes: 117 additions & 0 deletions src/providers/token-fee-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { BigNumber } from '@ethersproject/bignumber';
import { BaseProvider } from '@ethersproject/providers';
import { ChainId } from '@uniswap/sdk-core';

import { TokenFeeDetector__factory } from '../types/other/factories/TokenFeeDetector__factory';
import { TokenFeeDetector } from '../types/other/TokenFeeDetector';
import { log, WRAPPED_NATIVE_CURRENCY } from '../util';

import { ProviderConfig } from './provider';

const DEFAULT_TOKEN_BUY_FEE_BPS = BigNumber.from(0);
const DEFAULT_TOKEN_SELL_FEE_BPS = BigNumber.from(0);

// on detector failure, assume no fee
export const DEFAULT_TOKEN_FEE_RESULT = {
buyFeeBps: DEFAULT_TOKEN_BUY_FEE_BPS,
sellFeeBps: DEFAULT_TOKEN_SELL_FEE_BPS,
};

type Address = string;

export type TokenFeeResult = {
buyFeeBps?: BigNumber;
sellFeeBps?: BigNumber;
};
export type TokenFeeMap = Record<Address, TokenFeeResult>;

// address at which the FeeDetector lens is deployed
const FEE_DETECTOR_ADDRESS = (chainId: ChainId) => {
switch (chainId) {
case ChainId.MAINNET:
default:
return '0x19C97dc2a25845C7f9d1d519c8C2d4809c58b43f';
}
};

// Amount has to be big enough to avoid rounding errors, but small enough that
// most v2 pools will have at least this many token units
// 10000 is the smallest number that avoids rounding errors in bps terms
const AMOUNT_TO_FLASH_BORROW = '10000';
// 1M gas limit per validate call, should cover most swap cases
const GAS_LIMIT_PER_VALIDATE = 1_000_000;

export interface ITokenFeeFetcher {
fetchFees(
addresses: Address[],
providerConfig?: ProviderConfig
): Promise<TokenFeeMap>;
}

export class OnChainTokenFeeFetcher implements ITokenFeeFetcher {
private BASE_TOKEN: string;
private readonly contract: TokenFeeDetector;

constructor(
private chainId: ChainId,
rpcProvider: BaseProvider,
private tokenFeeAddress = FEE_DETECTOR_ADDRESS(chainId),
private gasLimitPerCall = GAS_LIMIT_PER_VALIDATE,
private amountToFlashBorrow = AMOUNT_TO_FLASH_BORROW
) {
this.BASE_TOKEN = WRAPPED_NATIVE_CURRENCY[this.chainId]?.address;
this.contract = TokenFeeDetector__factory.connect(
this.tokenFeeAddress,
rpcProvider
);
}

public async fetchFees(
addresses: Address[],
providerConfig?: ProviderConfig
): Promise<TokenFeeMap> {
const tokenToResult: TokenFeeMap = {};

const functionParams = addresses.map((address) => [
address,
this.BASE_TOKEN,
this.amountToFlashBorrow,
]) as [string, string, string][];

const results = await Promise.all(
functionParams.map(async ([address, baseToken, amountToBorrow]) => {
try {
// We use the validate function instead of batchValidate to avoid poison pill problem.
// One token that consumes too much gas could cause the entire batch to fail.
const feeResult = await this.contract.callStatic.validate(
address,
baseToken,
amountToBorrow,
{
gasLimit: this.gasLimitPerCall,
blockTag: providerConfig?.blockNumber,
}
);
return { address, ...feeResult };
} catch (err) {
log.error(
{ err },
`Error calling validate on-chain for token ${address}`
);
// in case of FOT token fee fetch failure, we return null
// so that they won't get returned from the token-fee-fetcher
// and thus no fee will be applied, and the cache won't cache on FOT tokens with failed fee fetching
return { address, buyFeeBps: undefined, sellFeeBps: undefined };
}
})
);

results.forEach(({ address, buyFeeBps, sellFeeBps }) => {
if (buyFeeBps || sellFeeBps) {
tokenToResult[address] = { buyFeeBps, sellFeeBps };
}
});

return tokenToResult;
}
}
Loading

0 comments on commit 59ce2bc

Please sign in to comment.