Skip to content

Commit 60403f7

Browse files
authored
fix(tokens): fall back to the house sell fee on the buy leg (#1305)
1 parent 79a4c4f commit 60403f7

3 files changed

Lines changed: 58 additions & 6 deletions

File tree

‎apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import com.getcode.opencode.model.core.errors.SwapError
5050
import com.getcode.opencode.model.financial.Currency
5151
import com.getcode.opencode.model.financial.CurrencyCode
5252
import com.getcode.opencode.model.financial.Fiat
53+
import com.getcode.opencode.model.financial.HOUSE_SELL_FEE_BPS
5354
import com.getcode.opencode.model.financial.Limits
5455
import com.getcode.opencode.model.financial.LocalFiat
5556
import com.getcode.opencode.model.financial.Rate
@@ -63,6 +64,7 @@ import com.getcode.opencode.model.financial.max
6364
import com.getcode.opencode.model.financial.min
6465
import com.getcode.opencode.model.financial.minus
6566
import com.getcode.opencode.model.financial.plus
67+
import com.getcode.opencode.model.financial.sellFeeBpsOrHouseRate
6668
import com.getcode.opencode.model.financial.toFiat
6769
import com.getcode.opencode.model.financial.usdf
6870
import com.getcode.opencode.model.transactions.SwapState
@@ -483,8 +485,8 @@ class SwapViewModel @Inject constructor(
483485
get() {
484486
val purpose = stateFlow.value.purpose as? SwapPurpose.Convert ?: return 0
485487
if (purpose.mint == Mint.usdf) return DEFAULT_CONVERT_FEE_BPS
486-
return stateFlow.value.tokenWithBalance?.token?.launchpadMetadata?.sellFeeBps
487-
?: DEFAULT_CONVERT_FEE_BPS
488+
return stateFlow.value.tokenWithBalance?.token?.launchpadMetadata
489+
.sellFeeBpsOrHouseRate
488490
}
489491

490492
private val feeAmount: Fiat
@@ -559,12 +561,13 @@ class SwapViewModel @Inject constructor(
559561
/**
560562
* The fee a buy funded by [fundingToken] charges, as bps and whether it rides on top of the
561563
* entered amount. A v2 Get from Dollars pays the flat house rate on top (no pool to skim);
562-
* every other currency has its pool's sell fee grossed up into the debit; v1's reserves buy is
564+
* every other currency has its pool's sell fee grossed up into the debit (falling back to the
565+
* house rate when the pool doesn't declare one, as [convertFeeBps] does); v1's reserves buy is
563566
* free.
564567
*/
565568
private fun buyFeeFor(fundingToken: Token): Pair<Int, Boolean> = when {
566569
fundingToken.address != Mint.usdf ->
567-
(fundingToken.launchpadMetadata?.sellFeeBps ?: 0) to false
570+
fundingToken.launchpadMetadata.sellFeeBpsOrHouseRate to false
568571
stateFlow.value.isGet -> DEFAULT_CONVERT_FEE_BPS to true
569572
else -> 0 to false
570573
}
@@ -1315,7 +1318,7 @@ class SwapViewModel @Inject constructor(
13151318
}
13161319
} else {
13171320
enteredFiat.grossingUpLaunchpadSellFee(
1318-
bps = fundingToken.launchpadMetadata?.sellFeeBps ?: 0,
1321+
bps = fundingToken.launchpadMetadata.sellFeeBpsOrHouseRate,
13191322
)
13201323
},
13211324
token = fundingToken,
@@ -1891,7 +1894,7 @@ class SwapViewModel @Inject constructor(
18911894
* House rate for a conversion when the source pool doesn't charge its own sell fee — 1%,
18921895
* matching the launchpad default.
18931896
*/
1894-
private const val DEFAULT_CONVERT_FEE_BPS = 100
1897+
private const val DEFAULT_CONVERT_FEE_BPS = HOUSE_SELL_FEE_BPS
18951898

18961899
val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
18971900
when (event) {

‎services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/LaunchpadSellFee.kt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,18 @@ fun Fiat.spendableUnderSellFeeOnTop(bps: Int): Fiat {
5858
val cappedBps = bps.coerceIn(0, MAX_FEE_BPS)
5959
return this / (1.0 + cappedBps / MAX_FEE_BPS.toDouble())
6060
}
61+
62+
/**
63+
* The flat house sell fee, in basis points, charged wherever a pool doesn't declare one of its own.
64+
*/
65+
const val HOUSE_SELL_FEE_BPS = 100
66+
67+
/**
68+
* This pool's declared sell fee, or the house rate when there is no pool to declare one.
69+
*
70+
* Erring high is the safe direction: the fee is grossed up into the debit, so falling back to zero
71+
* prices a debit the funding balance can't actually cover. iOS assumes the house rate on every leg
72+
* for the same reason.
73+
*/
74+
val LaunchpadMetadata?.sellFeeBpsOrHouseRate: Int
75+
get() = this?.sellFeeBps ?: HOUSE_SELL_FEE_BPS

‎services/opencode/src/test/kotlin/com/getcode/opencode/model/financial/LaunchpadSellFeeTest.kt‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.getcode.opencode.model.financial
22

33
import kotlin.test.Test
44
import kotlin.test.assertEquals
5+
import com.getcode.opencode.tests.generateRandomPublicKeyForTest
56
import kotlin.test.assertFalse
67

78
/**
@@ -110,4 +111,37 @@ class LaunchpadSellFeeTest {
110111
// Clamped to 10_000 (100%): half the balance covers a fee equal to the entry.
111112
assertEquals("$10.00", balance.spendableUnderSellFeeOnTop(bps = 12_000).formatted())
112113
}
114+
115+
// region House-rate fallback
116+
117+
@Test
118+
fun `a pool that declares its own sell fee uses it`() {
119+
assertEquals(250, pool(sellFeeBps = 250).sellFeeBpsOrHouseRate)
120+
}
121+
122+
@Test
123+
fun `a token with no pool falls back to the house rate`() {
124+
// Erring high is the safe direction — the fee is grossed up into the debit, so a zero
125+
// fallback prices a debit the funding balance can't actually cover. iOS assumes the house
126+
// rate on every leg for the same reason.
127+
assertEquals(HOUSE_SELL_FEE_BPS, null.sellFeeBpsOrHouseRate)
128+
}
129+
130+
private fun pool(sellFeeBps: Int): LaunchpadMetadata {
131+
val key = generateRandomPublicKeyForTest()
132+
return LaunchpadMetadata(
133+
currencyConfig = key,
134+
liquidityPool = key,
135+
seed = key,
136+
authority = key,
137+
mintVault = key,
138+
coreMintVault = key,
139+
currentCirculatingSupplyQuarks = 1_000_000L,
140+
sellFeeBps = sellFeeBps,
141+
price = Fiat(fiat = 1.0),
142+
marketCap = Fiat(fiat = 1000.0),
143+
)
144+
}
145+
146+
// endregion
113147
}

0 commit comments

Comments
 (0)