Incorporating trading fees
Last modified:
Let denote the pool fee. For example, if the pool fee is , then . Let and denote the pre-trade reserves of tokens and , respectively, so that
Suppose a trader deposits a gross amount of token and receives an amount of token . The actual post-trade pool reserves will be , because the trader sends the gross amount to the pool and withdraws . However, since the fee is charged on the input side, only the effective amount participates in the constant-product update. The fee-adjusted swap condition is therefore
Expanding the left-hand side, we obtain
Hence,
Solving for , we obtain
Thus, if a trader sends an input amount of token , the amount of token received is
Likewise, solving for , we obtain
This formula is the exact real-valued input. On-chain integer arithmetic applies rounding rules: getAmountOut floors the quotient, while getAmountIn returns floor division plus one wei (shown in the Solidity implementation below).
Therefore, if a trader wants to receive an output amount of token , the required gross input amount of token is
It can also be written in the canonical CenturionDEX v2 code-style notation:
Worked example (with fee):
Using the canonical notation introduced above, the correspondence between the implementation variables and our mathematical notation is
Moreover, the code in the next block hardcodes a trading fee of . For this reason, the implementation uses the integer factors and instead of writing the fee term as directly. This scaling is only a convenient way to avoid floating-point arithmetic in the smart contract; algebraically, it is equivalent to the same swap formula once the quotient is simplified.
// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'CenturionV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'CenturionV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
// given an output amount of an asset and pair reserves, returns a required input amount of the other asset
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
require(amountOut > 0, 'CenturionV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'CenturionV2Library: INSUFFICIENT_LIQUIDITY');
uint numerator = reserveIn.mul(amountOut).mul(1000);
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}Note that getAmountIn returns (numerator / denominator) + 1. The +1 is a deliberate ceiling-style adjustment that ensures the post-swap balances satisfy the on-chain K check (balance0Adjusted * balance1Adjusted >= reserve0 * reserve1 * 1000^2) even when integer division truncates.
It is important to note that, after a swap, the pool reserves are updated as follows:
Accordingly, the product parameter is updated from to a new value , where
Expanding this expression, we obtain
From the fee-adjusted swap equation,
it follows that
Therefore,
In particular, if , then . In other words, whenever the pool charges a positive fee, the product parameter increases slightly after each trade. This increase reflects the fact that part of the input amount remains in the pool and is captured by liquidity providers.
The spot price also changes after the swap. The new spot price of token in terms of token is
In summary, if the pool has no fees, that is, if , then , and the pre-trade and post-trade reserve states lie on the same constant-product curve . By contrast, if , then , so the post-trade reserve state lies on a higher constant-product curve.
If the liquidity pool charges a positive fee , then the product parameter increases after each trade. Let and denote the pool reserves before the trade, and suppose a trader sends a gross amount of token to the pool and receives an amount of token . Since the fee is charged on the input side, only the amount is effective for the swap calculation. Therefore, the fee-adjusted constant-product condition is
This is the natural canonical version of the same relation.
As shown above, the fee-adjusted swap condition implies that the effective post-trade state
still lies on the original constant-product curve, since
This corresponds to the solid curve in the animation above. However, the actual pool reserves after the trade are
Moreover,
Thus, the actual post-trade reserve state is vertically above the fee-adjusted point by the amount of fees collected in token . As a result, the updated reserves no longer lie on the original curve , but instead lie on a new constant-product curve with a larger product parameter. This is the dashed curve in the next animation:
Increase in the Average Execution Price
Rounding convention for this example: displayed numeric results are rounded to two decimals.
Consider a CenturionDEX v2 pool between CTN and USDC with trading fee . Let the initial reserves be
The initial spot price is therefore
Suppose a trader wants to buy a fixed amount
of token , that is, 400 CTN. Using the fee-adjusted output formula,
the required USDC input for the first purchase is
Numerically, this gives
Hence, the average execution price of the first purchase is
So, buying 400 CTN initially costs about 1,266,958.77 USDC, which corresponds to an average price of about 3,167.40 USDC per CTN.
After this first trade, the pool reserves are updated to
Thus, after the first purchase, the pool contains 7,600 CTN and approximately 25,266,958.77 USDC.
Now suppose the trader wants to buy another 400 CTN. Applying the same formula again, but this time using the updated reserve state , we obtain
Therefore,
and the average execution price of the second purchase is
So, the second 400-CTN purchase is more expensive than the first one: it requires about 1,407,943.76 USDC, which corresponds to an average price of about 3,519.86 USDC per CTN.
This illustrates a general feature of constant-product AMMs: repeated buys of the same asset push the average execution price upward. After each purchase, the reserve of token decreases while the reserve of token increases, so subsequent buyers face a less favorable point on the pricing curve.
More generally, if a trader buys an amount of token from a pool with current reserves , then the total amount of token that must be paid is
Accordingly, the average execution price is
This expression makes the previous effect explicit: as becomes smaller and becomes larger after each buy, the average purchase price increases.