Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
@paraswap/dex-lib
Advanced tools
**DexLib** is a library used by ParaSwap backend to integrate with decentralized exchanges. This library enables external DEX developers to integrate their DEX with ParaSwap by creating pull requests to this repository.
DexLib is a library used by ParaSwap backend to integrate with decentralized exchanges. This library enables external DEX developers to integrate their DEX with ParaSwap by creating pull requests to this repository.
feature/super-dex
)yarn install
param-case
:yarn init-integration <your-dex-name>
You can find template code for newly integrated DEX in src/dex/<your-dex-name>
Complete the template code by filling the functions implementations. Template code is highly documented which should help you build the implementation. You should look into existing DEX implementation in src/dex/
to understand the interfaces. Please refer below for detailed explanations and good practices.
Add <your-dex-name>
to dexes
list in src/dex/index.ts
Complete the test templates (All files with src/dex/<your-dex-name>/*.test.ts
). Each DEX implementation should have thorough testing. We have multiple kinds of tests each dex must have. You can refer to Writing Tests for detailed explanation. You can run all the tests using
yarn test-integration <your-dex-name>
ParaSwap optimizes price serving through an innovative event-based approach, bypassing the need for frequent fullnode RPC calls by utilizing smart contract events and in-memory state for pricing. This method, abstracted for ease of implementation, requires DEXs to initially fetch on-chain state, subscribe to updates via events, and use the updated in-memory state for efficient pricing.
Additionally, ParaSwap's main router, known as Augustus, is ingeniously crafted to only necessitate canonical (offchain) hints for navigating swaps across different DEXs. This design enables the seamless execution of sophisticated trading strategies that involve multiple DEXs and various layers of token swaps. This design also means that any liquidity source can be added without any contract change in most of the cases.
Typically, the first step of an integration would be to initialize its pools' state.
This can be done either
initializePricing
function in your DEX
orgetPricesVolume
functionThe latter is the most preferred option.
async initializePricing(blockNumber: number) {
const poolState = await getOnChainState(
this.dexHelper.multiContract,
this.swETHAddress,
this.swETHInterface,
blockNumber,
);
await this.eventPool.initialize(blockNumber, {
state: poolState,
});
}
The example above shows how the current state of the swETH
contract is, in this case the swETHToEthRate
. The getOnChainState
call is executing a Multicall contract call to the swETHAddress
(using the swETH ABI).
export async function getOnChainState(
multiContract: Contract,
poolAddress: string,
poolInterface: Interface,
blockNumber: number | 'latest',
): Promise<SWETHPoolState> {
const data: { returnData: any[] } = await multiContract.methods
.aggregate([
{
target: poolAddress,
callData: poolInterface.encodeFunctionData('swETHToETHRate', []),
},
])
.call({}, blockNumber);
const decodedData = coder.decode(['uint256'], data.returnData[0]);
const swETHToETHRateFixed = BigInt(decodedData[0].toString());
return {
swETHToETHRateFixed,
};
}
When interacting with smart-contracts on any blockchain, you should try to be mindful of the cost of RPC calls, thus, the best approach is to use Multicall. the Multicall contract can be accessed via this.dexHelper.multiContract
from your DEX.
The initializePricing
call will take care of setting the initial state of the pool (at the time of the DEX initializing). To ensure correct pricing, you can leverage the Stateful Event Subscriber.
Following the previous example, we can now implement a listener for the Reprice
event, which is emitted by the swETH
pool when the swETH -> ETH
rate changes, which is what our DEX needs to be aware of. To do this, we can override the processLog
method in the StatefulEventSubscriber
declared by our DEX. This method is called when an event which the subscriber is listening to is emitted. The state will be modified after the function is called, only if a state is returned. Returning null
will be ignored and the state will not be altered. In the case where you require RPC calls, you can implement the StatefulRpcPoller
, however using this may result in undesired charges as doing RPC calls is far more expensive.
decoder = (log: Log) => this.poolInterface.parseLog(log);
protected processLog(
state: DeepReadonly<SWETHPoolState>,
log: Readonly<Log>,
): AsyncOrSync<DeepReadonly<SWETHPoolState> | null> {
const event = this.decoder(log);
if (event.name === 'Reprice')
return {
swETHToETHRateFixed: BigInt(event.args.newSwETHToETHRate),
};
return null;
}
Some other DEX pools which could come in handy when implementing your own DEX pool listener.
Now that we can guarantee an up-to-date state of our pool, we need to ensure that we always provide correct pricing and rates when constructing a transaction.
To do this, the best approach is to replicate the contract's behaviour and calculations, any mistake in the number manipulation (mathematical operations, bit shifting, etc) can lead to wrong prices and result in various scenarios, for example:
Given swETH Contract we can check how it prices the out value to the deposit
function, and implement the counterpart on our DEX to make sure pricing is correct.
uint256 swETHAmount = wrap(msg.value).mul(_ethToSwETHRate()).unwrap();
This line is taking the ETH value (msg.value
) of the deposit
function and multiplying it to the current ethToSwETHRate
, since the pool keeps track of the opposite rate (swETH -> ETH
), we need to reverse this logic.
getPrice(blockNumber: number, ethAmount: bigint): bigint {
const state = this.getState(blockNumber);
if (!state) throw new Error('Cannot compute price');
const { swETHToETHRateFixed } = state;
return (ethAmount * BI_POWS[18]) / swETHToETHRateFixed;
}
When a user wants to swap, we need to compute Augustus's calldata, this involves computing the necessary data to swap through your DEX. Concretely, you need to abi-encode a swap through your DEX for a given placeholder amount and extra metadata about your DEX in order to allow to perform complex swaps.
For instance, let's take the swell
DEX integration and the getDexParam
implementation to see how this encoding is achieved. Keep in mind that the following example was stripped down for simplicity.
getDexParam(
srcToken: Address,
destToken: Address,
srcAmount: NumberAsString,
destAmount: NumberAsString,
recipient: Address,
data: SwellData,
side: SwapSide,
): DexExchangeParam {
const swapData = this.swETHInterface.encodeFunctionData(
swETHFunctions.deposit,
[],
);
return {
needWrapNative: this.needWrapNative,
dexFuncHasRecipient: false,
exchangeData: swapData,
targetExchange: this.swETHAddress,
returnAmountPos: undefined,
};
}
getDexParam(
srcToken: Address,
destToken: Address,
srcAmount: NumberAsString,
destAmount: NumberAsString,
recipient: Address,
data: WooFiV2Data,
side: SwapSide,
): DexExchangeParam {
if (side === SwapSide.BUY) throw new Error(`Buy not supported`);
const _srcToken = srcToken.toLowerCase();
const _destToken = destToken.toLowerCase();
const swapData = ifaces.PPV2.encodeFunctionData('swap', [
_srcToken,
_destToken,
srcAmount,
MIN_CONVERSION_RATE,
recipient,
rebateTo,
]);
return {
needWrapNative: this.needWrapNative,
dexFuncHasRecipient: true,
exchangeData: swapData,
targetExchange: this.config.wooPPV2Address,
transferSrcTokenBeforeSwap: this.config.wooPPV2Address,
returnAmountPos:
side === SwapSide.SELL
? extractReturnAmountPosition(ifaces.PPV2, 'swap', 'realToAmount')
: undefined,
};
}
A considerable amount of abstraction is incorporated into the DEX library's upper encoding logic and Augustus to precisely set the swap amount (especially if your DEX follows another swap affected by slippage in any direction). Moreover, it ensures the successful execution of the swap by managing all necessary prerequisites, including token approvals, wrapping/unwrapping of WETH, transfers, and more.
In order to ensure correctness of encoding please make use of these parameters:
needWrapNative
: if true, tells if the DEX only deals with wrapped native tokens (eg. on Ethereum it only executes trades with wETH, not native ETH).dexFuncHasRecipient
: if true, tells if the DEX can swap and transfer to an arbitrary address (recipient
) else we would append a transfer callexchangeData
: the call data required by the DEX, and typically requires targetting the contract's interface to encode data.transferSrcTokenBeforeSwap
: if your DEX requires a transfer before the swap happens, rather than encoding it within the exchangeData
targetExchange
: the contract against which we swapspender
: a contract that we need to approve in order to swap against targetExchange
. If not set, then the spender will be targetExchange
returnAmountPos
: the offset position inside the return values from an external call to a dex where we expect our swap return value (output amount) to be.
There is a helper function extractReturnAmountPosition
which could be used to automatically calculate return amount position.
If the DEX swap function doesn't support outputs then undefined
should be passed.
For example:
To verify the validity of the encoding, we recommend looking at this link and using Tenderly to validate transactions. If the encoding is done incorrectly at the Interface level, you will see errors in your testing logs.
When the ParaSwap aggregator is searching for the best connector between two or more tokens, it needs to be able to access this data in a fast and reliable manner.
These data aggregations can be very time-consuming, include undesired data, and for FIAT calculations, result in invalid prices and such mistakes. To do this, certain DEXes implement the getTopPoolsForToken
method, which calls specific Subgraphs (on The Graph), eg. Uniswap V2 subgraph.
This method will return an array of the following type:
export type PoolLiquidity = {
exchange: string;
address: Address;
connectorTokens: Token[];
liquidityUSD: number;
};
We can look at the Solidly V3 implementation below, to see how the DEX interacts with a subgraph to compute the PoolLiquidity[]
.
async getTopPoolsForToken(
tokenAddress: Address,
limit: number,
): Promise<PoolLiquidity[]> {
const _tokenAddress = tokenAddress.toLowerCase();
const res = await this._querySubgraph(
`query ($token: Bytes!, $count: Int) {
pools0: pools(first: $count, orderBy: totalValueLockedUSD, orderDirection: desc, where: {token0: $token, liquidity_gt: "0"}) {
id
token0 {
id
decimals
}
token1 {
id
decimals
}
totalValueLockedUSD
}
pools1: pools(first: $count, orderBy: totalValueLockedUSD, orderDirection: desc, where: {token1: $token, liquidity_gt: "0"}) {
id
token0 {
id
decimals
}
token1 {
id
decimals
}
totalValueLockedUSD
}
}`,
{
token: _tokenAddress,
count: limit,
},
);
if (!(res && res.pools0 && res.pools1)) {
this.logger.error(
`Error_${this.dexKey}_Subgraph: couldn't fetch the pools from the subgraph`,
);
return [];
}
const pools0 = _.map(res.pools0, pool => ({
exchange: this.dexKey,
address: pool.id.toLowerCase(),
connectorTokens: [
{
address: pool.token1.id.toLowerCase(),
decimals: parseInt(pool.token1.decimals),
},
],
liquidityUSD:
parseFloat(pool.totalValueLockedUSD) * UNISWAPV3_EFFICIENCY_FACTOR,
}));
const pools1 = _.map(res.pools1, pool => ({
exchange: this.dexKey,
address: pool.id.toLowerCase(),
connectorTokens: [
{
address: pool.token0.id.toLowerCase(),
decimals: parseInt(pool.token0.decimals),
},
],
liquidityUSD:
parseFloat(pool.totalValueLockedUSD) * UNISWAPV3_EFFICIENCY_FACTOR,
}));
const pools = _.slice(
_.sortBy(_.concat(pools0, pools1), [pool => -1 * pool.liquidityUSD]),
0,
limit,
);
return pools;
}
<you-dex-name>-integration.test.ts
): Tests the basic validity of the integration like if prices are valid, obeys the limit pools, etc.<you-dex-name>-events.test.ts
): Unit tests the event based system. This is done by fetching the state on-chain before the event, manually pushing the block logs to the event subscriber, comparing the local state with on-chain state.<you-dex-name>-e2e.test.ts
): End to end test the integration which involves pricing, transaction building and simulating the transaction on chain using tenderly fork simulations. E2E tests use the Tenderly fork api. Please add the following to your .env file:In order to run tests, you will need to use Tenderly and so have .env file with this environment variables
TENDERLY_TOKEN=Find this under Account>Settings>Authorization.
TENDERLY_ACCOUNT_ID=Your Tenderly account name.
TENDERLY_PROJECT=Name of a Tenderly project you have created in your
dashboard.
FAQs
**DexLib** is a library used by ParaSwap backend to integrate with decentralized exchanges. This library enables external DEX developers to integrate their DEX with ParaSwap by creating pull requests to this repository.
We found that @paraswap/dex-lib demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 9 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.