Skip to content

feat: handle app fees #1677

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

feat: handle app fees #1677

wants to merge 1 commit into from

Conversation

ashwinrava
Copy link
Member

@ashwinrava ashwinrava commented Jul 23, 2025

  • Add 2 query params appFeePercent and appFeeRecipient
  • Add function calculateAppFee which returns the the fee amount and action to be used by the multicall handler
  • Use new function in cross-swap-service for different swap scenarios to apply the app fee
  • Create final JSON with USD amounts for each fee and converted base unit values

Tested and documented scenario outputs here: https://docs.google.com/document/d/1YawQNsNY-7I8OXqLImtveve6Y306WTD-cJYmeme7lA8

Closes ACX-4225

Closes ACX-4224

Copy link

vercel bot commented Jul 23, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
app-frontend-v3 ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 1, 2025 5:14pm
sepolia-frontend-v3 ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 1, 2025 5:14pm

Copy link
Contributor

@dohaki dohaki left a comment

Choose a reason for hiding this comment

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

I think the overall approach makes sense to me! There are still some open questions on my side that came up while reviewing. Some might be more of a product question but would be interesting to get your view on it.

Comment on lines 456 to 457
crossSwap.type === AMOUNT_TYPE.EXACT_INPUT
? destinationSwapQuote.minAmountOut
Copy link
Contributor

Choose a reason for hiding this comment

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

This should never be the case because we are inside getCrossSwapQuotesForOutputB2A which only handles output-amount based flows

