Repository navigation
Charge a 20 bps fee on swaps - #700
Conversation
Every 1Click quote now carries appFees with our 20 bps for liq.selfevident.near. On the confidential quotes QTC swaps use, 1Click keeps its own 20 bps on top, so a swap costs 40 bps in fees, half of them ours. 1Click appends its own entry to the echoed appFees and the quote signature does not cover them, so a quote is refused unless the echo holds our fee as sent.
n13
left a comment
There was a problem hiding this comment.
Reviewer model: Opus 5.5
Reviewed at 06e8472. The change does what it says. Every quote sends our fee, and a quote whose echo drops it is refused. swap_service_test (46 tests) and swap_flow_test (30 tests) pass locally. I found no correctness bug. The points below are about robustness, docs and disclosure.
-
The fee is hard-coded and the check fails closed, so a change on 1Click's side stops all swaps until a release.
quantus_sdk/lib/src/services/swap_service.dart:69and:243,quantus_sdk/lib/src/constants/app_constants.dart:53-54.- The check needs the echo to hold exactly
{liq.selfevident.near, 20}.appFeesis unsigned, and 1Click already rewrites it on some paths. Keyless public dry quotes made during this review echoed our 20 as 10 and our 40 as 20, with 1Click taking a cut. - Your probes show the confidential path with the partner key echoes it unchanged. But that rests on 1Click's partner terms, which we don't control. If they change, every dry and live quote throws
SwapQuoteIntegrityException. The only remote lever isenableSwap, which turns swaps off entirely. - The same applies if the recipient ever has to change. Every installed build keeps paying the old account until users update.
quantusAssetIdalready comes from remote config "so a surprising listing needs no update". I suggest passing the fee bps and recipient intoSwapServicethe same way, with a hard upper bound in code. Then a bad echo or a recipient change is a config change, not a release.
- The check needs the echo to hold exactly
-
mobile-app/docs/near-intents-swaps.mdis now wrong.- Line 119 lists what
POST /v0/quotesends and which echo fields are checked. Neither mentionsappFees. - The Fees section (154-158) says 1Click echoes its fee "on a request that sent none" and treats 1Click's fee as the only one.
- Lines 169, 192 and 220 give the user's cost and the preflight cost without our 20 bps.
Please update it in this PR.
- Line 119 lists what
-
Users are not told about the new fee. The review screen's only fee row is the QTC network fee (
mobile-app/lib/v2/screens/swap/review_swap_screen.dart:204). Our 0.2% and 1Click's 0.2% are hidden in the rate. The form's estimate (swapEstimateOut,mobile-app/lib/v2/screens/swap/swap_providers.dart:85) uses listed prices with no fees. So the gap between form and review grows from about 0.2% to about 0.4%, with nothing on screen to explain it. This is a product call, but a wallet that takes its own cut usually shows it. -
Test gaps in
quantus_sdk/test/services/swap_service_test.dart.- The only negative test (line 317) drops our recipient. Both fees are 20, so a predicate that checks only the recipient still passes the suite. Add a case with our recipient and a different fee.
- No test covers a missing
appFees. In that caseswap_service.dart:242throws a rawTypeError, notSwapQuoteIntegrityException. It still fails closed and matches the other casts in this method, so this part is minor. _quoteResponse(line 74) always addsappFeesback after...?request, so it cannot express a missing echo. A test built on it would pass for the wrong reason.
-
Small test cleanups.
_ourFee(line 23) rebuilds_appFeefrom the same constants, and line 178 then pins it to a literal. Writing the literal once in the body expectation does the same job.- The 1Click fee recipient hex is copied from
swap_service_test.dart:22intomobile-app/test/screens/swap_flow_test.dart:179.
On "Before relying on it": the withdrawal looks solvable.
- On mainnet,
intents.nearhas auth-by-predecessor enabled forliq.selfevident.near. The account can callft_withdrawormt_withdrawdirectly, andadd_public_keyto get a key for signed intents. - It has no keys and no balance there yet.
- One real swap is still needed to see whether the fee lands in the public or the confidential balance.
Verdict (advisory): Approve. None of these is a correctness bug. I would still do 1 and 2 before this reaches users.
n13
left a comment
There was a problem hiding this comment.
APPROVE (opinion) — no blocking findings at 06e84723f411103968c017b0e6302d40fd6f6d93 (base 572e6a5ea76ea9fbb4785debac3c28dcffe5a745).
Reviewed the complete four-file diff and traced preview/live quotes, exact-input/exact-output handling, response verification, and swap confirmation. Both getQuote and createSwap use _quoteJson, so the fee and echo check cover both paths. The predicate checks both our recipient and amount while allowing 1Click's additional entry. Quoted amounts continue to drive the displayed terms and deposit.
Nonblocking: update mobile-app/docs/near-intents-swaps.md:119 and its Fees section at line 152 to include the Quantus fee and echo check; the existing text still describes requests without our fee.
Validation:
dart run melos exec --concurrency=1 --scope=quantus_sdk -- 'flutter test --no-pub --reporter=expanded test/services/swap_service_test.dart': 46 passed.dart run melos exec --concurrency=1 --scope=resonance_network_wallet -- 'flutter test --no-pub --reporter=expanded test/screens/swap_flow_test.dart': 30 passed.- Workspace
dart format lib test --line-length=120 --output=none --set-exit-if-changedthrough Melos: 817 files checked, zero changes. git diff --check: passed. GitHub Analyze and dependency checks: successful at the reviewed head.- Local workspace analysis was terminated at the required 10-second cap; cold-wallet and miner completed cleanly, SDK/mobile analysis remained incomplete.
Live limitation: a keyless confidential dry-quote probe returned HTTP 401, so I could not independently confirm the configured partner's fee policy. 1Click's fee documentation describes a generic split with partner-specific exceptions. The 20+20 confidential behavior reported in this PR should remain distinguished from that generic policy. Fee settlement and withdrawal remain unverified, as the PR already notes; this review did not fund a swap.
Posted as an opinion because the authenticated reviewer account authored this PR.
Problem
Swaps earned Quantus nothing: the quote request sent no
appFees. The only fee was 1Click's own 20 bps, which goes to 1Click.Changes
appFees: [{recipient: <our NEAR account>, fee: 20}]. The values areAppConstants.oneClickFeeBpsandAppConstants.oneClickFeeRecipient, next to the referral. The recipient is a temporary implicit account for now.basic) quotes, so 1Click keeps its 20 bps and adds ours on top rather than splitting it. A swap costs 40 bps (0.40%) in fees, and half goes to us. Dry quotes made during the research confirm this:[{ours, 10}, {1Click, 20}]for a 10 bps request.appFees, and the quote signature doesn't cover them. A quote is now refused unless the echoed list contains our entry exactly as sent.Tests
swap_service_test:swap_flow_test: the fake 1Click echoes both fees.melos run analyzeis clean.Before relying on it
Fees from confidential swaps may settle in a confidential balance on NEAR Intents, which needs a signed session to read or move. One small real swap should confirm where the fee lands and that it can be withdrawn.