@@ -1073,6 +1136,7 @@ export async function getCrossSwapQuotesForOutputByRouteA2A(
prioritizedDestinationStrategy.result.bridgeableOutputToken,
routerAddress:
prioritizedDestinationStrategy.result.destinationRouter.address,
destinationOutputAmount: crossSwap.amount,
Copy link
Contributor

Choose a reason for hiding this comment

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

We are inside getCrossSwapQuotesForExactInputByRouteA2A so this needs to be

Suggested change
destinationOutputAmount: crossSwap.amount,
destinationOutputAmount: prioritizedDestinationStrategy.indicativeDestinationSwapQuote.minAmountOut,

Copy link
Contributor

Choose a reason for hiding this comment

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

appFee is missing here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Discussed offline about appFee - We only need to include it in the final bridgeQuote.message override.

@@ -1159,6 +1223,7 @@ export async function getCrossSwapQuotesForOutputByRouteA2A(
prioritizedDestinationStrategy.indicativeDestinationSwapQuote,
bridgeableOutputToken,
routerAddress: destinationRouter.address,
destinationOutputAmount: crossSwap.amount,
Copy link
Contributor

Choose a reason for hiding this comment

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

We are inside getCrossSwapQuotesForExactInputByRouteA2A so this needs to be

Suggested change
destinationOutputAmount: crossSwap.amount,
destinationOutputAmount: prioritizedDestinationStrategy.indicativeDestinationSwapQuote.minAmountOut,

Copy link
Contributor

Choose a reason for hiding this comment

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

appFee is missing here?

Comment on lines 466 to 472
// Get USD prices for all tokens and native tokens
const [
inputTokenPriceUsd,
outputTokenPriceUsd,
originNativePriceUsd,
destinationNativePriceUsd,
] = await Promise.all([
Copy link
Contributor

Choose a reason for hiding this comment

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

From a perf perspective it would be nice if we can pass all prices in as args. So that we do a Promise.all from within ./approval/_service.ts alongside one of the async methods so improve latency

utils.formatUnits(bridgeFees.totalRelayFee.total, outputToken.decimals)
) * outputTokenPriceUsd;

const totalFeeUsd = relayerTotalUsd + appFeeUsd;
Copy link
Contributor

Choose a reason for hiding this comment

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

We also need to price in the "swap impact". So basically

swapImpactUsd = inputAmountUsd - outputAmountUsd - relayerTotalUsd - appFeeUsd`

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated!

Comment on lines 528 to 533
pct: getNormalizedPercentage(
originGasUsd,
params.inputAmount,
inputToken.decimals,
inputTokenPriceUsd
),
Copy link
Contributor

Choose a reason for hiding this comment

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

If I think about this one, the pct doesn't really make sense to me 🤔 Because the origin gas is not priced into the total fees (at least for gasful flows)

Comment on lines 535 to 548
destinationGas: {
amount: destinationGas.total,
amountUsd: destinationGasUsd,
pct: destinationGas.pct,
},
relayerCapital: {
amount: relayerCapital.total,
amountUsd: relayerCapitalUsd,
pct: relayerCapital.pct,
},
relayerTotal: {
amount: bridgeFees.totalRelayFee.total,
amountUsd: relayerTotalUsd,
pct: bridgeFees.totalRelayFee.pct,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we can directly reuse the pct values here from the bridge quote. Because in case of origin and/or destination swaps, the swap impact is not priced into the total

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I removed pct from these for now. I am a little unclear what the percentage is meant to reflect. Is it always w.r.t input amount?

@ashwinrava ashwinrava force-pushed the app-fee-destination-swap branch from 41c8881 to c6ea809 Compare July 29, 2025 00:54
@ashwinrava ashwinrava changed the title feat: handle app fees for destination swap feat: handle app fees Jul 29, 2025
@ashwinrava ashwinrava force-pushed the app-fee-destination-swap branch from c6ea809 to a1d9510 Compare July 29, 2025 19:52
@ashwinrava ashwinrava force-pushed the app-fee-destination-swap branch 2 times, most recently from cf87020 to af01b7f Compare July 30, 2025 22:43
@ashwinrava ashwinrava force-pushed the app-fee-destination-swap branch from af01b7f to 02a8289 Compare July 30, 2025 23:59
Copy link
Contributor

@dohaki dohaki left a comment

Choose a reason for hiding this comment

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

Nice this looks awesome! I could also send some tx with app fees 💪 There are still some open questions I left on the response format of the fees. We can sync on that offline as well

amountUsd: totalFeeUsd,
pct: totalFeeUsd / inputAmountUsd,
},
originGas: {
Copy link
Contributor

Choose a reason for hiding this comment

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

In the PRD it was specified to also include the respective token information for the components. We can also add this as a fast-follow if you think the required work is a bit too much

Copy link
Contributor

Choose a reason for hiding this comment

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

So originGas would include the respective gas tokens of the chain. relayerCapital, destinationGas and relayerTotal in the bridged output token? And app in the output token


const bridgeFees = bridgeQuote.suggestedFees;
const relayerCapital = bridgeFees.relayerCapitalFee;
const destinationGas = bridgeFees.relayerGasFee;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we are missing lpFee

return {
total: {
amount: totalFeeUsd,
amountUsd: totalFeeUsd,
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably return them as strings? 🤔 To avoid scientific notation but not sure what the best practice is here


return {
total: {
amount: totalFeeUsd,
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm this should probably be in the inputToken?

Comment on lines 156 to 162
if (appFee.feeAmount.gt(0)) {
bridgeQuote.message = buildExactInputBridgeTokenMessage(
crossSwap,
bridgeQuote.outputAmount,
appFee
);
}
Copy link
Contributor

@melisaguevara melisaguevara Jul 31, 2025

Choose a reason for hiding this comment

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

Isn't this needed even if appFee.feeAmount is 0? So that the output is updated from crossSwap.amount to bridgeQuote.amount?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah good point. Will amend.

@ashwinrava ashwinrava force-pushed the app-fee-destination-swap branch from 96236f4 to d705281 Compare August 1, 2025 16:52
@ashwinrava ashwinrava changed the base branch from EPIC-swap-actions to master August 1, 2025 16:53
Copy link

linear bot commented Aug 1, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants